aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/networking/.gitignore26
-rw-r--r--libs/networking/WordPressNetworking/build.gradle59
-rw-r--r--libs/networking/WordPressNetworking/gradle.properties-example2
-rw-r--r--libs/networking/WordPressNetworking/src/main/AndroidManifest.xml3
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java13
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java93
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java19
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java8
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java10
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java259
-rw-r--r--libs/networking/build.gradle12
-rw-r--r--libs/networking/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--libs/networking/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/networking/gradlew164
-rw-r--r--libs/networking/gradlew.bat90
-rw-r--r--libs/networking/settings.gradle3
-rw-r--r--libs/utils/.gitignore25
-rw-r--r--libs/utils/WordPressUtils/build.gradle55
-rw-r--r--libs/utils/WordPressUtils/gradle.properties-example1
-rw-r--r--libs/utils/WordPressUtils/src/main/AndroidManifest.xml5
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java101
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java214
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java25
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java94
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java93
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java77
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java106
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java35
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java116
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java22
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java138
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java554
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java236
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java36
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java132
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java79
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java96
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java91
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/README.md1
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java121
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java278
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java17
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java7
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java9
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java37
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java165
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java35
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Version.java47
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java59
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java198
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java44
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java29
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHeaderTransformer.java99
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHelper.java142
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/strings.xml4
-rw-r--r--libs/utils/build.gradle0
-rw-r--r--libs/utils/gradle/wrapper/gradle-wrapper.jarbin0 -> 51348 bytes
-rw-r--r--libs/utils/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/utils/gradlew164
-rw-r--r--libs/utils/gradlew.bat90
-rw-r--r--libs/utils/settings.gradle1
-rw-r--r--libs/wpcomrest/.gitignore25
-rw-r--r--libs/wpcomrest/LICENSE-GPL279
-rw-r--r--libs/wpcomrest/LICENSE-MIT21
-rw-r--r--libs/wpcomrest/README.md26
-rw-r--r--libs/wpcomrest/WordPressComRest/build.gradle31
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/AndroidManifest.xml22
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivity.java91
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivityTest.java44
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/NotesActivity.java183
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/WPClient.java35
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/OauthTest.java32
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/RestClientTest.java26
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/util/TestExecutorService.java20
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-hdpi/ic_launcher.pngbin0 -> 9397 bytes
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-ldpi/ic_launcher.pngbin0 -> 2729 bytes
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-mdpi/ic_launcher.pngbin0 -> 5237 bytes
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-xhdpi/ic_launcher.pngbin0 -> 14383 bytes
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/login.xml23
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/main.xml16
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/note.xml25
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/raw/oauth.properties3
-rw-r--r--libs/wpcomrest/WordPressComRest/src/androidTest/res/values/strings.xml8
-rw-r--r--libs/wpcomrest/WordPressComRest/src/main/AndroidManifest.xml8
-rw-r--r--libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/Oauth.java168
-rw-r--r--libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestClient.java101
-rw-r--r--libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestRequest.java83
-rw-r--r--libs/wpcomrest/build.gradle0
-rw-r--r--libs/wpcomrest/gradle/wrapper/gradle-wrapper.jarbin0 -> 51348 bytes
-rw-r--r--libs/wpcomrest/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/wpcomrest/gradlew164
-rw-r--r--libs/wpcomrest/gradlew.bat90
-rw-r--r--libs/wpcomrest/settings.gradle1
-rw-r--r--libs/wpcomrest/test/.gitignore3
-rw-r--r--libs/wpcomrest/test/ant.properties18
-rw-r--r--libs/wpcomrest/test/app/.gitignore3
-rw-r--r--libs/wpcomrest/test/app/AndroidManifest.xml17
-rw-r--r--libs/wpcomrest/test/app/ant.properties17
-rw-r--r--libs/wpcomrest/test/app/build.xml92
-rw-r--r--libs/wpcomrest/test/app/proguard-project.txt20
-rw-r--r--libs/wpcomrest/test/app/project.properties15
-rw-r--r--libs/wpcomrest/test/build.xml92
-rw-r--r--libs/wpcomrest/test/proguard-project.txt20
-rw-r--r--libs/wpcomrest/test/project.properties14
-rw-r--r--libs/wpcomrest/tools/deploy-mvn-artifact.conf-example1
-rwxr-xr-xlibs/wpcomrest/tools/deploy-mvn-artifact.sh19
106 files changed, 6513 insertions, 0 deletions
diff --git a/libs/networking/.gitignore b/libs/networking/.gitignore
new file mode 100644
index 000000000..20679a346
--- /dev/null
+++ b/libs/networking/.gitignore
@@ -0,0 +1,26 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
+libs
diff --git a/libs/networking/WordPressNetworking/build.gradle b/libs/networking/WordPressNetworking/build.gradle
new file mode 100644
index 000000000..b0b18ab47
--- /dev/null
+++ b/libs/networking/WordPressNetworking/build.gradle
@@ -0,0 +1,59 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies { classpath 'com.android.tools.build:gradle:0.12.+' }
+}
+
+repositories {
+ mavenCentral()
+ maven { url 'http://wordpress-mobile.github.io/WordPress-Android' }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+
+android {
+ compileSdkVersion 20
+ buildToolsVersion "20.0.0"
+
+ defaultConfig {
+ applicationId "org.wordpress.android.networking"
+ minSdkVersion 14
+ targetSdkVersion 20
+ versionCode 1
+ versionName "1.0.0"
+ }
+}
+
+wordpress {
+ utils {
+ repo 'WordPress-Utils-Android'
+ subproject 'WordPressUtils'
+ artifact 'org.wordpress:wordpress-utils:1.0.+'
+ }
+ wpcomrest {
+ repo 'Automattic/android-wordpress-com-rest'
+ subproject 'WordPressComRest'
+ artifact 'com.automattic:wordpresscom-rest:1.0.0'
+ }
+}
+
+dependencies {
+ compile 'com.mcxiaoke.volley:library:1.0.+'
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ def repo_url = ""
+ if (project.hasProperty("repository")) {
+ repo_url = project.repository
+ }
+ repository(url: repo_url)
+ pom.version = android.defaultConfig.versionName
+ pom.groupId = "org.wordpress"
+ pom.artifactId = "wordpress-networking"
+ }
+ }
+}
diff --git a/libs/networking/WordPressNetworking/gradle.properties-example b/libs/networking/WordPressNetworking/gradle.properties-example
new file mode 100644
index 000000000..5a17295c3
--- /dev/null
+++ b/libs/networking/WordPressNetworking/gradle.properties-example
@@ -0,0 +1,2 @@
+wp.db_secret = wordpress
+repository = file:///Users/max/work/automattic/WordPress-Android-gh-pages/
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..24b63a6b8
--- /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(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..3e1e668ae
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java
@@ -0,0 +1,93 @@
+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 {
+ static private final String SITE_PREFIX = "https://public-api.wordpress.com/rest/v1/sites/";
+ static private final String BATCH_CALL_PREFIX = "https://public-api.wordpress.com/rest/v1/batch/?urls%5B%5D=%2Fsites%2F";
+ 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(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 url) {
+ if (url == null) {
+ return null;
+ }
+ if (url.startsWith(SITE_PREFIX) && !SITE_PREFIX.equals(url)) {
+ int marker = SITE_PREFIX.length();
+ if (url.indexOf("/", marker) < marker) {
+ return null;
+ }
+ return url.substring(marker, url.indexOf("/", marker));
+ } else if (url.startsWith(BATCH_CALL_PREFIX) && !BATCH_CALL_PREFIX.equals(url)) {
+ int marker = BATCH_CALL_PREFIX.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.toString());
+ 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..076d878e0
--- /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;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class RestClientFactory {
+ public static RestClientFactoryAbstract sFactory;
+
+ public static RestClient instantiate(RequestQueue queue) {
+ if (sFactory == null) {
+ sFactory = new RestClientFactoryDefault();
+ }
+ AppLog.v(T.UTILS, "instantiate RestClient using sFactory: " + sFactory.getClass());
+ return sFactory.make(queue);
+ }
+}
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..2e5906e4a
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java
@@ -0,0 +1,8 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public interface RestClientFactoryAbstract {
+ public RestClient make(RequestQueue queue);
+}
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..79646bb79
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java
@@ -0,0 +1,10 @@
+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);
+ }
+}
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..8b3e4e531
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java
@@ -0,0 +1,259 @@
+/**
+ * Interface to the WordPress.com REST API.
+ */
+package org.wordpress.android.networking;
+
+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.RestClient;
+import com.wordpress.rest.RestRequest;
+import com.wordpress.rest.RestRequest.ErrorListener;
+import com.wordpress.rest.RestRequest.Listener;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+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;
+
+ /**
+ * 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(RequestQueue queue, Authenticator authenticator) {
+ // load an existing access token from prefs if we have one
+ mAuthenticator = authenticator;
+ mRestClient = RestClientFactory.instantiate(queue);
+ mRestClient.setUserAgent(sUserAgent);
+ }
+
+ /**
+ * 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(String siteId, String 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/%s/comments/%s/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);
+ }
+
+ /**
+ * Get all a site's themes
+ */
+ public void getThemes(String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes?limit=%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<String, String>();
+ 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);
+ }
+
+ /**
+ * 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
+
+ RestRequest request = mRestClient.makeRequest(Method.GET, RestClient.getAbsoluteURL(path, params), 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();
+ RestRequest request = mRestClient.makeRequest(Method.GET, RestClient.getAbsoluteURL(path, params), 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) {
+ post(path, null, 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, RestClient.getAbsoluteURL(path), 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();
+ }
+}
diff --git a/libs/networking/build.gradle b/libs/networking/build.gradle
new file mode 100644
index 000000000..c69d6de80
--- /dev/null
+++ b/libs/networking/build.gradle
@@ -0,0 +1,12 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.automattic.android:gradle-wordpresslibraries:1.+'
+ }
+}
+
+allprojects {
+ apply plugin:'wordpress'
+} \ No newline at end of file
diff --git a/libs/networking/gradle/wrapper/gradle-wrapper.jar b/libs/networking/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8c0fb64a8
--- /dev/null
+++ b/libs/networking/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/networking/gradle/wrapper/gradle-wrapper.properties b/libs/networking/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..1e61d1fd3
--- /dev/null
+++ b/libs/networking/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
diff --git a/libs/networking/gradlew b/libs/networking/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/networking/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/networking/gradlew.bat b/libs/networking/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/networking/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/networking/settings.gradle b/libs/networking/settings.gradle
new file mode 100644
index 000000000..f476a5afd
--- /dev/null
+++ b/libs/networking/settings.gradle
@@ -0,0 +1,3 @@
+include ':WordPressNetworking'
+include ':libs:wpcomrest:WordPressComRest'
+include ':libs:utils:WordPressUtils'
diff --git a/libs/utils/.gitignore b/libs/utils/.gitignore
new file mode 100644
index 000000000..8babf679a
--- /dev/null
+++ b/libs/utils/.gitignore
@@ -0,0 +1,25 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
diff --git a/libs/utils/WordPressUtils/build.gradle b/libs/utils/WordPressUtils/build.gradle
new file mode 100644
index 000000000..da68c80de
--- /dev/null
+++ b/libs/utils/WordPressUtils/build.gradle
@@ -0,0 +1,55 @@
+
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.12.+'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+
+repositories {
+ mavenCentral()
+ maven { url 'http://wordpress-mobile.github.io/WordPress-Android' }
+}
+
+dependencies {
+ compile 'commons-lang:commons-lang:2.6'
+ compile 'com.mcxiaoke.volley:library:1.0.+'
+ compile 'com.github.castorflex.smoothprogressbar:library:0.4.0'
+ compile 'org.wordpress:pulltorefresh-main:+@aar' // org.wordpress version includes some fixes
+ compile 'com.android.support:support-v13:19.0.+'
+}
+
+android {
+ defaultPublishConfig 'debug'
+
+ compileSdkVersion 19
+ buildToolsVersion "19.1.0"
+
+ defaultConfig {
+ applicationId "org.wordpress.android.util"
+ versionName "1.0.2"
+ versionCode 1
+ minSdkVersion 14
+ targetSdkVersion 19
+ }
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ def repo_url = ""
+ if (project.hasProperty("repository")) {
+ repo_url = project.repository
+ }
+ repository(url: repo_url)
+ pom.version = android.defaultConfig.versionName
+ pom.groupId = "org.wordpress"
+ pom.artifactId = "wordpress-utils"
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/gradle.properties-example b/libs/utils/WordPressUtils/gradle.properties-example
new file mode 100644
index 000000000..36ceb8db2
--- /dev/null
+++ b/libs/utils/WordPressUtils/gradle.properties-example
@@ -0,0 +1 @@
+repository=file:///Users/max/work/automattic/WordPress-Android-gh-pages/
diff --git a/libs/utils/WordPressUtils/src/main/AndroidManifest.xml b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..4f3bd125a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.util">
+
+</manifest>
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java
new file mode 100644
index 000000000..76800de4c
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 wordpress.org
+ *
+ * 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 org.wordpress.android.util;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+public class AlertUtil {
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ */
+ public static void showAlert(Context context, int titleId, int messageId) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(android.R.string.ok, null)
+ .setMessage(messageId)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ */
+ public static void showAlert(Context context, int titleId, String message) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(android.R.string.ok, null)
+ .setMessage(message)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ * @param positiveButtontxt
+ * @param positiveListener
+ * @param negativeButtontxt
+ * @param negativeListener
+ */
+ public static void showAlert(Context context, int titleId, int messageId,
+ CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener,
+ CharSequence negativeButtontxt, DialogInterface.OnClickListener negativeListener) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(positiveButtontxt, positiveListener)
+ .setNegativeButton(negativeButtontxt, negativeListener)
+ .setMessage(messageId)
+ .setCancelable(false)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ * @param positiveButtontxt
+ * @param positiveListener
+ */
+ public static void showAlert(Context context, int titleId, String message,
+ CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(positiveButtontxt, positiveListener)
+ .setMessage(message)
+ .setCancelable(false)
+ .create();
+
+ dlg.show();
+ }
+}
+
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
new file mode 100644
index 000000000..f2fff1b2e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
@@ -0,0 +1,214 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * simple wrapper for Android log calls, enables recording & displaying log
+ */
+public class AppLog {
+ // T for Tag
+ public enum T {READER, EDITOR, MEDIA, NUX, API, STATS, UTILS, NOTIFS, DB, POSTS, COMMENTS, THEMES, TESTS, PROFILING, SIMPERIUM}
+ public static final String TAG = "WordPress";
+
+ private static boolean mEnableRecording = false;
+
+ private AppLog() {
+ throw new AssertionError();
+ }
+
+ /*
+ * defaults to false, pass true to capture log so it can be displayed by AppLogViewerActivity
+ */
+ public static void enableRecording(boolean enable) {
+ mEnableRecording = enable;
+ }
+
+ public static void v(T tag, String message) {
+ Log.v(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.v, message);
+ }
+
+ public static void d(T tag, String message) {
+ Log.d(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.d, message);
+ }
+
+ public static void i(T tag, String message) {
+ Log.i(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.i, message);
+ }
+
+ public static void w(T tag, String message) {
+ Log.w(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.w, message);
+ }
+
+ public static void e(T tag, String message) {
+ Log.e(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.e, message);
+ }
+
+ public static void e(T tag, String message, Throwable tr) {
+ Log.e(TAG + "-" + tag.toString(), message, tr);
+ addEntry(tag, LogLevel.e, message + " - exception: " + tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getHTMLStringStackTrace(tr));
+ }
+
+ public static void e(T tag, Throwable tr) {
+ Log.e(TAG + "-" + tag.toString(), tr.getMessage(), tr);
+ addEntry(tag, LogLevel.e, tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getHTMLStringStackTrace(tr));
+ }
+
+ public static void e(T tag, String volleyErrorMsg, int statusCode) {
+ if (TextUtils.isEmpty(volleyErrorMsg)) {
+ return;
+ }
+ String logText;
+ if (statusCode == -1) {
+ logText = volleyErrorMsg;
+ } else {
+ logText = volleyErrorMsg + ", status " + statusCode;
+ }
+ Log.e(TAG + "-" + tag.toString(), logText);
+ addEntry(tag, LogLevel.w, logText);
+ }
+
+ // --------------------------------------------------------------------------------------------------------
+
+ private static final int MAX_ENTRIES = 99;
+
+ private enum LogLevel {
+ v, d, i, w, e;
+ private String toHtmlColor() {
+ switch(this) {
+ case v:
+ return "grey";
+ case i:
+ return "black";
+ case w:
+ return "purple";
+ case e:
+ return "red";
+ case d:
+ default:
+ return "teal";
+ }
+ }
+ }
+
+ private static class LogEntry {
+ LogLevel logLevel;
+ String logText;
+ T logTag;
+
+ private String toHtml() {
+ StringBuilder sb = new StringBuilder()
+ .append("<font color='")
+ .append(logLevel.toHtmlColor())
+ .append("'>")
+ .append("[")
+ .append(logTag.name())
+ .append("] ")
+ .append(logLevel.name())
+ .append(": ")
+ .append(logText)
+ .append("</font>");
+ return sb.toString();
+ }
+ }
+
+ private static class LogEntryList extends ArrayList<LogEntry> {
+ private synchronized boolean addEntry(LogEntry entry) {
+ if (size() >= MAX_ENTRIES)
+ removeFirstEntry();
+ return add(entry);
+ }
+ private void removeFirstEntry() {
+ Iterator<LogEntry> it = iterator();
+ if (!it.hasNext())
+ return;
+ try {
+ remove(it.next());
+ } catch (NoSuchElementException e) {
+ // ignore
+ }
+ }
+ }
+
+ private static LogEntryList mLogEntries = new LogEntryList();
+
+ private static void addEntry(T tag, LogLevel level, String text) {
+ // skip if recording is disabled (default)
+ if (!mEnableRecording)
+ return;
+ LogEntry entry = new LogEntry();
+ entry.logLevel = level;
+ entry.logText = text;
+ entry.logTag = tag;
+ mLogEntries.addEntry(entry);
+ }
+
+ private static String getStringStackTrace(Throwable throwable) {
+ StringWriter errors = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(errors));
+ return errors.toString();
+ }
+
+ private static String getHTMLStringStackTrace(Throwable throwable) {
+ return getStringStackTrace(throwable).replace("\n", "<br/>");
+ }
+
+ /*
+ * returns entire log as html for display (see AppLogViewerActivity)
+ */
+ public static String toHtml(Context context) {
+ StringBuilder sb = new StringBuilder();
+
+ // add version & device info
+ sb.append("WordPress Android version: " + ProfilingUtils.getVersionName(context)).append("<br />")
+ .append("Android device name: " + DeviceUtils.getInstance().getDeviceName(context)).append("<br />");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ int lineNum = 1;
+ while (it.hasNext()) {
+ sb.append("<font color='silver'>")
+ .append(String.format("%02d", lineNum))
+ .append("</font> ")
+ .append(it.next().toHtml())
+ .append("<br />");
+ lineNum++;
+ }
+ return sb.toString();
+ }
+
+
+ /*
+ * returns entire log as plain text
+ */
+ public static String toPlainText(Context context) {
+ StringBuilder sb = new StringBuilder();
+
+ // add version & device info
+ sb.append("WordPress Android version: " + ProfilingUtils.getVersionName(context)).append("\n")
+ .append("Android device name: " + DeviceUtils.getInstance().getDeviceName(context)).append("\n\n");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ int lineNum = 1;
+ while (it.hasNext()) {
+ sb.append(String.format("%02d - ", lineNum))
+ .append(it.next().logText)
+ .append("\n");
+ lineNum++;
+ }
+ return sb.toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
new file mode 100644
index 000000000..166085a4f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.util;
+
+import java.util.Comparator;
+import java.util.Map;
+
+public class BlogUtils {
+ public static Comparator<Object> BlogNameComparator = new Comparator<Object>() {
+ public int compare(Object blog1, Object blog2) {
+ Map<Object, Object> blogMap1 = (Map<Object, Object>) blog1;
+ Map<Object, Object> blogMap2 = (Map<Object, Object>) blog2;
+
+ String blogName1 = MapUtils.getMapStr(blogMap1, "blogName");
+ if (blogName1.length() == 0) {
+ blogName1 = MapUtils.getMapStr(blogMap1, "url");
+ }
+
+ String blogName2 = MapUtils.getMapStr(blogMap2, "blogName");
+ if (blogName2.length() == 0) {
+ blogName2 = MapUtils.getMapStr(blogMap2, "url");
+ }
+
+ return blogName1.compareToIgnoreCase(blogName2);
+ }
+ };
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java
new file mode 100644
index 000000000..639d5479c
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java
@@ -0,0 +1,94 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class DeviceUtils {
+ private static DeviceUtils instance;
+ private boolean isKindleFire = false;
+
+ public boolean isKindleFire() {
+ return isKindleFire;
+ }
+
+ public static DeviceUtils getInstance() {
+ if (instance == null) {
+ instance = new DeviceUtils();
+ }
+ return instance;
+ }
+
+ private DeviceUtils() {
+ isKindleFire = android.os.Build.MODEL.equalsIgnoreCase("kindle fire") ? true: false;
+ }
+
+ /**
+ * Checks camera availability recursively based on API level.
+ *
+ * TODO: change "android.hardware.camera.front" and "android.hardware.camera.any" to
+ * {@link PackageManager#FEATURE_CAMERA_FRONT} and {@link PackageManager#FEATURE_CAMERA_ANY},
+ * respectively, once they become accessible or minSdk version is incremented.
+ *
+ * @param context The context.
+ * @return Whether camera is available.
+ */
+ public boolean hasCamera(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
+ || pm.hasSystemFeature("android.hardware.camera.front");
+ }
+
+ return pm.hasSystemFeature("android.hardware.camera.any");
+ }
+
+ public String getDeviceName(Context context) {
+ String manufacturer = Build.MANUFACTURER;
+ String undecodedModel = Build.MODEL;
+ String model = null;
+
+ try {
+ Properties prop = new Properties();
+ InputStream fileStream;
+ // Read the device name from a precomplied list:
+ // see http://making.meetup.com/post/29648976176/human-readble-android-device-names
+ fileStream = context.getAssets().open("android_models.properties");
+ prop.load(fileStream);
+ fileStream.close();
+ String decodedModel = prop.getProperty(undecodedModel.replaceAll(" ", "_"));
+ if (decodedModel != null && !decodedModel.trim().equals("")) {
+ model = decodedModel;
+ }
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e.getMessage());
+ }
+
+ if (model == null) { //Device model not found in the list
+ if (undecodedModel.startsWith(manufacturer)) {
+ model = capitalize(undecodedModel);
+ } else {
+ model = capitalize(manufacturer) + " " + undecodedModel;
+ }
+ }
+ return model;
+ }
+
+ private String capitalize(String s) {
+ if (s == null || s.length() == 0) {
+ return "";
+ }
+ char first = s.charAt(0);
+ if (Character.isUpperCase(first)) {
+ return s;
+ } else {
+ return Character.toUpperCase(first) + s.substring(1);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
new file mode 100644
index 000000000..f64527e9a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
@@ -0,0 +1,93 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class DisplayUtils {
+ private DisplayUtils() {
+ throw new AssertionError();
+ }
+
+ public static boolean isLandscape(Context context) {
+ if (context == null)
+ return false;
+ return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ public static boolean isLandscapeTablet(Context context) {
+ return isLandscape(context) && isTablet(context);
+ }
+
+ public static Point getDisplayPixelSize(Context context) {
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ return size;
+ }
+
+ public static int getDisplayPixelWidth(Context context) {
+ Point size = getDisplayPixelSize(context);
+ return (size.x);
+ }
+
+ public static int getDisplayPixelHeight(Context context) {
+ Point size = getDisplayPixelSize(context);
+ return (size.y);
+ }
+
+ public static int dpToPx(Context context, int dp) {
+ float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
+ return (int) px;
+ }
+
+ public static int pxToDp(Context context, int px) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return (int) ((px/displayMetrics.density)+0.5);
+ }
+
+ public static boolean isTablet(Context context) {
+ // http://stackoverflow.com/a/8427523/1673548
+ if (context == null)
+ return false;
+ return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
+ }
+
+ public static boolean isXLarge(Context context) {
+ if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE)
+ return true;
+ return false;
+ }
+
+ /**
+ * returns the height of the ActionBar if one is enabled - supports both the native ActionBar
+ * and ActionBarSherlock - http://stackoverflow.com/a/15476793/1673548
+ */
+ public static int getActionBarHeight(Context context) {
+ if (context == null) {
+ return 0;
+ }
+ TypedValue tv = new TypedValue();
+ if (context.getTheme() != null
+ && context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
+ return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
+ }
+
+ // if we get this far, it's because the device doesn't support an ActionBar,
+ // so return the standard ActionBar height (48dp)
+ return dpToPx(context, 48);
+ }
+
+ /**
+ * detect when FEATURE_ACTION_BAR_OVERLAY has been set
+ */
+ public static boolean hasActionBarOverlay(Window window) {
+ return window.hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
new file mode 100644
index 000000000..64ee67e56
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+/**
+ * EditText utils
+ */
+public class EditTextUtils {
+ private EditTextUtils() {
+ throw new AssertionError();
+ }
+
+ /**
+ * returns text string from passed EditText
+ */
+ public static String getText(EditText edit) {
+ if (edit.getText() == null) {
+ return "";
+ }
+ return edit.getText().toString();
+ }
+
+ /**
+ * moves caret to end of text
+ */
+ public static void moveToEnd(EditText edit) {
+ if (edit.getText() == null) {
+ return;
+ }
+ edit.setSelection(edit.getText().toString().length());
+ }
+
+ /**
+ * returns true if nothing has been entered into passed editor
+ */
+ public static boolean isEmpty(EditText edit) {
+ return TextUtils.isEmpty(getText(edit));
+ }
+
+ /**
+ * hide the soft keyboard for the passed EditText
+ */
+ public static void hideSoftInput(EditText edit) {
+ if (edit == null) {
+ return;
+ }
+
+ InputMethodManager imm = getInputMethodManager(edit);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * show the soft keyboard for the passed EditText
+ */
+ public static void showSoftInput(EditText edit) {
+ if (edit == null) {
+ return;
+ }
+
+ edit.requestFocus();
+
+ InputMethodManager imm = getInputMethodManager(edit);
+ if (imm != null) {
+ imm.showSoftInput(edit, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+
+ private static InputMethodManager getInputMethodManager(EditText edit) {
+ Context context = edit.getContext();
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java
new file mode 100644
index 000000000..5a7566a96
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java
@@ -0,0 +1,106 @@
+package org.wordpress.android.util;
+
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.util.SparseArray;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES;
+
+public class Emoticons {
+ public static final int EMOTICON_COLOR = 0xFF21759B;
+ private static final boolean HAS_EMOJI = SDK_INT >= VERSION_CODES.JELLY_BEAN;
+ private static final Map<String, String> wpSmilies;
+ public static final SparseArray<String> wpSmiliesCodePointToText;
+
+ static {
+ Map<String, String> smilies = new HashMap<String, String>();
+ smilies.put("icon_mrgreen.gif", HAS_EMOJI ? "\uD83D\uDE00" : ":mrgreen:" );
+ smilies.put("icon_neutral.gif", HAS_EMOJI ? "\uD83D\uDE14" : ":|" );
+ smilies.put("icon_twisted.gif", HAS_EMOJI ? "\uD83D\uDE16" : ":twisted:" );
+ smilies.put("icon_arrow.gif", HAS_EMOJI ? "\u27A1" : ":arrow:" );
+ smilies.put("icon_eek.gif", HAS_EMOJI ? "\uD83D\uDE32" : "8-O" );
+ smilies.put("icon_smile.gif", HAS_EMOJI ? "\uD83D\uDE0A" : ":)" );
+ smilies.put("icon_confused.gif", HAS_EMOJI ? "\uD83D\uDE15" : ":?" );
+ smilies.put("icon_cool.gif", HAS_EMOJI ? "\uD83D\uDE0A" : "8)" );
+ smilies.put("icon_evil.gif", HAS_EMOJI ? "\uD83D\uDE21" : ":evil:" );
+ smilies.put("icon_biggrin.gif", HAS_EMOJI ? "\uD83D\uDE03" : ":D" );
+ smilies.put("icon_idea.gif", HAS_EMOJI ? "\uD83D\uDCA1" : ":idea:" );
+ smilies.put("icon_redface.gif", HAS_EMOJI ? "\uD83D\uDE33" : ":oops:" );
+ smilies.put("icon_razz.gif", HAS_EMOJI ? "\uD83D\uDE1D" : ":P" );
+ smilies.put("icon_rolleyes.gif", HAS_EMOJI ? "\uD83D\uDE0F" : ":roll:" );
+ smilies.put("icon_wink.gif", HAS_EMOJI ? "\uD83D\uDE09" : ";)" );
+ smilies.put("icon_cry.gif", HAS_EMOJI ? "\uD83D\uDE22" : ":'(" );
+ smilies.put("icon_surprised.gif", HAS_EMOJI ? "\uD83D\uDE32" : ":o" );
+ smilies.put("icon_lol.gif", HAS_EMOJI ? "\uD83D\uDE03" : ":lol:" );
+ smilies.put("icon_mad.gif", HAS_EMOJI ? "\uD83D\uDE21" : ":x" );
+ smilies.put("icon_sad.gif", HAS_EMOJI ? "\uD83D\uDE1E" : ":(" );
+ smilies.put("icon_exclaim.gif", HAS_EMOJI ? "\u2757" : ":!:" );
+ smilies.put("icon_question.gif", HAS_EMOJI ? "\u2753" : ":?:" );
+
+ wpSmilies = Collections.unmodifiableMap(smilies);
+
+ wpSmiliesCodePointToText = new SparseArray<String>(20);
+ wpSmiliesCodePointToText.put(10145, ":arrow:");
+ wpSmiliesCodePointToText.put(128161, ":idea:");
+ wpSmiliesCodePointToText.put(128512, ":mrgreen:");
+ wpSmiliesCodePointToText.put(128515, ":D");
+ wpSmiliesCodePointToText.put(128522, ":)");
+ wpSmiliesCodePointToText.put(128521, ";)");
+ wpSmiliesCodePointToText.put(128532, ":|");
+ wpSmiliesCodePointToText.put(128533, ":?");
+ wpSmiliesCodePointToText.put(128534, ":twisted:");
+ wpSmiliesCodePointToText.put(128542, ":(");
+ wpSmiliesCodePointToText.put(128545, ":evil:");
+ wpSmiliesCodePointToText.put(128546, ":'(");
+ wpSmiliesCodePointToText.put(128562, ":o");
+ wpSmiliesCodePointToText.put(128563, ":oops:");
+ wpSmiliesCodePointToText.put(128527, ":roll:");
+ wpSmiliesCodePointToText.put(10071, ":!:");
+ wpSmiliesCodePointToText.put(10067, ":?:");
+ }
+
+ public static String lookupImageSmiley(String url){
+ return lookupImageSmiley(url, "");
+ }
+
+ public static String lookupImageSmiley(String url, String ifNone){
+ String file = url.substring(url.lastIndexOf("/") + 1);
+ if (wpSmilies.containsKey(file)) {
+ return wpSmilies.get(file);
+ }
+ return ifNone;
+ }
+
+ public static Spanned replaceEmoticonsWithEmoji(SpannableStringBuilder html){
+ ImageSpan imgs[] = html.getSpans(0, html.length(), ImageSpan.class);
+ for (ImageSpan img : imgs) {
+ String emoticon = Emoticons.lookupImageSmiley(img.getSource());
+ if (!emoticon.equals("")) {
+ int start = html.getSpanStart(img);
+ html.replace(start, html.getSpanEnd(img), emoticon);
+ html.setSpan(new ForegroundColorSpan(EMOTICON_COLOR), start,
+ start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ html.removeSpan(img);
+ }
+ }
+ return html;
+ }
+
+ public static String replaceEmoticonsWithEmoji(final String text) {
+ if (text != null && text.contains("icon_")) {
+ final SpannableStringBuilder html = (SpannableStringBuilder)replaceEmoticonsWithEmoji((SpannableStringBuilder) Html.fromHtml(text));
+ // Html.toHtml() is used here rather than toString() since the latter strips html
+ return Html.toHtml(html);
+ } else {
+ return text;
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java
new file mode 100644
index 000000000..28282ed5f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.util;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+public class FormatUtils {
+ /*
+ * NumberFormat isn't synchronized, so a separate instance must be created for each thread
+ * http://developer.android.com/reference/java/text/NumberFormat.html
+ */
+ private static final ThreadLocal<NumberFormat> IntegerInstance = new ThreadLocal<NumberFormat>() {
+ @Override
+ protected NumberFormat initialValue() {
+ return NumberFormat.getIntegerInstance();
+ }
+ };
+
+ private static final ThreadLocal<DecimalFormat> DecimalInstance = new ThreadLocal<DecimalFormat>() {
+ @Override
+ protected DecimalFormat initialValue() {
+ return (DecimalFormat) DecimalFormat.getInstance();
+ }
+ };
+
+ /*
+ * returns the passed integer formatted with thousands-separators based on the current locale
+ */
+ public static final String formatInt(int value) {
+ return IntegerInstance.get().format(value).toString();
+ }
+
+ public static final String formatDecimal(int value) {
+ return DecimalInstance.get().format(value).toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java
new file mode 100644
index 000000000..e861a88b8
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java
@@ -0,0 +1,116 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+public final class GeocoderUtils {
+ private GeocoderUtils() {
+ throw new AssertionError();
+ }
+
+ public static Geocoder getGeocoder(Context context) {
+ // first make sure a Geocoder service exists on this device (requires API 9)
+ if (!Geocoder.isPresent()) {
+ return null;
+ }
+
+ Geocoder gcd;
+
+ try {
+ gcd = new Geocoder(context, Locale.getDefault());
+ } catch (NullPointerException cannotIstantiateEx) {
+ AppLog.e(AppLog.T.UTILS, "Cannot instantiate Geocoder", cannotIstantiateEx);
+ return null;
+ }
+
+ return gcd;
+ }
+
+ public static Address getAddressFromCoords(Context context, double latitude, double longitude) {
+ Address address = null;
+ List<Address> addresses = null;
+
+ Geocoder gcd = getGeocoder(context);
+
+ if (gcd == null) {
+ return null;
+ }
+
+ try {
+ addresses = gcd.getFromLocation(latitude, longitude, 1);
+ } catch (IOException e) {
+ // may get "Unable to parse response from server" IOException here if Geocoder
+ // service is hit too frequently
+ AppLog.e(AppLog.T.UTILS,
+ "Unable to parse response from server. Is Geocoder service hitting the server too frequently?",
+ e
+ );
+ }
+
+ // addresses may be null or empty if network isn't connected
+ if (addresses != null && addresses.size() > 0) {
+ address = addresses.get(0);
+ }
+
+ return address;
+ }
+
+ public static Address getAddressFromLocationName(Context context, String locationName) {
+ int maxResults = 1;
+ Address address = null;
+ List<Address> addresses = null;
+
+ Geocoder gcd = getGeocoder(context);
+
+ if (gcd == null) {
+ return null;
+ }
+
+ try {
+ addresses = gcd.getFromLocationName(locationName, maxResults);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, "Failed to get coordinates from location", e);
+ }
+
+ // addresses may be null or empty if network isn't connected
+ if (addresses != null && addresses.size() > 0) {
+ address = addresses.get(0);
+ }
+
+ return address;
+ }
+
+ public static String getLocationNameFromAddress(Address address) {
+ String locality = "", adminArea = "", country = "";
+ if (address.getLocality() != null) {
+ locality = address.getLocality();
+ }
+
+ if (address.getAdminArea() != null) {
+ adminArea = address.getAdminArea();
+ }
+
+ if (address.getCountryName() != null) {
+ country = address.getCountryName();
+ }
+
+ return ((locality.equals("")) ? locality : locality + ", ")
+ + ((adminArea.equals("")) ? adminArea : adminArea + " ") + country;
+ }
+
+ public static double[] getCoordsFromAddress(Address address) {
+ double[] coordinates = new double[2];
+
+ if (address.hasLatitude() && address.hasLongitude()) {
+ coordinates[0] = address.getLatitude();
+ coordinates[1] = address.getLongitude();
+ }
+
+ return coordinates;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
new file mode 100644
index 000000000..c10ce69c8
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
@@ -0,0 +1,22 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+public class GravatarUtils {
+ /*
+ * see https://en.gravatar.com/site/implement/images/
+ */
+ public static String gravatarUrlFromEmail(final String email, int size) {
+ if (TextUtils.isEmpty(email))
+ return "";
+
+ String url = "http://gravatar.com/avatar/"
+ + StringUtils.getMd5Hash(email)
+ + "?d=mm";
+
+ if (size > 0)
+ url += "&s=" + Integer.toString(size);
+
+ return url;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
new file mode 100644
index 000000000..c79fe0ecb
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.QuoteSpan;
+
+import org.apache.commons.lang.StringEscapeUtils;
+
+public class HtmlUtils {
+ /*
+ * removes html from the passed string - relies on Html.fromHtml which handles invalid HTML,
+ * but it's very slow, so avoid using this where performance is important
+ */
+ public static String stripHtml(final String text) {
+ if (TextUtils.isEmpty(text)) {
+ return "";
+ }
+ return Html.fromHtml(text).toString().trim();
+ }
+
+ /*
+ * this is much faster than stripHtml() but should only be used when we know the html is valid
+ * since the regex will be unpredictable with invalid html
+ */
+ public static String fastStripHtml(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return str;
+ }
+
+ // insert a line break before P tags unless the only one is at the start
+ if (str.lastIndexOf("<p") > 0) {
+ str = str.replaceAll("<p(.|\n)*?>", "\n<p>");
+ }
+
+ // convert BR tags to line breaks
+ if (str.contains("<br")) {
+ str = str.replaceAll("<br(.|\n)*?>", "\n");
+ }
+
+ // use regex to strip tags, then convert entities in the result
+ return trimStart(fastUnescapeHtml(str.replaceAll("<(.|\n)*?>", "")));
+ }
+
+ /*
+ * same as apache.commons.lang.StringUtils.stripStart() but also removes non-breaking
+ * space (160) chars
+ */
+ private static String trimStart(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return "";
+ }
+ int start = 0;
+ while (start != strLen && (Character.isWhitespace(str.charAt(start)) || str.charAt(start) == 160)) {
+ start++;
+ }
+ return str.substring(start);
+ }
+
+ /*
+ * convert html entities to actual Unicode characters - relies on commons apache lang
+ */
+ public static String fastUnescapeHtml(final String text) {
+ if (text == null || !text.contains("&")) {
+ return text;
+ }
+ return StringEscapeUtils.unescapeHtml(text);
+ }
+
+ /*
+ * converts an R.color.xxx resource to an HTML hex color
+ */
+ public static String colorResToHtmlColor(Context context, int resId) {
+ try {
+ return String.format("#%06X", 0xFFFFFF & context.getResources().getColor(resId));
+ } catch (Resources.NotFoundException e) {
+ return "#000000";
+ }
+ }
+
+ /*
+ * remove <script>..</script> blocks from the passed string - added to project after noticing
+ * comments on posts that use the "Sociable" plugin ( http://wordpress.org/plugins/sociable/ )
+ * may have a script block which contains <!--//--> followed by a CDATA section followed by <!]]>,
+ * all of which will show up if we don't strip it here (example: http://cl.ly/image/0J0N3z3h1i04 )
+ * first seen at http://houseofgeekery.com/2013/11/03/13-terrible-x-men-we-wont-see-in-the-movies/
+ */
+ public static String stripScript(final String text) {
+ if (text == null) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder(text);
+ int start = sb.indexOf("<script");
+
+ while (start > -1) {
+ int end = sb.indexOf("</script>", start);
+ if (end == -1) {
+ return sb.toString();
+ }
+ sb.delete(start, end + 9);
+ start = sb.indexOf("<script", start);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * an alternative to Html.fromHtml() supporting <ul>, <ol>, <blockquote> tags and replacing Emoticons with Emojis
+ */
+ public static SpannableStringBuilder fromHtml(String source, WPImageGetter wpImageGetter) {
+ SpannableStringBuilder html;
+ try {
+ html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, new WPHtmlTagHandler());
+ } catch (RuntimeException runtimeException) {
+ // In case our tag handler fails
+ html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, null);
+ }
+ Emoticons.replaceEmoticonsWithEmoji(html);
+ QuoteSpan spans[] = html.getSpans(0, html.length(), QuoteSpan.class);
+ for (QuoteSpan span : spans) {
+ html.setSpan(new WPQuoteSpan(), html.getSpanStart(span), html.getSpanEnd(span), html.getSpanFlags(span));
+ html.setSpan(new ForegroundColorSpan(0xFF666666), html.getSpanStart(span), html.getSpanEnd(span),
+ html.getSpanFlags(span));
+ html.removeSpan(span);
+ }
+ return html;
+ }
+
+ public static Spanned fromHtml(String source) {
+ return fromHtml(source, null);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
new file mode 100644
index 000000000..31dadc911
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
@@ -0,0 +1,554 @@
+package org.wordpress.android.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.widget.ImageView;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+
+public class ImageUtils {
+ public static int[] getImageSize(Uri uri, Context context){
+ String path = null;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ if (uri.toString().contains("content:")) {
+ String[] projection = new String[] { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };
+ Cursor cur = context.getContentResolver().query(uri, projection, null, null, null);
+ if (cur != null) {
+ if (cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ path = cur.getString(dataColumn);
+ }
+ cur.close();
+ }
+ }
+
+ if (TextUtils.isEmpty(path)) {
+ //The file isn't ContentResolver, or it can't be access by ContentResolver. Try to access the file directly.
+ path = uri.toString().replace("content://media", "");
+ path = path.replace("file://", "");
+ }
+
+ BitmapFactory.decodeFile(path, options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ return new int[]{imageWidth, imageHeight};
+ }
+
+ // Read the orientation from ContentResolver. If it fails, read from EXIF.
+ public static int getImageOrientation(Context ctx, String filePath) {
+ Uri curStream;
+ int orientation = 0;
+
+ // Remove file protocol
+ filePath = filePath.replace("file://", "");
+
+ if (!filePath.contains("content://"))
+ curStream = Uri.parse("content://media" + filePath);
+ else
+ curStream = Uri.parse(filePath);
+
+ try {
+ Cursor cur = ctx.getContentResolver().query(curStream, new String[]{MediaStore.Images.Media.ORIENTATION}, null, null, null);
+ if (cur != null) {
+ if (cur.moveToFirst()) {
+ orientation = cur.getInt(cur.getColumnIndex(MediaStore.Images.Media.ORIENTATION));
+ }
+ cur.close();
+ }
+ } catch (Exception errReadingContentResolver) {
+ AppLog.e(AppLog.T.UTILS, errReadingContentResolver);
+ }
+
+ if (orientation == 0) {
+ orientation = getExifOrientation(filePath);
+ }
+
+ return orientation;
+ }
+
+
+ public static int getExifOrientation(String path) {
+ ExifInterface exif;
+ try {
+ exif = new ExifInterface(path);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return 0;
+ }
+
+ int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
+
+ switch (exifOrientation) {
+ case ExifInterface.ORIENTATION_NORMAL:
+ return 0;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return 90;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return 180;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ public static Bitmap downloadBitmap(String url) {
+ final DefaultHttpClient client = new DefaultHttpClient();
+
+ final HttpGet getRequest = new HttpGet(url);
+
+ try {
+ HttpResponse response = client.execute(getRequest);
+ final int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode != HttpStatus.SC_OK) {
+ AppLog.w(AppLog.T.UTILS, "ImageDownloader Error " + statusCode
+ + " while retrieving bitmap from " + url);
+ return null;
+ }
+
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ InputStream inputStream = null;
+ try {
+ inputStream = entity.getContent();
+ return BitmapFactory.decodeStream(inputStream);
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ entity.consumeContent();
+ }
+ }
+ } catch (Exception e) {
+ // Could provide a more explicit error message for IOException or
+ // IllegalStateException
+ getRequest.abort();
+ AppLog.w(AppLog.T.UTILS, "ImageDownloader Error while retrieving bitmap from " + url);
+ }
+ return null;
+ }
+
+ /** From http://developer.android.com/training/displaying-bitmaps/load-bitmap.html **/
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ // Calculate ratios of height and width to requested height and width
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+
+ // Choose the smallest ratio as inSampleSize value, this will guarantee
+ // a final image with both dimensions larger than or equal to the
+ // requested height and width.
+ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ return inSampleSize;
+ }
+
+
+ public interface BitmapWorkerCallback {
+ public void onBitmapReady(String filePath, ImageView imageView, Bitmap bitmap);
+ }
+
+ public static class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private final BitmapWorkerCallback callback;
+ private int targetWidth;
+ private int targetHeight;
+ private String path;
+
+ public BitmapWorkerTask(ImageView imageView, int width, int height, BitmapWorkerCallback callback) {
+ // Use a WeakReference to ensure the ImageView can be garbage collected
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ this.callback = callback;
+ targetWidth = width;
+ targetHeight = height;
+ }
+
+ // Decode image in background.
+ @Override
+ protected Bitmap doInBackground(String... params) {
+ path = params[0];
+
+ BitmapFactory.Options bfo = new BitmapFactory.Options();
+ bfo.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, bfo);
+
+ bfo.inSampleSize = calculateInSampleSize(bfo, targetWidth, targetHeight);
+ bfo.inJustDecodeBounds = false;
+
+ // get proper rotation
+ int bitmapWidth = 0;
+ int bitmapHeight = 0;
+ try {
+ File f = new File(path);
+ ExifInterface exif = new ExifInterface(f.getPath());
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ int angle = 0;
+ if (orientation == ExifInterface.ORIENTATION_NORMAL) { // no need to rotate
+ return BitmapFactory.decodeFile(path, bfo);
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
+ angle = 90;
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
+ angle = 180;
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
+ angle = 270;
+ }
+
+ Matrix mat = new Matrix();
+ mat.postRotate(angle);
+
+ try {
+ Bitmap bmp = BitmapFactory.decodeStream(new FileInputStream(f), null, bfo);
+ if (bmp == null) {
+ AppLog.e(AppLog.T.UTILS, "can't decode bitmap: " + f.getPath());
+ return null;
+ }
+ bitmapWidth = bmp.getWidth();
+ bitmapHeight = bmp.getHeight();
+ return Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), mat, true);
+ } catch (OutOfMemoryError oom) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + oom);
+ }
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, "Error in setting image", e);
+ }
+
+ return null;
+ }
+
+ // Once complete, see if ImageView is still around and set bitmap.
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (imageViewReference == null || bitmap == null)
+ return;
+
+ final ImageView imageView = imageViewReference.get();
+
+ if (callback != null)
+ callback.onBitmapReady(path, imageView, bitmap);
+
+ }
+ }
+
+
+ public static String getTitleForWPImageSpan(Context ctx, String filePath) {
+ if (filePath == null)
+ return null;
+
+ Uri curStream;
+ String title;
+
+ if (!filePath.contains("content://"))
+ curStream = Uri.parse("content://media" + filePath);
+ else
+ curStream = Uri.parse(filePath);
+
+ if (filePath.contains("video")) {
+ return "Video";
+ } else {
+ String[] projection = new String[] { MediaStore.Images.Thumbnails.DATA };
+
+ Cursor cur;
+ try {
+ cur = ctx.getContentResolver().query(curStream, projection, null, null, null);
+ } catch (Exception e1) {
+ AppLog.e(AppLog.T.UTILS, e1);
+ return null;
+ }
+ File jpeg;
+ if (cur != null) {
+ String thumbData = "";
+ if (cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ thumbData = cur.getString(dataColumn);
+ }
+ cur.close();
+ if (thumbData == null) {
+ return null;
+ }
+ jpeg = new File(thumbData);
+ } else {
+ String path = filePath.toString().replace("file://", "");
+ jpeg = new File(path);
+ }
+ title = jpeg.getName();
+ return title;
+ }
+ }
+
+ /**
+ * Resizes an image to be placed in the Post Content Editor
+ *
+ * @return resized bitmap
+ */
+ public static Bitmap getWPImageSpanThumbnailFromFilePath(Context context, String filePath, int targetWidth) {
+ if (filePath == null || context == null) {
+ return null;
+ }
+
+ Uri curUri;
+ if (!filePath.contains("content://")) {
+ curUri = Uri.parse("content://media" + filePath);
+ } else {
+ curUri = Uri.parse(filePath);
+ }
+
+ if (filePath.contains("video")) {
+ // Load the video thumbnail from the MediaStore
+ int videoId = 0;
+ try {
+ videoId = Integer.parseInt(curUri.getLastPathSegment());
+ } catch (NumberFormatException e) {
+ }
+ ContentResolver crThumb = context.getContentResolver();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+ Bitmap videoThumbnail = MediaStore.Video.Thumbnails.getThumbnail(crThumb, videoId, MediaStore.Video.Thumbnails.MINI_KIND,
+ options);
+ if (videoThumbnail != null) {
+ return getScaledBitmapAtLongestSide(videoThumbnail, targetWidth);
+ } else {
+ return null;
+ }
+ } else {
+ // Create resized bitmap
+ int rotation = getImageOrientation(context, filePath);
+ byte[] bytes = createThumbnailFromUri(context, curUri, targetWidth, null, rotation);
+
+ if (bytes != null && bytes.length > 0) {
+ try {
+ Bitmap resizedBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ if (resizedBitmap != null) {
+ return getScaledBitmapAtLongestSide(resizedBitmap, targetWidth);
+ }
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /*
+ Resize a bitmap to the targetSize on its longest side.
+ */
+ public static Bitmap getScaledBitmapAtLongestSide(Bitmap bitmap, int targetSize) {
+ if (bitmap.getWidth() <= targetSize && bitmap.getHeight() <= targetSize) {
+ // Do not resize.
+ return bitmap;
+ }
+
+ int targetWidth, targetHeight;
+ if (bitmap.getHeight() > bitmap.getWidth()) {
+ // Resize portrait bitmap
+ targetHeight = targetSize;
+ float percentage = (float) targetSize / bitmap.getHeight();
+ targetWidth = (int)(bitmap.getWidth() * percentage);
+ } else {
+ // Resize landscape or square image
+ targetWidth = targetSize;
+ float percentage = (float) targetSize / bitmap.getWidth();
+ targetHeight = (int)(bitmap.getHeight() * percentage);
+ }
+
+ return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);
+ }
+
+ /**
+ * nbradbury - 21-Feb-2014 - similar to createThumbnail but more efficient since it doesn't
+ * require passing the full-size image as an array of bytes[]
+ */
+ public static byte[] createThumbnailFromUri(Context context,
+ Uri imageUri,
+ int maxWidth,
+ String fileExtension,
+ int rotation) {
+ if (context == null || imageUri == null || maxWidth <= 0)
+ return null;
+
+ String filePath = null;
+ if (imageUri.toString().contains("content:")) {
+ String[] projection = new String[] { MediaStore.Images.Media.DATA };
+ Cursor cur = context.getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null) {
+ if (cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ filePath = cur.getString(dataColumn);
+ }
+ cur.close();
+ }
+ }
+
+ if (TextUtils.isEmpty(filePath)) {
+ //access the file directly
+ filePath = imageUri.toString().replace("content://media", "");
+ filePath = filePath.replace("file://", "");
+ }
+
+ // get just the image bounds
+ BitmapFactory.Options optBounds = new BitmapFactory.Options();
+ optBounds.inJustDecodeBounds = true;
+
+ try {
+ BitmapFactory.decodeFile(filePath, optBounds);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+
+ // determine correct scale value (should be power of 2)
+ // http://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/3549021#3549021
+ int scale = 1;
+ if (maxWidth > 0 && optBounds.outWidth > maxWidth) {
+ double d = Math.pow(2, (int) Math.round(Math.log(maxWidth / (double) optBounds.outWidth) / Math.log(0.5)));
+ scale = (int) d;
+ }
+
+ BitmapFactory.Options optActual = new BitmapFactory.Options();
+ optActual.inSampleSize = scale;
+
+ // Get the roughly resized bitmap
+ Bitmap bmpResized;
+ try {
+ bmpResized = BitmapFactory.decodeFile(filePath, optActual);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+
+ if (bmpResized == null)
+ return null;
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ // Now calculate exact scale in order to resize accurately
+ float percentage = (float) maxWidth / bmpResized.getWidth();
+ float proportionateHeight = bmpResized.getHeight() * percentage;
+ int finalHeight = (int) Math.rint(proportionateHeight);
+
+ float scaleWidth = ((float) maxWidth) / bmpResized.getWidth();
+ float scaleHeight = ((float) finalHeight) / bmpResized.getHeight();
+
+ float scaleBy = Math.min(scaleWidth, scaleHeight);
+
+ // Resize the bitmap to exact size
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleBy, scaleBy);
+
+ // apply rotation
+ if (rotation != 0) {
+ matrix.setRotate(rotation);
+ }
+
+ Bitmap.CompressFormat fmt;
+ if (fileExtension != null && fileExtension.equalsIgnoreCase("png")) {
+ fmt = Bitmap.CompressFormat.PNG;
+ } else {
+ fmt = Bitmap.CompressFormat.JPEG;
+ }
+
+ final Bitmap bmpRotated;
+ try {
+ bmpRotated = Bitmap.createBitmap(bmpResized, 0, 0, bmpResized.getWidth(), bmpResized.getHeight(), matrix,
+ true);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+ bmpRotated.compress(fmt, 100, stream);
+ bmpResized.recycle();
+ bmpRotated.recycle();
+
+ return stream.toByteArray();
+ }
+
+ public static Bitmap getCircularBitmap(final Bitmap bitmap) {
+ if (bitmap==null)
+ return null;
+
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(Color.RED);
+ canvas.drawOval(rectF, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ // outline
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1f);
+ paint.setColor(Color.DKGRAY);
+ canvas.drawOval(rectF, paint);
+
+ return output;
+ }
+
+ public static Bitmap getRoundedEdgeBitmap(final Bitmap bitmap, int radius) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(Color.RED);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1f);
+ paint.setColor(Color.DKGRAY);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+
+ return output;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java
new file mode 100644
index 000000000..199fba703
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java
@@ -0,0 +1,236 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+
+public class JSONUtil {
+ private static String QUERY_SEPERATOR=".";
+ private static String QUERY_ARRAY_INDEX_START="[";
+ private static String QUERY_ARRAY_INDEX_END="]";
+ private static String QUERY_ARRAY_FIRST="first";
+ private static String QUERY_ARRAY_LAST="last";
+
+ private static final String JSON_NULL_STR = "null";
+
+ private static final String TAG="JSONUtil";
+ /**
+ * Given a JSONObject and a key path (e.g property.child) and a default it will
+ * traverse the object graph and pull out the desired property
+ */
+ public static <U> U queryJSON(JSONObject source, String query, U defaultObject) {
+ int nextSeperator = query.indexOf(QUERY_SEPERATOR);
+ int nextIndexStart = query.indexOf(QUERY_ARRAY_INDEX_START);
+ if (nextSeperator == -1 && nextIndexStart == -1) {
+ // last item let's get it
+ try {
+ if (!source.has(query)) {
+ return defaultObject;
+ }
+ Object result = source.get(query);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to " + defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Unable to get the Key from the input object. Key:" + query, e);
+ return defaultObject;
+ }
+ }
+ int endQuery;
+ if (nextSeperator == -1 || nextIndexStart == -1) {
+ endQuery = Math.max(nextSeperator, nextIndexStart);
+ } else {
+ endQuery = Math.min(nextSeperator, nextIndexStart);
+ }
+ String nextQuery = query.substring(endQuery);
+ String key = query.substring(0, endQuery);
+ try {
+ if (source == null) {
+ return defaultObject;
+ }
+ if (nextQuery.indexOf(QUERY_SEPERATOR) == 0) {
+ return queryJSON(source.getJSONObject(key), nextQuery.substring(1), defaultObject);
+ } else if (nextQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) {
+ return queryJSON(source.getJSONArray(key), nextQuery, defaultObject);
+ } else if (!nextQuery.equals("")) {
+ return defaultObject;
+ }
+ Object result = source.get(key);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to " + defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Unable to get the Key from the input object. Key:" + query, e);
+ return defaultObject;
+ }
+ }
+
+ /**
+ * Given a JSONArray and a query (e.g. [0].property) it will traverse the array and
+ * pull out the requested property.
+ *
+ * Acceptable indexes include negative numbers to reference items from the end of
+ * the list as well as "last" and "first" as more explicit references to "0" and "-1"
+ */
+ public static <U> U queryJSON(JSONArray source, String query, U defaultObject){
+ // query must start with [ have an index and then have ]
+ int indexStart = query.indexOf(QUERY_ARRAY_INDEX_START);
+ int indexEnd = query.indexOf(QUERY_ARRAY_INDEX_END);
+ if (indexStart == -1 || indexEnd == -1 || indexStart > indexEnd) {
+ return defaultObject;
+ }
+ // get "index" from "[index]"
+ String indexStr = query.substring(indexStart + 1, indexEnd);
+ int index;
+ if (indexStr.equals(QUERY_ARRAY_FIRST)) {
+ index = 0;
+ } else if (indexStr.equals(QUERY_ARRAY_LAST)) {
+ index = -1;
+ } else {
+ index = Integer.parseInt(indexStr);
+ }
+ if (index < 0) {
+ index = source.length() + index;
+ }
+ // copy remaining query
+ String remainingQuery = query.substring(indexEnd + 1);
+ try {
+ if (remainingQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) {
+ return queryJSON(source.getJSONArray(index), remainingQuery, defaultObject);
+ } else if (remainingQuery.indexOf(QUERY_SEPERATOR) == 0) {
+ return queryJSON(source.getJSONObject(index), remainingQuery.substring(1), defaultObject);
+ } else if (!remainingQuery.equals("")) {
+ // TODO throw an exception since the query isn't valid?
+ AppLog.w(T.UTILS, String.format("Incorrect query for next object %s", remainingQuery));
+ return defaultObject;
+ }
+ Object result = source.get(index);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to "+defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Unable to get the Key from the input object. Key:" + query, e);
+ return defaultObject;
+ }
+ }
+
+ /**
+ * Convert a JSONArray (expected to contain strings) in a string list
+ */
+ public static ArrayList<String> fromJSONArrayToStringList(JSONArray jsonArray) {
+ ArrayList<String> stringList = new ArrayList<String>();
+ for (int i = 0; i < jsonArray.length(); i++) {
+ try {
+ stringList.add(jsonArray.getString(i));
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+ return stringList;
+ }
+
+ /**
+ * Convert a string list in a JSONArray
+ */
+ public static JSONArray fromStringListToJSONArray(ArrayList<String> stringList) {
+ JSONArray jsonArray = new JSONArray();
+ if (stringList != null) {
+ for (int i = 0; i < stringList.size(); i++) {
+ jsonArray.put(stringList.get(i));
+ }
+ }
+ return jsonArray;
+ }
+
+ /*
+ * wrapper for JSONObject.optString() which handles "null" values
+ */
+ public static String getString(JSONObject json, String name) {
+ String value = json.optString(name);
+ // return empty string for "null"
+ if (JSON_NULL_STR.equals(value))
+ return "";
+ return value;
+ }
+
+ /*
+ * use with strings that contain HTML entities
+ */
+ public static String getStringDecoded(JSONObject json, String name) {
+ String value = getString(json, name);
+ return HtmlUtils.fastUnescapeHtml(value);
+ }
+
+ /*
+ * replacement for JSONObject.optBoolean() - optBoolean() only checks for "true" and "false",
+ * but our API sometimes uses "0" to denote false
+ */
+ public static boolean getBool(JSONObject json, String name) {
+ String value = getString(json, name);
+ if (TextUtils.isEmpty(value))
+ return false;
+ if (value.equals("0"))
+ return false;
+ if (value.equalsIgnoreCase("false"))
+ return false;
+ return true;
+ }
+
+ /*
+ * returns the JSONObject child of the passed parent that matches the passed query
+ * this is basically an "optJSONObject" that supports nested queries, for example:
+ *
+ * getJSONChild("meta/data/site")
+ *
+ * would find this:
+ *
+ * "meta": {
+ * "data": {
+ * "site": {
+ * "ID": 3584907,
+ * "name": "WordPress.com News",
+ * }
+ * }
+ * }
+ */
+ public static JSONObject getJSONChild(final JSONObject jsonParent, final String query) {
+ if (jsonParent == null || TextUtils.isEmpty(query))
+ return null;
+ String[] names = query.split("/");
+ JSONObject jsonChild = null;
+ for (int i = 0; i < names.length; i++) {
+ if (jsonChild == null) {
+ jsonChild = jsonParent.optJSONObject(names[i]);
+ } else {
+ jsonChild = jsonChild.optJSONObject(names[i]);
+ }
+ if (jsonChild == null)
+ return null;
+ }
+ return jsonChild;
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java
new file mode 100644
index 000000000..d60e9da6c
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java
@@ -0,0 +1,36 @@
+package org.wordpress.android.util;
+
+import android.view.View;
+import android.widget.ListView;
+
+public class ListScrollPositionManager {
+ private int mSelectedPosition;
+ private int mListViewScrollStateIndex;
+ private int mListViewScrollStateOffset;
+ private ListView mListView;
+ private boolean mSetSelection;
+
+ public ListScrollPositionManager(ListView listView, boolean setSelection) {
+ mListView = listView;
+ mSetSelection = setSelection;
+ }
+
+ public void saveScrollOffset() {
+ mListViewScrollStateIndex = mListView.getFirstVisiblePosition();
+ View view = mListView.getChildAt(0);
+ mListViewScrollStateOffset = 0;
+ if (view != null) {
+ mListViewScrollStateOffset = view.getTop();
+ }
+ if (mSetSelection) {
+ mSelectedPosition = mListView.getCheckedItemPosition();
+ }
+ }
+
+ public void restoreScrollOffset() {
+ mListView.setSelectionFromTop(mListViewScrollStateIndex, mListViewScrollStateOffset);
+ if (mSetSelection) {
+ mListView.setItemChecked(mSelectedPosition, true);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java
new file mode 100644
index 000000000..12439fd28
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java
@@ -0,0 +1,132 @@
+//This Handy-Dandy class acquired and tweaked from http://stackoverflow.com/a/3145655/309558
+package org.wordpress.android.util;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+
+public class LocationHelper {
+ Timer timer1;
+ LocationManager lm;
+ LocationResult locationResult;
+ boolean gps_enabled = false;
+ boolean network_enabled = false;
+
+ public boolean getLocation(Context context, LocationResult result) {
+ locationResult = result;
+ if (lm == null)
+ lm = (LocationManager) context
+ .getSystemService(Context.LOCATION_SERVICE);
+
+ // exceptions will be thrown if provider is not permitted.
+ try {
+ gps_enabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ } catch (Exception ex) {
+ }
+ try {
+ network_enabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
+ } catch (Exception ex) {
+ }
+
+ // don't start listeners if no provider is enabled
+ if (!gps_enabled && !network_enabled)
+ return false;
+
+ if (gps_enabled)
+ lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps);
+
+ if (network_enabled)
+ lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
+
+ timer1 = new Timer();
+ timer1.schedule(new GetLastLocation(), 30000);
+ return true;
+ }
+
+ LocationListener locationListenerGps = new LocationListener() {
+ public void onLocationChanged(Location location) {
+ timer1.cancel();
+ locationResult.gotLocation(location);
+ lm.removeUpdates(this);
+ lm.removeUpdates(locationListenerNetwork);
+ }
+
+ public void onProviderDisabled(String provider) {
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ LocationListener locationListenerNetwork = new LocationListener() {
+ public void onLocationChanged(Location location) {
+ timer1.cancel();
+ locationResult.gotLocation(location);
+ lm.removeUpdates(this);
+ lm.removeUpdates(locationListenerGps);
+ }
+
+ public void onProviderDisabled(String provider) {
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ class GetLastLocation extends TimerTask {
+ @Override
+ public void run() {
+ lm.removeUpdates(locationListenerGps);
+ lm.removeUpdates(locationListenerNetwork);
+
+ Location net_loc = null, gps_loc = null;
+ if (gps_enabled)
+ gps_loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ if (network_enabled)
+ net_loc = lm
+ .getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+
+ // if there are both values use the latest one
+ if (gps_loc != null && net_loc != null) {
+ if (gps_loc.getTime() > net_loc.getTime())
+ locationResult.gotLocation(gps_loc);
+ else
+ locationResult.gotLocation(net_loc);
+ return;
+ }
+
+ if (gps_loc != null) {
+ locationResult.gotLocation(gps_loc);
+ return;
+ }
+ if (net_loc != null) {
+ locationResult.gotLocation(net_loc);
+ return;
+ }
+ locationResult.gotLocation(null);
+ }
+ }
+
+ public static abstract class LocationResult {
+ public abstract void gotLocation(Location location);
+ }
+
+ public void cancelTimer() {
+ if (timer1 != null) {
+ timer1.cancel();
+ lm.removeUpdates(locationListenerGps);
+ lm.removeUpdates(locationListenerNetwork);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
new file mode 100644
index 000000000..981e537d2
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.util;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * wrappers for extracting values from a Map object
+ */
+public class MapUtils {
+ /*
+ * returns a String value for the passed key in the passed map
+ * always returns "" instead of null
+ */
+ public static String getMapStr(final Map<?, ?> map, final String key) {
+ if (map == null || key == null || !map.containsKey(key) || map.get(key) == null) {
+ return "";
+ }
+ return map.get(key).toString();
+ }
+
+ /*
+ * returns an int value for the passed key in the passed map
+ * defaultValue is returned if key doesn't exist or isn't a number
+ */
+ public static int getMapInt(final Map<?, ?> map, final String key) {
+ return getMapInt(map, key, 0);
+ }
+ public static int getMapInt(final Map<?, ?> map, final String key, int defaultValue) {
+ try {
+ return Integer.parseInt(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * long version of above
+ */
+ public static long getMapLong(final Map<?, ?> map, final String key) {
+ return getMapLong(map, key, 0);
+ }
+ public static long getMapLong(final Map<?, ?> map, final String key, long defaultValue) {
+ try {
+ return Long.parseLong(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * returns a date object from the passed key in the passed map
+ * returns null if key doesn't exist or isn't a date
+ */
+ public static Date getMapDate(final Map<?, ?> map, final String key) {
+ if (map==null || key==null || !map.containsKey(key))
+ return null;
+ try {
+ return (Date) map.get(key);
+ } catch (ClassCastException e) {
+ return null;
+ }
+ }
+
+ /*
+ * returns a boolean value from the passed key in the passed map
+ * returns true unless key doesn't exist, or the value is "0" or "false"
+ */
+ public static boolean getMapBool(final Map<?, ?> map, final String key) {
+ String value = getMapStr(map, key);
+ if (value.isEmpty())
+ return false;
+ if (value.startsWith("0")) // handles "0" and "0.0"
+ return false;
+ if (value.equalsIgnoreCase("false"))
+ return false;
+ // all other values are assume to be true
+ return true;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
new file mode 100644
index 000000000..497d756ee
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
@@ -0,0 +1,96 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+/**
+ * routines related to the Photon API
+ * http://developer.wordpress.com/docs/photon/
+ */
+public class PhotonUtils {
+ private PhotonUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * gravatars often contain the ?s= parameter which determines their size - detect this and
+ * replace it with a new ?s= parameter which requests the avatar at the exact size needed
+ */
+ public static String fixAvatar(final String imageUrl, int avatarSz) {
+ if (TextUtils.isEmpty(imageUrl))
+ return "";
+
+ // if this isn't a gravatar image, return as resized photon image url
+ if (!imageUrl.contains("gravatar.com"))
+ return getPhotonImageUrl(imageUrl, avatarSz, avatarSz);
+
+ // remove all other params, then add query string for size and "mystery man" default
+ return UrlUtils.removeQuery(imageUrl) + String.format("?s=%d&d=mm", avatarSz);
+ }
+
+ /*
+ * returns true if the passed url is an obvious "mshots" url
+ */
+ public static boolean isMshotsUrl(final String imageUrl) {
+ return (imageUrl != null && imageUrl.contains("/mshots/"));
+ }
+
+ /*
+ * returns a photon url for the passed image with the resize query set to the passed dimensions
+ */
+ public static String getPhotonImageUrl(String imageUrl, int width, int height) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // make sure it's valid
+ int schemePos = imageUrl.indexOf("://");
+ if (schemePos == -1) {
+ return imageUrl;
+ }
+
+ // remove existing query string since it may contain params that conflict with the passed ones
+ imageUrl = UrlUtils.removeQuery(imageUrl);
+
+ // don't use with GIFs - photon breaks animated GIFs, and sometimes returns a GIF that
+ // can't be read by BitmapFactory.decodeByteArray (used by Volley in ImageRequest.java
+ // to decode the downloaded image)
+ // ex: http://i0.wp.com/lusianne.files.wordpress.com/2013/08/193.gif?resize=768,320
+ if (imageUrl.endsWith(".gif")) {
+ return imageUrl;
+ }
+
+ // if this is an "mshots" url, skip photon and return it with a query that sets the width/height
+ // (these are screenshots of the blog that often appear in freshly pressed posts)
+ // see http://wp.tutsplus.com/tutorials/how-to-generate-website-screenshots-for-your-wordpress-site/
+ // ex: http://s.wordpress.com/mshots/v1/http%3A%2F%2Fnickbradbury.com?w=600
+ if (isMshotsUrl(imageUrl)) {
+ return imageUrl + String.format("?w=%d&h=%d", width, height);
+ }
+
+ // if both width & height are passed use the "resize" param, use only "w" or "h" if just
+ // one of them is set, otherwise no query string
+ final String query;
+ if (width > 0 && height > 0) {
+ query = String.format("?resize=%d,%d", width, height);
+ } else if (width > 0) {
+ query = String.format("?w=%d", width);
+ } else if (height > 0) {
+ query = String.format("?h=%d", height);
+ } else {
+ query = "";
+ }
+
+ // return passed url+query if it's already a photon url
+ if (imageUrl.contains(".wp.com")) {
+ if (imageUrl.contains("i0.wp.com") || imageUrl.contains("i1.wp.com") || imageUrl.contains("i2.wp.com"))
+ return imageUrl + query;
+ }
+
+ // must use https for https image urls
+ if (UrlUtils.isHttps(imageUrl)) {
+ return "https://i0.wp.com/" + imageUrl.substring(schemePos+3, imageUrl.length()) + query;
+ } else {
+ return "http://i0.wp.com/" + imageUrl.substring(schemePos+3, imageUrl.length()) + query;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
new file mode 100644
index 000000000..251db2a3b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
@@ -0,0 +1,91 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.SystemClock;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+
+/**
+ * forked from android.util.TimingLogger to use AppLog instead of Log + new static interface.
+ */
+public class ProfilingUtils {
+ private static ProfilingUtils sInstance;
+
+ private String mLabel;
+ private ArrayList<Long> mSplits;
+ private ArrayList<String> mSplitLabels;
+
+ public static void start(String label) {
+ getInstance().reset(label);
+ }
+
+ public static void split(String splitLabel) {
+ getInstance().addSplit(splitLabel);
+ }
+
+ public static void dump() {
+ getInstance().dumpToLog();
+ }
+
+ private static ProfilingUtils getInstance() {
+ if (sInstance == null) {
+ sInstance = new ProfilingUtils();
+ }
+ return sInstance;
+ }
+
+ public ProfilingUtils() {
+ reset("init");
+ }
+
+ public void reset(String label) {
+ mLabel = label;
+ reset();
+ }
+
+ public void reset() {
+ if (mSplits == null) {
+ mSplits = new ArrayList<Long>();
+ mSplitLabels = new ArrayList<String>();
+ } else {
+ mSplits.clear();
+ mSplitLabels.clear();
+ }
+ addSplit(null);
+ }
+
+ public void addSplit(String splitLabel) {
+ long now = SystemClock.elapsedRealtime();
+ mSplits.add(now);
+ mSplitLabels.add(splitLabel);
+ }
+
+ public void dumpToLog() {
+ AppLog.d(T.PROFILING, mLabel + ": begin");
+ final long first = mSplits.get(0);
+ long now = first;
+ for (int i = 1; i < mSplits.size(); i++) {
+ now = mSplits.get(i);
+ final String splitLabel = mSplitLabels.get(i);
+ final long prev = mSplits.get(i - 1);
+ AppLog.d(T.PROFILING, mLabel + ": " + (now - prev) + " ms, " + splitLabel);
+ }
+ AppLog.d(T.PROFILING, mLabel + ": end, " + (now - first) + " ms");
+ }
+
+ // Returns app version name String
+ public static String getVersionName(Context context) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
+ return pi.versionName == null ? "" : pi.versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ return "";
+ }
+ }
+}
+
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/README.md b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/README.md
new file mode 100644
index 000000000..62a759585
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/README.md
@@ -0,0 +1 @@
+# org.wordpress.android.util \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
new file mode 100644
index 000000000..8d1b4b437
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
@@ -0,0 +1,121 @@
+package org.wordpress.android.util;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDoneException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteStatement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SqlUtils {
+ private SqlUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * SQLite doesn't have a boolean datatype, so booleans are stored as 0=false, 1=true
+ */
+ public static long boolToSql(boolean value) {
+ return (value ? 1 : 0);
+ }
+ public static boolean sqlToBool(int value) {
+ return (value != 0);
+ }
+
+ public static void closeStatement(SQLiteStatement stmt) {
+ if (stmt != null) {
+ stmt.close();
+ }
+ }
+
+ public static void closeCursor(Cursor c) {
+ if (c != null && !c.isClosed()) {
+ c.close();
+ }
+ }
+
+ /*
+ * wrapper for DatabaseUtils.longForQuery() which returns 0 if query returns no rows
+ */
+ public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ try {
+ return DatabaseUtils.longForQuery(db, query, selectionArgs);
+ } catch (SQLiteDoneException e) {
+ return 0;
+ }
+ }
+
+ public static int intForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ long value = longForQuery(db, query, selectionArgs);
+ return (int)value;
+ }
+
+ public static boolean boolForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ long value = longForQuery(db, query, selectionArgs);
+ return sqlToBool((int) value);
+ }
+
+ /*
+ * wrapper for DatabaseUtils.stringForQuery(), returns "" if query returns no rows
+ */
+ public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ try {
+ return DatabaseUtils.stringForQuery(db, query, selectionArgs);
+ } catch (SQLiteDoneException e) {
+ return "";
+ }
+ }
+
+ /*
+ * returns the number of rows in the passed table
+ */
+ public static long getRowCount(SQLiteDatabase db, String tableName) {
+ return DatabaseUtils.queryNumEntries(db, tableName);
+ }
+
+ /*
+ * removes all rows from the passed table
+ */
+ public static void deleteAllRowsInTable(SQLiteDatabase db, String tableName) {
+ db.delete(tableName, null, null);
+ }
+
+ /*
+ * drop all tables from the passed SQLiteDatabase - make sure to pass a
+ * writable database
+ */
+ public static boolean dropAllTables(SQLiteDatabase db) throws SQLiteException {
+ if (db == null) {
+ return false;
+ }
+
+ if (db.isReadOnly()) {
+ throw new SQLiteException("can't drop tables from a read-only database");
+ }
+
+ List<String> tableNames = new ArrayList<String>();
+ Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null);
+ if (cursor.moveToFirst()) {
+ do {
+ String tableName = cursor.getString(0);
+ if (!tableName.equals("android_metadata") && !tableName.equals("sqlite_sequence")) {
+ tableNames.add(tableName);
+ }
+ } while (cursor.moveToNext());
+ }
+
+ db.beginTransaction();
+ try {
+ for (String tableName: tableNames) {
+ db.execSQL("DROP TABLE IF EXISTS " + tableName);
+ }
+ db.setTransactionSuccessful();
+ return true;
+ } finally {
+ db.endTransaction();
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
new file mode 100644
index 000000000..eca31ffd1
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
@@ -0,0 +1,278 @@
+package org.wordpress.android.util;
+
+import android.text.Html;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class StringUtils {
+ public static String[] mergeStringArrays(String array1[], String array2[]) {
+ if (array1 == null || array1.length == 0) {
+ return array2;
+ }
+ if (array2 == null || array2.length == 0) {
+ return array1;
+ }
+ List<String> array1List = Arrays.asList(array1);
+ List<String> array2List = Arrays.asList(array2);
+ List<String> result = new ArrayList<String>(array1List);
+ List<String> tmp = new ArrayList<String>(array1List);
+ tmp.retainAll(array2List);
+ result.addAll(array2List);
+ return ((String[]) result.toArray(new String[result.size()]));
+ }
+
+ public static String convertHTMLTagsForUpload(String source) {
+ // bold
+ source = source.replace("<b>", "<strong>");
+ source = source.replace("</b>", "</strong>");
+
+ // italics
+ source = source.replace("<i>", "<em>");
+ source = source.replace("</i>", "</em>");
+
+ return source;
+ }
+
+ public static String convertHTMLTagsForDisplay(String source) {
+ // bold
+ source = source.replace("<strong>", "<b>");
+ source = source.replace("</strong>", "</b>");
+
+ // italics
+ source = source.replace("<em>", "<i>");
+ source = source.replace("</em>", "</i>");
+
+ return source;
+ }
+
+ public static String addPTags(String source) {
+ String[] asploded = source.split("\n\n");
+
+ if (asploded.length > 0) {
+ StringBuilder wrappedHTML = new StringBuilder();
+ for (int i = 0; i < asploded.length; i++) {
+ String trimmed = asploded[i].trim();
+ if (trimmed.length() > 0) {
+ trimmed = trimmed.replace("<br />", "<br>").replace("<br/>", "<br>").replace("<br>\n", "<br>")
+ .replace("\n", "<br>");
+ wrappedHTML.append("<p>");
+ wrappedHTML.append(trimmed);
+ wrappedHTML.append("</p>");
+ }
+ }
+ return wrappedHTML.toString();
+ } else {
+ return source;
+ }
+ }
+
+ public static BigInteger getMd5IntHash(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] messageDigest = md.digest(input.getBytes());
+ BigInteger number = new BigInteger(1, messageDigest);
+ return number;
+ } catch (NoSuchAlgorithmException e) {
+ AppLog.e(T.UTILS, e);
+ return null;
+ }
+ }
+
+ public static String getMd5Hash(String input) {
+ BigInteger number = getMd5IntHash(input);
+ String md5 = number.toString(16);
+ while (md5.length() < 32) {
+ md5 = "0" + md5;
+ }
+ return md5;
+ }
+
+ public static String unescapeHTML(String html) {
+ if (html != null) {
+ return Html.fromHtml(html).toString();
+ } else {
+ return "";
+ }
+ }
+
+ /*
+ * nbradbury - adapted from Html.escapeHtml(), which was added in API Level 16
+ * TODO: not thoroughly tested yet, so marked as private - not sure I like the way
+ * this replaces two spaces with "&nbsp;"
+ */
+ private static String escapeHtml(final String text) {
+ if (text == null) {
+ return "";
+ }
+
+ StringBuilder out = new StringBuilder();
+ int length = text.length();
+
+ for (int i = 0; i < length; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("&lt;");
+ } else if (c == '>') {
+ out.append("&gt;");
+ } else if (c == '&') {
+ out.append("&amp;");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("&#").append((int) c).append(";");
+ } else if (c == ' ') {
+ while (i + 1 < length && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+
+ return out.toString();
+ }
+
+ /*
+ * returns empty string if passed string is null, otherwise returns passed string
+ */
+ public static String notNullStr(String s) {
+ if (s == null) {
+ return "";
+ }
+ return s;
+ }
+
+ /**
+ * returns true if two strings are equal or two strings are null
+ */
+ public static boolean equals(String s1, String s2) {
+ if (s1 == null) {
+ return s2 == null;
+ }
+ return s1.equals(s2);
+ }
+
+ /*
+ * capitalizes the first letter in the passed string - based on Apache commons/lang3/StringUtils
+ * http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/StringUtils.java?revision=1497829&view=markup
+ */
+ public static String capitalize(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return str;
+ }
+
+ char firstChar = str.charAt(0);
+ if (Character.isTitleCase(firstChar)) {
+ return str;
+ }
+
+ return new StringBuilder(strLen).append(Character.toTitleCase(firstChar)).append(str.substring(1)).toString();
+ }
+
+ /*
+ * Wrap an image URL in a photon URL
+ * Check out http://developer.wordpress.com/docs/photon/
+ */
+ public static String getPhotonUrl(String imageUrl, int size) {
+ imageUrl = imageUrl.replace("http://", "").replace("https://", "");
+ return "http://i0.wp.com/" + imageUrl + "?w=" + size;
+ }
+
+ public static String getHost(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return "";
+ }
+
+ int doubleslash = url.indexOf("//");
+ if (doubleslash == -1) {
+ doubleslash = 0;
+ } else {
+ doubleslash += 2;
+ }
+
+ int end = url.indexOf('/', doubleslash);
+ end = (end >= 0) ? end : url.length();
+
+ return url.substring(doubleslash, end);
+ }
+
+ public static String replaceUnicodeSurrogateBlocksWithHTMLEntities(final String inputString) {
+ final int length = inputString.length();
+ StringBuilder out = new StringBuilder(); // Used to hold the output.
+ for (int offset = 0; offset < length; ) {
+ final int codepoint = inputString.codePointAt(offset);
+ final char current = inputString.charAt(offset);
+ if (Character.isHighSurrogate(current) || Character.isLowSurrogate(current)) {
+ if (Emoticons.wpSmiliesCodePointToText.get(codepoint) != null) {
+ out.append(Emoticons.wpSmiliesCodePointToText.get(codepoint));
+ } else {
+ final String htmlEscapedChar = "&#x" + Integer.toHexString(codepoint) + ";";
+ out.append(htmlEscapedChar);
+ }
+ } else {
+ out.append(current);
+ }
+ offset += Character.charCount(codepoint);
+ }
+ return out.toString();
+ }
+
+ /**
+ * This method ensures that the output String has only
+ * valid XML unicode characters as specified by the
+ * XML 1.0 standard. For reference, please see
+ * <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
+ * standard</a>. This method will return an empty
+ * String if the input is null or empty.
+ *
+ * @param in The String whose non-valid characters we want to remove.
+ * @return The in String, stripped of non-valid characters.
+ */
+ public static final String stripNonValidXMLCharacters(String in) {
+ StringBuilder out = new StringBuilder(); // Used to hold the output.
+ char current; // Used to reference the current character.
+
+ if (in == null || ("".equals(in))) {
+ return ""; // vacancy test.
+ }
+ for (int i = 0; i < in.length(); i++) {
+ current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
+ if ((current == 0x9) ||
+ (current == 0xA) ||
+ (current == 0xD) ||
+ ((current >= 0x20) && (current <= 0xD7FF)) ||
+ ((current >= 0xE000) && (current <= 0xFFFD)) ||
+ ((current >= 0x10000) && (current <= 0x10FFFF))) {
+ out.append(current);
+ }
+ }
+ return out.toString();
+ }
+
+ /*
+ * simple wrapper for Integer.valueOf(string) so caller doesn't need to catch NumberFormatException
+ */
+ public static int stringToInt(String s) {
+ return stringToInt(s, 0);
+ }
+ public static int stringToInt(String s, int defaultValue) {
+ if (s == null)
+ return defaultValue;
+ try {
+ return Integer.valueOf(s);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
new file mode 100644
index 000000000..4ba0c96ed
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
@@ -0,0 +1,17 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+import org.wordpress.android.util.AppLog.T;
+
+public class SystemServiceFactory {
+ public static SystemServiceFactoryAbstract sFactory;
+
+ public static Object get(Context context, String name) {
+ if (sFactory == null) {
+ sFactory = new SystemServiceFactoryDefault();
+ }
+ AppLog.v(T.UTILS, "instantiate SystemService using sFactory: " + sFactory.getClass());
+ return sFactory.get(context, name);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java
new file mode 100644
index 000000000..a9d522db4
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public interface SystemServiceFactoryAbstract {
+ public Object get(Context context, String name);
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java
new file mode 100644
index 000000000..eb488dde9
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public class SystemServiceFactoryDefault implements SystemServiceFactoryAbstract {
+ public Object get(Context context, String name) {
+ return context.getSystemService(name);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java
new file mode 100644
index 000000000..9b99c6ea5
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java
@@ -0,0 +1,37 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.widget.Toast;
+
+/**
+ * Provides a simplified way to show toast messages without having to create the toast, set the
+ * desired gravity, etc.
+ */
+public class ToastUtils {
+ public enum Duration {SHORT, LONG}
+
+ private ToastUtils() {
+ throw new AssertionError();
+ }
+
+ public static Toast showToast(Context context, int stringResId) {
+ return showToast(context, stringResId, Duration.SHORT);
+ }
+
+ public static Toast showToast(Context context, int stringResId, Duration duration) {
+ return showToast(context, context.getString(stringResId), duration);
+ }
+
+ public static Toast showToast(Context context, String text) {
+ return showToast(context, text, Duration.SHORT);
+ }
+
+ public static Toast showToast(Context context, String text, Duration duration) {
+ Toast toast = Toast.makeText(context, text,
+ (duration == Duration.SHORT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG));
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ return toast;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
new file mode 100644
index 000000000..4438b8950
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
@@ -0,0 +1,165 @@
+package org.wordpress.android.util;
+
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.net.IDN;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+
+public class UrlUtils {
+ public static String urlEncode(final String text) {
+ try {
+ return URLEncoder.encode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return text;
+ }
+ }
+
+ public static String urlDecode(final String text) {
+ try {
+ return URLDecoder.decode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return text;
+ }
+ }
+
+ public static String getDomainFromUrl(final String urlString) {
+ if (urlString == null) {
+ return "";
+ }
+ Uri uri = Uri.parse(urlString);
+ return uri.getHost();
+ }
+
+ /**
+ * Convert IDN names to punycode if necessary
+ */
+ public static String convertUrlToPunycodeIfNeeded(String url) {
+ if (!Charset.forName("US-ASCII").newEncoder().canEncode(url)) {
+ if (url.toLowerCase().startsWith("http://")) {
+ url = "http://" + IDN.toASCII(url.substring(7));
+ } else if (url.toLowerCase().startsWith("https://")) {
+ url = "https://" + IDN.toASCII(url.substring(8));
+ } else {
+ url = IDN.toASCII(url);
+ }
+ }
+ return url;
+ }
+
+ public static String addUrlSchemeIfNeeded(String url, boolean isHTTPS) {
+ if (url == null) {
+ return null;
+ }
+
+ if (!URLUtil.isValidUrl(url)) {
+ if (!(url.toLowerCase().startsWith("http://")) && !(url.toLowerCase().startsWith("https://"))) {
+ url = (isHTTPS ? "https" : "http") + "://" + url;
+ }
+ }
+
+ return url;
+ }
+
+ /**
+ * normalizes a URL, primarily for comparison purposes, for example so that
+ * normalizeUrl("http://google.com/") = normalizeUrl("http://google.com")
+ */
+ public static String normalizeUrl(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ // this routine is called from some performance-critical code and creating a URI from a string
+ // is slow, so skip it when possible - if we know it's not a relative path (and 99.9% of the
+ // time it won't be for our purposes) then we can normalize it without java.net.URI.normalize()
+ if (urlString.startsWith("http") && !urlString.contains("build/intermediates/exploded-aar/org.wordpress/graphview/3.1.1")) {
+ // return without a trailing slash
+ if (urlString.endsWith("/")) {
+ return urlString.substring(0, urlString.length() - 1);
+ }
+ return urlString;
+ }
+
+ // url is relative, so fall back to using slower java.net.URI normalization
+ try {
+ URI uri = URI.create(urlString);
+ return uri.normalize().toString();
+ } catch (IllegalArgumentException e) {
+ return urlString;
+ }
+ }
+
+ /**
+ * returns the passed url without the query parameters
+ */
+ public static String removeQuery(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+ int pos = urlString.indexOf("?");
+ if (pos == -1) {
+ return urlString;
+ }
+ return urlString.substring(0, pos);
+ }
+
+ /**
+ * returns true if passed url is https:
+ */
+ public static boolean isHttps(final String urlString) {
+ return (urlString != null && urlString.startsWith("https:"));
+ }
+
+ /**
+ * returns https: version of passed http: url
+ */
+ public static String makeHttps(final String urlString) {
+ if (urlString == null || !urlString.startsWith("http:")) {
+ return urlString;
+ }
+ return "https:" + urlString.substring(5, urlString.length());
+ }
+
+ /**
+ * see http://stackoverflow.com/a/8591230/1673548
+ */
+ public static String getUrlMimeType(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ String extension = MimeTypeMap.getFileExtensionFromUrl(urlString);
+ if (extension == null) {
+ return null;
+ }
+
+ MimeTypeMap mime = MimeTypeMap.getSingleton();
+ String mimeType = mime.getMimeTypeFromExtension(extension);
+ if (mimeType == null) {
+ return null;
+ }
+
+ return mimeType;
+ }
+
+ /**
+ * returns false if the url is not valid or if the url host is null, else true
+ */
+ public static boolean isValidUrlAndHostNotNull(String url) {
+ try {
+ URI uri = URI.create(url);
+ if (uri.getHost() == null) {
+ return false;
+ }
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java
new file mode 100644
index 000000000..dae02b4f0
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.util;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.util.Patterns;
+
+import java.util.regex.Pattern;
+
+public class UserEmail {
+ /**
+ * Get primary account and return its name if it matches the email address pattern.
+ *
+ * @return primary account email address if it can be found or empty string else.
+ */
+ public static String getPrimaryEmail(Context context) {
+ try {
+ AccountManager accountManager = AccountManager.get(context);
+ if (accountManager == null)
+ return "";
+ Account[] accounts = accountManager.getAccounts();
+ Pattern emailPattern = Patterns.EMAIL_ADDRESS;
+ for (Account account : accounts) {
+ // make sure account.name is an email address before adding to the list
+ if (emailPattern.matcher(account.name).matches()) {
+ return account.name;
+ }
+ }
+ return "";
+ } catch (SecurityException e) {
+ // exception will occur if app doesn't have GET_ACCOUNTS permission
+ return "";
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Version.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Version.java
new file mode 100644
index 000000000..6e695db45
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/Version.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.util;
+
+//See: http://stackoverflow.com/a/11024200
+public class Version implements Comparable<Version> {
+ private String version;
+
+ public final String get() {
+ return this.version;
+ }
+
+ public Version(String version) {
+ if(version == null)
+ throw new IllegalArgumentException("Version can not be null");
+ if(!version.matches("[0-9]+(\\.[0-9]+)*"))
+ throw new IllegalArgumentException("Invalid version format");
+ this.version = version;
+ }
+
+ @Override public int compareTo(Version that) {
+ if(that == null)
+ return 1;
+ String[] thisParts = this.get().split("\\.");
+ String[] thatParts = that.get().split("\\.");
+ int length = Math.max(thisParts.length, thatParts.length);
+ for(int i = 0; i < length; i++) {
+ int thisPart = i < thisParts.length ?
+ Integer.parseInt(thisParts[i]) : 0;
+ int thatPart = i < thatParts.length ?
+ Integer.parseInt(thatParts[i]) : 0;
+ if(thisPart < thatPart)
+ return -1;
+ if(thisPart > thatPart)
+ return 1;
+ }
+ return 0;
+ }
+
+ @Override public boolean equals(Object that) {
+ if(this == that)
+ return true;
+ if(that == null)
+ return false;
+ if(this.getClass() != that.getClass())
+ return false;
+ return this.compareTo((Version) that) == 0;
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java
new file mode 100644
index 000000000..fa96a998a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java
@@ -0,0 +1,59 @@
+package org.wordpress.android.util;
+
+import android.text.Editable;
+import android.text.Html;
+import android.text.style.BulletSpan;
+import android.text.style.LeadingMarginSpan;
+
+import org.xml.sax.XMLReader;
+
+import java.util.Vector;
+
+/**
+ * Handle tags that the Html class doesn't understand
+ * Tweaked from source at http://stackoverflow.com/questions/4044509/android-how-to-use-the-html-taghandler
+ */
+public class WPHtmlTagHandler implements Html.TagHandler {
+ private int mListItemCount = 0;
+ private Vector<String> mListParents = new Vector<String>();
+
+ @Override
+ public void handleTag(final boolean opening, final String tag, Editable output,
+ final XMLReader xmlReader) {
+ if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) {
+ if (opening) {
+ mListParents.add(tag);
+ } else {
+ mListParents.remove(tag);
+ }
+ mListItemCount = 0;
+ } else if (tag.equals("li") && !opening) {
+ handleListTag(output);
+ }
+ }
+
+ private void handleListTag(Editable output) {
+ if (mListParents.lastElement().equals("ul")) {
+ output.append("\n");
+ String[] split = output.toString().split("\n");
+ int start = 0;
+ if (split.length != 1) {
+ int lastIndex = split.length - 1;
+ start = output.length() - split[lastIndex].length() - 1;
+ }
+ output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0);
+ } else if (mListParents.lastElement().equals("ol")) {
+ mListItemCount++;
+ output.append("\n");
+ String[] split = output.toString().split("\n");
+ int start = 0;
+ if (split.length != 1) {
+ int lastIndex = split.length - 1;
+ start = output.length() - split[lastIndex].length() - 1;
+ }
+ output.insert(start, mListItemCount + ". ");
+ output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start,
+ output.length(), 0);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java
new file mode 100644
index 000000000..60b0d605b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java
@@ -0,0 +1,198 @@
+package org.wordpress.android.util;
+
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * ImageGetter for Html.fromHtml()
+ * adapted from existing ImageGetter code in NoteCommentFragment
+ */
+public class WPImageGetter implements Html.ImageGetter {
+ private WeakReference<TextView> mWeakView;
+ private int mMaxSize;
+ private ImageLoader mImageLoader;
+ private Drawable mLoadingDrawable;
+ private Drawable mFailedDrawable;
+
+ public WPImageGetter(TextView view) {
+ this(view, 0);
+ }
+
+ public WPImageGetter(TextView view, int maxSize) {
+ mWeakView = new WeakReference<TextView>(view);
+ mMaxSize = maxSize;
+ }
+
+ public WPImageGetter(TextView view, int maxSize, ImageLoader imageLoader, Drawable loadingDrawable,
+ Drawable failedDrawable) {
+ mWeakView = new WeakReference<TextView>(view);
+ mMaxSize = maxSize;
+ mImageLoader = imageLoader;
+ mLoadingDrawable = loadingDrawable;
+ mFailedDrawable = failedDrawable;
+ }
+
+ public void setImageLoader(ImageLoader imageLoader) {
+ mImageLoader = imageLoader;
+ }
+
+ public void setLoadingDrawable(Drawable loadingDrawable) {
+ mLoadingDrawable = loadingDrawable;
+ }
+
+ public void setFailedDrawable(Drawable failedDrawable) {
+ mFailedDrawable = failedDrawable;
+ }
+
+ private TextView getView() {
+ return mWeakView.get();
+ }
+
+ @Override
+ public Drawable getDrawable(String source) {
+ if (mImageLoader == null || mLoadingDrawable == null || mFailedDrawable == null) {
+ throw new RuntimeException("Developer, you need to call setImageLoader, setLoadingDrawable and setFailedDrawable");
+ }
+
+ if (TextUtils.isEmpty(source)) {
+ return null;
+ }
+
+ // images in reader comments may skip "http:" (no idea why) so make sure to add protocol here
+ if (source.startsWith("//")) {
+ source = "http:" + source;
+ }
+
+ // use Photon if a max size is requested (otherwise the full-sized image will be downloaded
+ // and then resized)
+ if (mMaxSize > 0) {
+ source = PhotonUtils.getPhotonImageUrl(source, mMaxSize, 0);
+ }
+
+ TextView view = getView();
+ // Drawable loading = view.getContext().getResources().getDrawable(R.drawable.remote_image); FIXME: here
+ // Drawable failed = view.getContext().getResources().getDrawable(R.drawable.remote_failed);
+ final RemoteDrawable remote = new RemoteDrawable(mLoadingDrawable, mFailedDrawable);
+
+ mImageLoader.get(source, new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ remote.displayFailed();
+ TextView view = getView();
+ if (view != null) {
+ view.invalidate();
+ }
+ }
+
+ @Override
+ public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
+ if (response.getBitmap() != null) {
+ // make sure view is still valid
+ TextView view = getView();
+ if (view == null) {
+ AppLog.w(T.UTILS, "WPImageGetter view is invalid");
+ return;
+ }
+
+ Drawable drawable = new BitmapDrawable(view.getContext().getResources(), response.getBitmap());
+ final int oldHeight = remote.getBounds().height();
+ int maxWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
+ if (mMaxSize > 0 && (maxWidth > mMaxSize || maxWidth == 0)) {
+ maxWidth = mMaxSize;
+ }
+ remote.setRemoteDrawable(drawable, maxWidth);
+
+ // image is from cache? don't need to modify view height
+ if (isImmediate) {
+ return;
+ }
+
+ int newHeight = remote.getBounds().height();
+ view.invalidate();
+ // For ICS
+ view.setHeight(view.getHeight() + newHeight - oldHeight);
+ // Pre ICS
+ view.setEllipsize(null);
+ }
+ }
+ });
+ return remote;
+ }
+
+ private static class RemoteDrawable extends BitmapDrawable {
+ protected Drawable mRemoteDrawable;
+ protected Drawable mLoadingDrawable;
+ protected Drawable mFailedDrawable;
+ private boolean mDidFail = false;
+
+ public RemoteDrawable(Drawable loadingDrawable, Drawable failedDrawable) {
+ mLoadingDrawable = loadingDrawable;
+ mFailedDrawable = failedDrawable;
+ setBounds(0, 0, mLoadingDrawable.getIntrinsicWidth(), mLoadingDrawable.getIntrinsicHeight());
+ }
+
+ public void displayFailed() {
+ mDidFail = true;
+ }
+
+ public void setBounds(int x, int y, int width, int height) {
+ super.setBounds(x, y, width, height);
+ if (mRemoteDrawable != null) {
+ mRemoteDrawable.setBounds(x, y, width, height);
+ return;
+ }
+ if (mLoadingDrawable != null) {
+ mLoadingDrawable.setBounds(x, y, width, height);
+ mFailedDrawable.setBounds(x, y, width, height);
+ }
+ }
+
+ public void setRemoteDrawable(Drawable remote) {
+ mRemoteDrawable = remote;
+ setBounds(0, 0, mRemoteDrawable.getIntrinsicWidth(), mRemoteDrawable.getIntrinsicHeight());
+ }
+
+ public void setRemoteDrawable(Drawable remote, int maxWidth) {
+ // null sentinel for now
+ if (remote == null) {
+ // throw error
+ return;
+ }
+ mRemoteDrawable = remote;
+ // determine if we need to scale the image to fit in view
+ int imgWidth = remote.getIntrinsicWidth();
+ int imgHeight = remote.getIntrinsicHeight();
+ float xScale = (float) imgWidth / (float) maxWidth;
+ if (xScale > 1.0f) {
+ setBounds(0, 0, Math.round(imgWidth / xScale), Math.round(imgHeight / xScale));
+ } else {
+ setBounds(0, 0, imgWidth, imgHeight);
+ }
+ }
+
+ public boolean didFail() {
+ return mDidFail;
+ }
+
+ public void draw(Canvas canvas) {
+ if (mRemoteDrawable != null) {
+ mRemoteDrawable.draw(canvas);
+ } else if (didFail()) {
+ mFailedDrawable.draw(canvas);
+ } else {
+ mLoadingDrawable.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java
new file mode 100644
index 000000000..37d5dfe6d
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.util;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.style.QuoteSpan;
+
+/**
+ * Customzed QuoteSpan for use in SpannableString's
+ */
+public class WPQuoteSpan extends QuoteSpan {
+ public static final int STRIPE_COLOR = 0xFF21759B;
+ private static final int STRIPE_WIDTH = 5;
+ private static final int GAP_WIDTH = 20;
+
+ public WPQuoteSpan(){
+ super(STRIPE_COLOR);
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ int margin = GAP_WIDTH * 2 + STRIPE_WIDTH;
+ return margin;
+ }
+
+ /**
+ * Draw a nice thick gray bar if Ice Cream Sandwhich or newer. There's a
+ * bug on older devices that does not respect the increased margin.
+ */
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
+ CharSequence text, int start, int end, boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(STRIPE_COLOR);
+
+ c.drawRect(GAP_WIDTH + x, top, x + dir * STRIPE_WIDTH, bottom, p);
+
+ p.setStyle(style);
+ p.setColor(color);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java
new file mode 100644
index 000000000..6a40c6f38
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java
@@ -0,0 +1,29 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.view.View;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.widget.ProgressBar;
+
+public class WPWebChromeClient extends WebChromeClient {
+ private ProgressBar mProgressBar;
+ private Activity mActivity;
+
+ public WPWebChromeClient(Activity activity, ProgressBar progressBar) {
+ mProgressBar = progressBar;
+ mActivity = activity;
+ }
+
+ public void onProgressChanged(WebView webView, int progress) {
+ if (!mActivity.isFinishing()) {
+ mActivity.setTitle(webView.getTitle());
+ }
+ if (progress == 100) {
+ mProgressBar.setVisibility(View.GONE);
+ } else {
+ mProgressBar.setVisibility(View.VISIBLE);
+ mProgressBar.setProgress(progress);
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHeaderTransformer.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHeaderTransformer.java
new file mode 100644
index 000000000..3fec8d91f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHeaderTransformer.java
@@ -0,0 +1,99 @@
+package org.wordpress.android.util.ptr;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+
+import org.wordpress.android.util.R;
+
+import uk.co.senab.actionbarpulltorefresh.library.DefaultHeaderTransformer;
+import uk.co.senab.actionbarpulltorefresh.library.sdk.Compat;
+
+public class PullToRefreshHeaderTransformer extends DefaultHeaderTransformer {
+ private View mHeaderView;
+ private ViewGroup mContentLayout;
+ private long mAnimationDuration;
+ private boolean mShowProgressBarOnly;
+ private Animation mHeaderOutAnimation;
+ private OnTopScrollChangedListener mOnTopScrollChangedListener;
+
+ public interface OnTopScrollChangedListener {
+ public void onTopScrollChanged(boolean scrolledOnTop);
+ }
+
+ public void setShowProgressBarOnly(boolean progressBarOnly) {
+ mShowProgressBarOnly = progressBarOnly;
+ }
+
+ @Override
+ public void onViewCreated(Activity activity, View headerView) {
+ super.onViewCreated(activity, headerView);
+ mHeaderView = headerView;
+ mContentLayout = (ViewGroup) headerView.findViewById(R.id.ptr_content);
+ mAnimationDuration = activity.getResources().getInteger(android.R.integer.config_shortAnimTime);
+ }
+
+ @Override
+ public boolean hideHeaderView() {
+ mShowProgressBarOnly = false;
+ return super.hideHeaderView();
+ }
+
+ @Override
+ public boolean showHeaderView() {
+ // Workaround to avoid this bug https://github.com/chrisbanes/ActionBar-PullToRefresh/issues/265
+ // Note, that also remove the alpha animation
+ resetContentLayoutAlpha();
+
+ boolean changeVis = mHeaderView.getVisibility() != View.VISIBLE;
+ mContentLayout.setVisibility(View.VISIBLE);
+ if (changeVis) {
+ mHeaderView.setVisibility(View.VISIBLE);
+ AnimatorSet animSet = new AnimatorSet();
+ ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mHeaderView, "alpha", 0f, 1f);
+ ObjectAnimator transAnim = ObjectAnimator.ofFloat(mContentLayout, "translationY",
+ -mContentLayout.getHeight(), 10f);
+ animSet.playTogether(transAnim, alphaAnim);
+ animSet.play(alphaAnim);
+ animSet.setDuration(mAnimationDuration);
+ animSet.start();
+ if (mShowProgressBarOnly) {
+ mContentLayout.setVisibility(View.INVISIBLE);
+ }
+ }
+ return changeVis;
+ }
+
+ @Override
+ public void onPulled(float percentagePulled) {
+ super.onPulled(percentagePulled);
+ }
+
+ private void resetContentLayoutAlpha() {
+ Compat.setAlpha(mContentLayout, 1f);
+ }
+
+ @Override
+ public void onReset() {
+ super.onReset();
+ // Reset the Content Layout
+ if (mContentLayout != null) {
+ Compat.setAlpha(mContentLayout, 1f);
+ mContentLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onTopScrollChanged(boolean scrolledOnTop) {
+ if (mOnTopScrollChangedListener != null) {
+ mOnTopScrollChangedListener.onTopScrollChanged(scrolledOnTop);
+ }
+ }
+
+ public void setOnTopScrollChangedListener(OnTopScrollChangedListener listener) {
+ mOnTopScrollChangedListener = listener;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHelper.java
new file mode 100644
index 000000000..3c7b46619
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/PullToRefreshHelper.java
@@ -0,0 +1,142 @@
+package org.wordpress.android.util.ptr;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import android.support.v4.content.LocalBroadcastManager;
+import android.view.View;
+
+import org.wordpress.android.util.R;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import uk.co.senab.actionbarpulltorefresh.library.ActionBarPullToRefresh;
+import uk.co.senab.actionbarpulltorefresh.library.ActionBarPullToRefresh.SetupWizard;
+import uk.co.senab.actionbarpulltorefresh.library.Options;
+import uk.co.senab.actionbarpulltorefresh.library.PullToRefreshLayout;
+import uk.co.senab.actionbarpulltorefresh.library.listeners.OnRefreshListener;
+import uk.co.senab.actionbarpulltorefresh.library.viewdelegates.ViewDelegate;
+
+public class PullToRefreshHelper implements OnRefreshListener {
+ public static final String BROADCAST_ACTION_REFRESH_MENU_PRESSED = "REFRESH_MENU_PRESSED";
+ private static final String REFRESH_BUTTON_HIT_COUNT = "REFRESH_BUTTON_HIT_COUNT";
+ private static final Set<Integer> TOAST_FREQUENCY = new HashSet<Integer>(Arrays.asList(1, 5, 10, 20, 40, 80, 160,
+ 320, 640));
+ private PullToRefreshHeaderTransformer mHeaderTransformer;
+ private PullToRefreshLayout mPullToRefreshLayout;
+ private RefreshListener mRefreshListener;
+ private WeakReference<Activity> mActivityRef;
+
+ public PullToRefreshHelper(Activity activity, PullToRefreshLayout pullToRefreshLayout, RefreshListener listener) {
+ init(activity, pullToRefreshLayout, listener, null);
+ }
+
+ public PullToRefreshHelper(Activity activity, PullToRefreshLayout pullToRefreshLayout, RefreshListener listener,
+ java.lang.Class<?> viewClass) {
+ init(activity, pullToRefreshLayout, listener, viewClass);
+ }
+
+ public void init(Activity activity, PullToRefreshLayout pullToRefreshLayout, RefreshListener listener,
+ java.lang.Class<?> viewClass) {
+ mActivityRef = new WeakReference<Activity>(activity);
+ mRefreshListener = listener;
+ mPullToRefreshLayout = pullToRefreshLayout;
+ mHeaderTransformer = new PullToRefreshHeaderTransformer();
+ SetupWizard setupWizard = ActionBarPullToRefresh.from(activity).options(Options.create().headerTransformer(
+ mHeaderTransformer).build()).allChildrenArePullable().listener(this);
+ if (viewClass != null) {
+ setupWizard.useViewDelegate(viewClass, new ViewDelegate() {
+ @Override
+ public boolean isReadyForPull(View view, float v, float v2) {
+ return true;
+ }
+ }
+ );
+ }
+ setupWizard.setup(mPullToRefreshLayout);
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mHeaderTransformer.setShowProgressBarOnly(refreshing);
+ mPullToRefreshLayout.setRefreshing(refreshing);
+ }
+
+ public boolean isRefreshing() {
+ return mPullToRefreshLayout.isRefreshing();
+ }
+
+ @Override
+ public void onRefreshStarted(View view) {
+ mRefreshListener.onRefreshStarted(view);
+ }
+
+ public interface RefreshListener {
+ public void onRefreshStarted(View view);
+ }
+
+ public void setEnabled(boolean enabled) {
+ mPullToRefreshLayout.setEnabled(enabled);
+ }
+
+ public void refreshAction() {
+ Activity activity = mActivityRef.get();
+ if (activity == null) {
+ return;
+ }
+ setRefreshing(true);
+ mRefreshListener.onRefreshStarted(mPullToRefreshLayout);
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+ int refreshHits = preferences.getInt(REFRESH_BUTTON_HIT_COUNT, 0);
+ refreshHits += 1;
+ if (TOAST_FREQUENCY.contains(refreshHits)) {
+ ToastUtils.showToast(activity, R.string.ptr_tip_message, Duration.LONG);
+ }
+ Editor editor = preferences.edit();
+ editor.putInt(REFRESH_BUTTON_HIT_COUNT, refreshHits);
+ editor.commit();
+ }
+
+ public void registerReceiver(Context context) {
+ if (context == null) {
+ return;
+ }
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BROADCAST_ACTION_REFRESH_MENU_PRESSED);
+ LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
+ lbm.registerReceiver(mReceiver, filter);
+ }
+
+ public void unregisterReceiver(Context context) {
+ if (context == null) {
+ return;
+ }
+ try {
+ LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
+ lbm.unregisterReceiver(mReceiver);
+ } catch (IllegalArgumentException e) {
+ // exception occurs if receiver already unregistered (safe to ignore)
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null || intent.getAction() == null) {
+ return;
+ }
+ if (intent.getAction().equals(BROADCAST_ACTION_REFRESH_MENU_PRESSED)) {
+ refreshAction();
+ }
+ }
+ };
+}
diff --git a/libs/utils/WordPressUtils/src/main/res/values/strings.xml b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
new file mode 100644
index 000000000..2061ba880
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="ptr_tip_message">Tip: Pull down to refresh</string>
+</resources>
diff --git a/libs/utils/build.gradle b/libs/utils/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/utils/build.gradle
diff --git a/libs/utils/gradle/wrapper/gradle-wrapper.jar b/libs/utils/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..0087cd3b1
--- /dev/null
+++ b/libs/utils/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/utils/gradle/wrapper/gradle-wrapper.properties b/libs/utils/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..2bdda831e
--- /dev/null
+++ b/libs/utils/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 09 11:48:51 CEST 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-1.11-all.zip
diff --git a/libs/utils/gradlew b/libs/utils/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/utils/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/utils/gradlew.bat b/libs/utils/gradlew.bat
new file mode 100644
index 000000000..aec99730b
--- /dev/null
+++ b/libs/utils/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/utils/settings.gradle b/libs/utils/settings.gradle
new file mode 100644
index 000000000..3519745ed
--- /dev/null
+++ b/libs/utils/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressUtils' \ No newline at end of file
diff --git a/libs/wpcomrest/.gitignore b/libs/wpcomrest/.gitignore
new file mode 100644
index 000000000..8babf679a
--- /dev/null
+++ b/libs/wpcomrest/.gitignore
@@ -0,0 +1,25 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
diff --git a/libs/wpcomrest/LICENSE-GPL b/libs/wpcomrest/LICENSE-GPL
new file mode 100644
index 000000000..356777934
--- /dev/null
+++ b/libs/wpcomrest/LICENSE-GPL
@@ -0,0 +1,279 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
diff --git a/libs/wpcomrest/LICENSE-MIT b/libs/wpcomrest/LICENSE-MIT
new file mode 100644
index 000000000..63ecc8b97
--- /dev/null
+++ b/libs/wpcomrest/LICENSE-MIT
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Automattic Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. \ No newline at end of file
diff --git a/libs/wpcomrest/README.md b/libs/wpcomrest/README.md
new file mode 100644
index 000000000..23db4c914
--- /dev/null
+++ b/libs/wpcomrest/README.md
@@ -0,0 +1,26 @@
+# WordPress REST Client for Android
+
+## Build
+
+To build the library, invoke the following `gradle` command in the project root directory:
+
+ $ ./gradlew build
+
+This will create an `aar` package at this location: `WordPressComRest/build/outputs/aar/WordPressComRest.aar`. Feel free to use it directly or put it in a maven repository.
+
+## Usage
+
+If you don't want to compile and host it. The easiest way to use it in your Android project is to add it as a library in your build.gradle file, don't forget to add the wordpress-mobile maven repository. For instance:
+
+ repositories {
+ maven { url 'http://wordpress-mobile.github.io/WordPress-Android' }
+ }
+
+ dependencies {
+ // use the latest 1.x version
+ compile 'com.automattic:wordpresscom-rest:1.+'
+ }
+
+## LICENSE
+
+This library is dual licensed unded MIT and GPL v2.
diff --git a/libs/wpcomrest/WordPressComRest/build.gradle b/libs/wpcomrest/WordPressComRest/build.gradle
new file mode 100644
index 000000000..8fa117bcf
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies { classpath 'com.android.tools.build:gradle:0.12.+' }
+}
+
+apply plugin: 'com.android.library'
+
+repositories {
+ mavenCentral()
+}
+
+android {
+ defaultPublishConfig 'debug'
+
+ compileSdkVersion 20
+ buildToolsVersion "19.1.0"
+
+ defaultConfig {
+ applicationId "com.wordpress.rest"
+ versionName "1.0.0"
+ versionCode 1
+ minSdkVersion 14
+ targetSdkVersion 20
+ }
+}
+
+dependencies {
+ compile 'com.mcxiaoke.volley:library:1.0.+'
+}
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/AndroidManifest.xml b/libs/wpcomrest/WordPressComRest/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..bb23b43d9
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.wordpress.notereader.tests"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <!-- We add an application tag here just so that we can indicate that
+ this package needs to link against the android.test library,
+ which is needed when building test cases. -->
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <!--
+ This declares that this application uses the instrumentation test runner targeting
+ the package of com.wordpress.notereader. To run the tests use the command:
+ "adb shell am instrument -w com.wordpress.notereader.tests/android.test.InstrumentationTestRunner"
+ -->
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.wordpress.notereader"
+ android:label="Tests for com.wordpress.notereader"/>
+</manifest>
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivity.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivity.java
new file mode 100644
index 000000000..07a0058e3
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivity.java
@@ -0,0 +1,91 @@
+package com.wordpress.notereader;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.content.Intent;
+
+import android.widget.Button;
+import android.widget.EditText;
+
+import android.util.Log;
+import android.view.View;
+
+import com.wordpress.rest.Oauth;
+import com.wordpress.rest.OauthToken;
+import com.wordpress.rest.OauthTokenResponseHandler;
+
+import java.util.Properties;
+import java.io.InputStream;
+
+import org.json.JSONObject;
+
+public class LoginActivity extends Activity {
+
+ private static final String OAUTH_ID_NAME="oauth.appid";
+ private static final String OAUTH_SECRET_NAME="oauth.appsecret";
+ private static final String OAUTH_REDIRECT_URI="oauth.redirect_uri";
+ public static final String OAUTH_TOKEN_EXTRA="oauth-access-token";
+ private static final String TAG="NotesLogin";
+ private Properties mConfig;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ getConfigProperties();
+
+ setContentView(R.layout.login);
+
+ final Oauth oauth = new Oauth(
+ mConfig.getProperty(OAUTH_ID_NAME),
+ mConfig.getProperty(OAUTH_SECRET_NAME),
+ mConfig.getProperty(OAUTH_REDIRECT_URI)
+ );
+
+ Button button = (Button) findViewById(R.id.signin_button);
+ final EditText usernameField = (EditText) findViewById(R.id.username);
+ final EditText passwordField = (EditText) findViewById(R.id.password);
+
+ button.setOnClickListener( new View.OnClickListener(){
+ public void onClick(View v){
+ oauth.requestAccessToken(
+ usernameField.getText().toString(),
+ passwordField.getText().toString(),
+ new OauthTokenResponseHandler(){
+ @Override
+ public void onSuccess(final OauthToken token){
+ runOnUiThread(new Runnable(){
+ @Override
+ public void run(){
+ Intent result = new Intent();
+ result.putExtra(OAUTH_TOKEN_EXTRA, token.toString());
+ setResult(Activity.RESULT_OK, result);
+ finish();
+ }
+ });
+ }
+ @Override
+ public void onFailure(Throwable e, JSONObject response){
+ Log.d(TAG, String.format("Failed %s", response));
+ }
+ }
+ );
+ }
+ });
+
+ }
+
+ protected Properties getConfigProperties(){
+ if (mConfig == null) {
+ mConfig = new Properties();
+ InputStream stream = getResources().openRawResource(R.raw.oauth);
+ try {
+ mConfig.load(stream);
+ } catch(java.io.IOException e){
+ mConfig = null;
+ Log.e(TAG, "Could not load config", e);
+ }
+ }
+ return mConfig;
+ }
+
+} \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivityTest.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivityTest.java
new file mode 100644
index 000000000..8401a3b56
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/LoginActivityTest.java
@@ -0,0 +1,44 @@
+package com.wordpress.notereader;
+
+// import com.wordpress.notereader.R;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.UiThreadTest;
+
+import android.widget.EditText;
+import android.widget.Button;
+
+public class LoginActivityTest extends ActivityInstrumentationTestCase2<LoginActivity> {
+
+ private LoginActivity mActivity;
+
+ public LoginActivityTest(){
+ super("com.wordpress.notereader", LoginActivity.class);
+ }
+
+ public void setUp(){
+ setActivityInitialTouchMode(false);
+ mActivity = getActivity();
+ }
+
+ public void testInitialViewState(){
+ EditText usernameField = (EditText) mActivity.findViewById(R.id.username);
+ Button signInButton = (Button) mActivity.findViewById(R.id.signin_button);
+
+ assertEquals("", usernameField.getText().toString());
+ assertEquals("Sign In", signInButton.getText().toString());
+ }
+
+ @UiThreadTest
+ public void testLogin(){
+ EditText usernameField = (EditText) mActivity.findViewById(R.id.username);
+ EditText passwordField = (EditText) mActivity.findViewById(R.id.password);
+ Button signInButton = (Button) mActivity.findViewById(R.id.signin_button);
+
+ usernameField.setText("mobiletestuser");
+ passwordField.setText("password");
+
+ signInButton.performClick();
+
+ }
+} \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/NotesActivity.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/NotesActivity.java
new file mode 100644
index 000000000..450df9227
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/NotesActivity.java
@@ -0,0 +1,183 @@
+package com.wordpress.notereader;
+
+import static com.wordpress.notereader.WPClient.*;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.loopj.android.http.JsonHttpResponseHandler;
+
+import android.content.SharedPreferences;
+import android.app.ListActivity;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import android.text.Html;
+
+public class NotesActivity extends ListActivity
+{
+ private static final int LOGIN_REQUEST_CODE=0xCC;
+ private static final String PREFERENCES_NAME="account-prefs";
+ private static final String OAUTH_TOKEN_PREFERENCE="oauth-access-token";
+ public static final String TAG="WPNotes";
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ loadAccessTokenFromPreferences();
+ setContentView(R.layout.main);
+ setListAdapter(new NotesAdapter(this));
+ }
+
+ @Override
+ public void onStart(){
+ super.onStart();
+ if (!isAuthenticated()) {
+ startLoginActivity();
+ } else {
+ refreshNotifications();
+ }
+ }
+
+ public void startLoginActivity(){
+ Intent intent = new Intent(this, LoginActivity.class);
+ startActivityForResult(intent, LOGIN_REQUEST_CODE);
+ }
+
+ private void refreshNotifications(){
+ get("notifications", new NotesResponseHandler());
+ }
+
+ private NotesAdapter getNotesAdapter(){
+ return (NotesAdapter) getListAdapter();
+ }
+
+ private class NotesAdapter extends ArrayAdapter<Note> {
+ private static final int LAYOUT_ID=R.layout.note;
+ private static final int VIEW_ID=R.id.note_label;
+ public NotesAdapter(Context context){
+ super(context, LAYOUT_ID, VIEW_ID);
+ }
+
+ public void bindView(int position, View view){
+ Log.d(TAG, String.format("Bind view for %d", position));
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent){
+ View view = super.getView(position, convertView, parent);
+
+ return view;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data){
+ Log.d(TAG, String.format("Activity finished %d with result %d", requestCode, resultCode));
+ switch (requestCode) {
+ case LOGIN_REQUEST_CODE:
+ Log.d(TAG, String.format("Was result (%d) ok? %d", resultCode, Activity.RESULT_OK));
+ if (resultCode == RESULT_OK) {
+ String accessToken = data.getStringExtra(LoginActivity.OAUTH_TOKEN_EXTRA);
+ setAccessToken(accessToken);
+ saveAccessTokenToPreferences(accessToken);
+ refreshNotifications();
+ }
+ break;
+
+ }
+ }
+
+ private void loadAccessTokenFromPreferences(){
+ SharedPreferences prefs = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
+ String token = prefs.getString(OAUTH_TOKEN_PREFERENCE, null);
+ Log.d(TAG, String.format("Retrieved access token: %s", token));
+ setAccessToken(token);
+ }
+
+ private void saveAccessTokenToPreferences(String token){
+ Log.d(TAG, String.format("Saving access token %s", token));
+ SharedPreferences.Editor prefs = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).edit();
+ prefs.putString(OAUTH_TOKEN_PREFERENCE, token);
+ prefs.commit();
+ }
+
+ private class NotesResponseHandler extends JsonHttpResponseHandler {
+ private static final String NOTES_KEY="notes";
+
+ @Override
+ public void onStart(){
+ Log.d(TAG, "Fetching notes");
+ }
+ @Override
+ public void onFinish(){
+ Log.d(TAG, "Finished notes request");
+ }
+ @Override
+ public void onFailure(Throwable error, JSONObject response){
+ Log.e(TAG, String.format("Failed to retrieve notes: %s"), error);
+ }
+
+ @Override
+ public void onFailure(Throwable error, String response){
+ Log.e(TAG, String.format("Failed: %s", response), error);
+ }
+
+ @Override
+ public void onSuccess(int statuScode, JSONObject notes){
+ try {
+ final JSONArray notesArray = notes.getJSONArray(NOTES_KEY);
+ getNotesAdapter().clear();
+ for (int i=0;i<notesArray.length();i++) {
+ JSONObject noteJson = notesArray.getJSONObject(i);
+ getNotesAdapter().add(new Note(noteJson));
+ }
+
+ runOnUiThread( new Runnable(){
+ @Override
+ public void run(){
+ getNotesAdapter().notifyDataSetChanged();
+ }
+ });
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Couldn't retrieve notes", e);
+ }
+ }
+
+ }
+
+ private class Note {
+
+ private static final String SUBJECT_KEY="subject";
+ private static final String SUBJECT_TEXT_KEY="text";
+ private static final String UNKNOWN_SUBJECT="no subject";
+
+ private JSONObject mNoteData;
+
+ public Note(JSONObject noteData){
+ mNoteData = noteData;
+ }
+
+ public String toString(){
+ // try to get the subject.text property
+ try {
+ JSONObject subject = mNoteData.getJSONObject(SUBJECT_KEY);
+ String subjectText = subject.getString(SUBJECT_TEXT_KEY);
+ return Html.fromHtml(subjectText.trim()).toString();
+ } catch (JSONException error) {
+ return UNKNOWN_SUBJECT;
+ }
+ }
+
+ }
+
+}
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/WPClient.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/WPClient.java
new file mode 100644
index 000000000..a1274f213
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/notereader/WPClient.java
@@ -0,0 +1,35 @@
+package com.wordpress.notereader;
+
+import com.wordpress.rest.RestClient;
+import com.loopj.android.http.AsyncHttpResponseHandler;
+import com.loopj.android.http.RequestParams;
+
+public class WPClient {
+
+ public static RestClient restClient = new RestClient();
+
+ public static void setAccessToken(String token){
+ restClient.setAccessToken(token);
+ }
+
+ public static boolean isAuthenticated(){
+ return restClient.isAuthenticated();
+ }
+
+ public static void get(String path, AsyncHttpResponseHandler handler){
+ restClient.get(path, handler);
+ }
+
+ public static void get(String path, RequestParams params, AsyncHttpResponseHandler handler){
+ restClient.get(path, params, handler);
+ }
+
+ public static void notifications(AsyncHttpResponseHandler handler){
+ notifications(null, handler);
+ }
+
+ public static void notifications(RequestParams params, AsyncHttpResponseHandler handler){
+ get("notifications", params, handler);
+ }
+
+} \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/OauthTest.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/OauthTest.java
new file mode 100644
index 000000000..2958b8934
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/OauthTest.java
@@ -0,0 +1,32 @@
+package com.wordpress.rest;
+
+import android.test.AndroidTestCase;
+import com.wordpress.rest.Oauth;
+
+import com.wordpress.util.TestExecutorService;
+
+import android.util.Log;
+
+public class OauthTest extends AndroidTestCase {
+
+ public final String TAG="WordPressTest";
+
+ private String mAppId;
+ private String mAppSecret;
+ private String mAppRedirectURI;
+ private Oauth mClient;
+
+ @Override
+ public void setUp(){
+ mClient = new Oauth(mAppId, mAppSecret, mAppRedirectURI);
+ }
+
+ public void testRequestAuthorizationURL(){
+ String url = mClient.getAuthorizationURL();
+
+ String expected = String.format("https://public-api.wordpress.com/oauth2/authorize?client_id=%s&redirect_uri=%s&response_type=code", mClient.getAppID(), mClient.getAppSecret(), mClient.getAppRedirectURI());
+ assertEquals(expected, url);
+ }
+
+
+} \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/RestClientTest.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/RestClientTest.java
new file mode 100644
index 000000000..7fa2053f1
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/rest/RestClientTest.java
@@ -0,0 +1,26 @@
+package com.wordpress.rest;
+
+import android.test.AndroidTestCase;
+
+public class RestClientTest extends AndroidTestCase {
+
+ public void testGetAbsoluteURLWithPath(){
+ String path = "me";
+ String url = RestClient.getAbsoluteURL(path);
+ String expected = String.format("%s%s", RestClient.REST_API_ENDPOINT_URL, path);
+ assertEquals(expected, url);
+ }
+
+ public void testGetAbsoluteURLWithLeadingSlash(){
+ String path = "/sites/mobileprojects.wordpress.com/posts";
+ String url = RestClient.getAbsoluteURL(path);
+ String expected = String.format("https://public-api.wordpress.com/rest/v1%s", path);
+ assertEquals(expected, url);
+ }
+
+ public void testGetAbsoluteURLWithFullURL(){
+ String expected = String.format("%s%s", RestClient.REST_API_ENDPOINT_URL, "notes");
+ assertEquals(expected, RestClient.getAbsoluteURL(expected));
+ }
+
+} \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/util/TestExecutorService.java b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/util/TestExecutorService.java
new file mode 100644
index 000000000..0ff3189e3
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/java/src/com/wordpress/util/TestExecutorService.java
@@ -0,0 +1,20 @@
+package com.wordpress.util;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.Future;
+import java.util.concurrent.BlockingQueue;
+
+public class TestExecutorService extends ThreadPoolExecutor {
+ public TestExecutorService(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
+ }
+
+ @Override
+ public Future<?> submit(Runnable runnable) {
+ FutureTask futureTask = new FutureTask(runnable, null);
+ futureTask.run(); // or queue this future to be run when you want it to be run
+ return futureTask;
+ }
+}
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-hdpi/ic_launcher.png b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..96a442e5b
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-ldpi/ic_launcher.png b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 000000000..99238729d
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-mdpi/ic_launcher.png b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..359047dfa
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-xhdpi/ic_launcher.png b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..71c6d760f
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/login.xml b/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/login.xml
new file mode 100644
index 000000000..d38f3cc88
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/login.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp"
+ >
+ <EditText android:id="@+id/username"
+ android:hint="@string/username_hint"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+ <EditText android:id="@+id/password"
+ android:hint="@string/password_hint"
+ android:password="true"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp" />
+ <Button android:id="@+id/signin_button"
+ android:text="@string/signin_label"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout>
+
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/main.xml b/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/main.xml
new file mode 100644
index 000000000..6bf742217
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/main.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+ <ListView android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+ <TextView
+ android:id="@android:id/empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/no_notifications" />
+</LinearLayout>
+
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/note.xml b/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/note.xml
new file mode 100644
index 000000000..4fab73b9f
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/layout/note.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="8sp"
+ >
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <ImageView
+ android:id="@+id/note_image"
+ android:layout_width="48sp"
+ android:layout_height="48sp"
+ android:scaleType="fitCenter" />
+ <TextView
+ android:id="@+id/note_label"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:gravity="fill" />
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/raw/oauth.properties b/libs/wpcomrest/WordPressComRest/src/androidTest/res/raw/oauth.properties
new file mode 100644
index 000000000..fa5b5d7b2
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/raw/oauth.properties
@@ -0,0 +1,3 @@
+oauth.appid=11
+oauth.appsecret=zf66wMPjz0BNohvOLsfYGyM2YbxwgVEVyiXXQDcVwzK73JDOPPuhrvCuoln0Ul9o
+oauth.redirect_uri=https://wordpress.com/ \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/androidTest/res/values/strings.xml b/libs/wpcomrest/WordPressComRest/src/androidTest/res/values/strings.xml
new file mode 100644
index 000000000..f8f6bccd9
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Notes</string>
+ <string name="signin_label">Sign In</string>
+ <string name="username_hint">Username</string>
+ <string name="password_hint">Password</string>
+ <string name="no_notifications">No notifications</string>
+</resources>
diff --git a/libs/wpcomrest/WordPressComRest/src/main/AndroidManifest.xml b/libs/wpcomrest/WordPressComRest/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..95a7bbddc
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.wordpress.rest"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <uses-sdk android:minSdkVersion="3" />
+ <uses-permission android:name="android.permission.INTERNET" />
+</manifest>
diff --git a/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/Oauth.java b/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/Oauth.java
new file mode 100644
index 000000000..250921772
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/Oauth.java
@@ -0,0 +1,168 @@
+package com.wordpress.rest;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Response;
+import com.android.volley.toolbox.HttpHeaderParser;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Oauth {
+ public static final String TAG = "WordPressREST";
+
+ public static final String AUTHORIZE_ENDPOINT = "https://public-api.wordpress.com/oauth2/authorize";
+ private static final String AUTHORIZED_ENDPOINT_FORMAT = "%s?client_id=%s&redirect_uri=%s&response_type=code";
+
+ public static final String TOKEN_ENDPOINT = "https://public-api.wordpress.com/oauth2/token";
+
+ public static final String CLIENT_ID_PARAM_NAME = "client_id";
+ public static final String REDIRECT_URI_PARAM_NAME = "redirect_uri";
+ public static final String CLIENT_SECRET_PARAM_NAME = "client_secret";
+ public static final String CODE_PARAM_NAME = "code";
+ public static final String GRANT_TYPE_PARAM_NAME = "grant_type";
+ public static final String USERNAME_PARAM_NAME = "username";
+ public static final String PASSWORD_PARAM_NAME = "password";
+
+ public static final String PASSWORD_GRANT_TYPE = "password";
+ public static final String BEARER_GRANT_TYPE = "bearer";
+ public static final String AUTHORIZATION_CODE_GRANT_TYPE = "authorization_code";
+
+ private String mAppId;
+ private String mAppSecret;
+ private String mAppRedirectURI;
+
+ public interface Listener extends Response.Listener<Token> {
+ }
+ public interface ErrorListener extends Response.ErrorListener {
+ }
+
+ public Oauth(String appId, String appSecret, String redirectURI) {
+ mAppId = appId;
+ mAppSecret = appSecret;
+ mAppRedirectURI = redirectURI;
+ }
+
+ public String getAppID() {
+ return mAppId;
+ }
+
+ public String getAppSecret() {
+ return mAppSecret;
+ }
+
+ public String getAppRedirectURI() {
+ return mAppRedirectURI;
+ }
+
+ public String getAuthorizationURL() {
+ return String.format(AUTHORIZED_ENDPOINT_FORMAT, AUTHORIZE_ENDPOINT, getAppID(), getAppRedirectURI());
+ }
+
+ public Request makeRequest(String username, String password, Listener listener, ErrorListener errorListener) {
+ return new PasswordRequest(getAppID(), getAppSecret(), getAppRedirectURI(), username, password, listener,
+ errorListener);
+ }
+
+ public Request makeRequest(String code, Listener listener, ErrorListener errorListener) {
+ return new BearerRequest(getAppID(), getAppSecret(), getAppRedirectURI(), code, listener, errorListener);
+ }
+
+ private static class Request extends com.android.volley.Request<Token> {
+ private final Listener mListener;
+ protected Map<String, String> mParams = new HashMap<String, String>();
+
+ Request(String appId, String appSecret, String redirectUri, Listener listener, ErrorListener errorListener) {
+ super(Method.POST, TOKEN_ENDPOINT, errorListener);
+ mListener = listener;
+ mParams.put(CLIENT_ID_PARAM_NAME, appId);
+ mParams.put(CLIENT_SECRET_PARAM_NAME, appSecret);
+ mParams.put(REDIRECT_URI_PARAM_NAME, redirectUri);
+ }
+
+ @Override
+ public Map<String, String> getParams() {
+ return mParams;
+ }
+
+ @Override
+ public void deliverResponse(Token token) {
+ mListener.onResponse(token);
+ }
+
+ @Override
+ protected Response<Token> parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ JSONObject tokenData = new JSONObject(jsonString);
+ return Response.success(Token.fromJSONObject(tokenData), HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException e) {
+ return Response.error(new ParseError(e));
+ } catch (JSONException je) {
+ return Response.error(new ParseError(je));
+ }
+ }
+ }
+
+ public static class PasswordRequest extends Request {
+
+ public PasswordRequest(String appId, String appSecret, String redirectUri, String username, String password,
+ Listener listener, ErrorListener errorListener) {
+ super(appId, appSecret, redirectUri, listener, errorListener);
+ mParams.put(USERNAME_PARAM_NAME, username);
+ mParams.put(PASSWORD_PARAM_NAME, password);
+ mParams.put(GRANT_TYPE_PARAM_NAME, PASSWORD_GRANT_TYPE);
+ }
+ }
+
+ public static class BearerRequest extends Request {
+
+ public BearerRequest(String appId, String appSecret, String redirectUri, String code, Listener listener,
+ ErrorListener errorListener) {
+ super(appId, appSecret, redirectUri, listener, errorListener);
+ mParams.put(CODE_PARAM_NAME, code);
+ mParams.put(GRANT_TYPE_PARAM_NAME, BEARER_GRANT_TYPE);
+ }
+ }
+
+ public static class Token {
+
+ private static final String TOKEN_TYPE_FIELD_NAME = "token_type";
+ private static final String ACCESS_TOKEN_FIELD_NAME = "access_token";
+ private static final String BLOG_URL_FIELD_NAME = "blog_url";
+ private static final String SCOPE_FIELD_NAME = "scope";
+ private static final String BLOG_ID_FIELD_NAME = "blog_id";
+
+ private String mTokenType;
+ private String mScope;
+ private String mAccessToken;
+ private String mBlogUrl;
+ private String mBlogId;
+
+ public Token(String accessToken, String blogUrl, String blogId, String scope, String tokenType) {
+ mAccessToken = accessToken;
+ mBlogUrl = blogUrl;
+ mBlogId = blogId;
+ mScope = scope;
+ mTokenType = tokenType;
+ }
+
+ public String getAccessToken() {
+ return mAccessToken;
+ }
+
+ public String toString() {
+ return getAccessToken();
+ }
+
+ public static Token fromJSONObject(JSONObject tokenJSON) throws JSONException {
+ return new Token(tokenJSON.getString(ACCESS_TOKEN_FIELD_NAME), tokenJSON.getString(BLOG_URL_FIELD_NAME),
+ tokenJSON.getString(BLOG_ID_FIELD_NAME), tokenJSON.getString(SCOPE_FIELD_NAME), tokenJSON.getString(
+ TOKEN_TYPE_FIELD_NAME));
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestClient.java b/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestClient.java
new file mode 100644
index 000000000..5320f679c
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestClient.java
@@ -0,0 +1,101 @@
+package com.wordpress.rest;
+
+import com.android.volley.Request.Method;
+import com.android.volley.RequestQueue;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Map;
+
+public class RestClient {
+ public static final String TAG = "WordPressREST";
+ public static final String REST_API_ENDPOINT_URL = "https://public-api.wordpress.com/rest/v1/";
+ public static final String PARAMS_ENCODING = "UTF-8";
+
+ private RequestQueue mQueue;
+ private String mAccessToken;
+ private String mUserAgent;
+
+ public RestClient(RequestQueue queue) {
+ mQueue = queue;
+ }
+
+ public RestClient(RequestQueue queue, String token) {
+ this(queue);
+ mAccessToken = token;
+ }
+
+ public RestRequest get(String path, Listener<JSONObject> listener, ErrorListener errorListener) {
+ return makeRequest(Method.GET, getAbsoluteURL(path), null, listener, errorListener);
+ }
+
+ public RestRequest post(String path, Map<String, String> body, Listener<JSONObject> listener,
+ ErrorListener errorListener) {
+ return makeRequest(Method.POST, getAbsoluteURL(path), body, listener, errorListener);
+ }
+
+ public RestRequest makeRequest(int method, String url, Map<String, String> params, Listener<JSONObject> listener,
+ ErrorListener errorListener) {
+ RestRequest request = new RestRequest(method, url, params, listener, errorListener);
+ request.setUserAgent(mUserAgent);
+ request.setAccessToken(mAccessToken);
+ return request;
+ }
+
+ public RestRequest send(RestRequest request) {
+ // Volley send the request
+ mQueue.add(request);
+ return request;
+ }
+
+ public static String getAbsoluteURL(String url) {
+ // if it already starts with our endpoint, let it pass through
+ if (url.indexOf(REST_API_ENDPOINT_URL) == 0) {
+ return url;
+ }
+ // if it has a leading slash, remove it
+ if (url.indexOf("/") == 0) {
+ url = url.substring(1);
+ }
+ // prepend the endpoint
+ return String.format("%s%s", REST_API_ENDPOINT_URL, url);
+ }
+
+ public static String getAbsoluteURL(String path, Map<String, String> params) {
+ String url = getAbsoluteURL(path);
+ if (params != null) {
+ // build a query string
+ StringBuilder query = new StringBuilder();
+ try {
+ for (Map.Entry<String, String> entry : params.entrySet()) {
+ query.append(URLEncoder.encode(entry.getKey(), PARAMS_ENCODING));
+ query.append("=");
+ query.append(URLEncoder.encode(entry.getValue(), PARAMS_ENCODING));
+ query.append("&");
+ }
+ } catch (UnsupportedEncodingException uee) {
+ throw new RuntimeException("Encoding not supported: " + PARAMS_ENCODING, uee);
+ }
+ url = String.format("%s?%s", url, query);
+ }
+ return url;
+ }
+
+ //Sets the User-Agent header to be sent with each future request.
+ public void setUserAgent(String userAgent) {
+ mUserAgent = userAgent;
+ }
+
+ // Sets the auth token to be used in the request header
+ public void setAccessToken(String token) {
+ mAccessToken = token;
+ }
+
+ public boolean isAuthenticated() {
+ return mAccessToken != null;
+ }
+}
diff --git a/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestRequest.java b/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestRequest.java
new file mode 100644
index 000000000..8ad4653d5
--- /dev/null
+++ b/libs/wpcomrest/WordPressComRest/src/main/java/com/wordpress/rest/RestRequest.java
@@ -0,0 +1,83 @@
+package com.wordpress.rest;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.toolbox.HttpHeaderParser;
+
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import java.io.UnsupportedEncodingException;
+
+public class RestRequest extends Request<JSONObject> {
+ public static final String USER_AGENT_HEADER = "User-Agent";
+ public static final String REST_AUTHORIZATION_HEADER = "Authorization";
+ public static final String REST_AUTHORIZATION_FORMAT = "Bearer %s";
+
+ public interface Listener extends Response.Listener<JSONObject> {
+ } //This is just a shortcut for Response.Listener<JSONObject>
+ public interface ErrorListener extends Response.ErrorListener {
+ } //This is just a shortcut for Response.ErrorListener
+
+ private final com.android.volley.Response.Listener<JSONObject> mListener;
+ private final Map<String, String> mParams;
+ private final Map<String, String> mHeaders = new HashMap<String, String>(2);
+
+ public RestRequest(int method, String url, Map<String, String> params,
+ com.android.volley.Response.Listener<JSONObject> listener,
+ com.android.volley.Response.ErrorListener errorListener) {
+ super(method, url, errorListener);
+ mParams = params;
+ mListener = listener;
+ }
+
+ public void removeAccessToken() {
+ setAccessToken(null);
+ }
+
+ public void setAccessToken(String token) {
+ if (token == null) {
+ mHeaders.remove(REST_AUTHORIZATION_HEADER);
+ } else {
+ mHeaders.put(REST_AUTHORIZATION_HEADER, String.format(REST_AUTHORIZATION_FORMAT, token));
+ }
+ }
+
+ public void setUserAgent(String userAgent) {
+ mHeaders.put(USER_AGENT_HEADER, userAgent);
+ }
+
+ @Override
+ public Map<String, String> getHeaders() {
+ return mHeaders;
+ }
+
+ @Override
+ protected void deliverResponse(JSONObject response) {
+ if (mListener != null) {
+ mListener.onResponse(response);
+ }
+ }
+
+ @Override
+ protected Map<String, String> getParams() {
+ return mParams;
+ }
+
+ @Override
+ protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ return Response.success(new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException e) {
+ return Response.error(new ParseError(e));
+ } catch (JSONException je) {
+ return Response.error(new ParseError(je));
+ }
+ }
+}
diff --git a/libs/wpcomrest/build.gradle b/libs/wpcomrest/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/wpcomrest/build.gradle
diff --git a/libs/wpcomrest/gradle/wrapper/gradle-wrapper.jar b/libs/wpcomrest/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..0087cd3b1
--- /dev/null
+++ b/libs/wpcomrest/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/wpcomrest/gradle/wrapper/gradle-wrapper.properties b/libs/wpcomrest/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..55431dfe6
--- /dev/null
+++ b/libs/wpcomrest/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Jul 10 14:47:28 CEST 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
diff --git a/libs/wpcomrest/gradlew b/libs/wpcomrest/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/wpcomrest/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/wpcomrest/gradlew.bat b/libs/wpcomrest/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/wpcomrest/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/wpcomrest/settings.gradle b/libs/wpcomrest/settings.gradle
new file mode 100644
index 000000000..473ceab34
--- /dev/null
+++ b/libs/wpcomrest/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressComRest'
diff --git a/libs/wpcomrest/test/.gitignore b/libs/wpcomrest/test/.gitignore
new file mode 100644
index 000000000..81e9c938d
--- /dev/null
+++ b/libs/wpcomrest/test/.gitignore
@@ -0,0 +1,3 @@
+bin/*
+local.properties
+gen/* \ No newline at end of file
diff --git a/libs/wpcomrest/test/ant.properties b/libs/wpcomrest/test/ant.properties
new file mode 100644
index 000000000..02634db80
--- /dev/null
+++ b/libs/wpcomrest/test/ant.properties
@@ -0,0 +1,18 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
+tested.project.dir=./app
diff --git a/libs/wpcomrest/test/app/.gitignore b/libs/wpcomrest/test/app/.gitignore
new file mode 100644
index 000000000..81e9c938d
--- /dev/null
+++ b/libs/wpcomrest/test/app/.gitignore
@@ -0,0 +1,3 @@
+bin/*
+local.properties
+gen/* \ No newline at end of file
diff --git a/libs/wpcomrest/test/app/AndroidManifest.xml b/libs/wpcomrest/test/app/AndroidManifest.xml
new file mode 100644
index 000000000..a1ea8b431
--- /dev/null
+++ b/libs/wpcomrest/test/app/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.wordpress.notereader"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+ <activity android:name="NotesActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".LoginActivity" />
+ </application>
+ <uses-permission android:name="android.permission.INTERNET" />
+</manifest>
diff --git a/libs/wpcomrest/test/app/ant.properties b/libs/wpcomrest/test/app/ant.properties
new file mode 100644
index 000000000..b0971e891
--- /dev/null
+++ b/libs/wpcomrest/test/app/ant.properties
@@ -0,0 +1,17 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
diff --git a/libs/wpcomrest/test/app/build.xml b/libs/wpcomrest/test/app/build.xml
new file mode 100644
index 000000000..b05b8e9e5
--- /dev/null
+++ b/libs/wpcomrest/test/app/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="NotesActivity" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked into
+ Version Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The ant.properties file can be created by you. It is only edited by the
+ 'android' tool to add properties to it.
+ This is the place to change some Ant specific build properties.
+ Here are some properties you may want to change/update:
+
+ source.dir
+ The name of the source directory. Default is 'src'.
+ out.dir
+ The name of the output directory. Default is 'bin'.
+
+ For other overridable properties, look at the beginning of the rules
+ files in the SDK, at tools/ant/build.xml
+
+ Properties related to the SDK location or the project target should
+ be updated using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems.
+
+ -->
+ <property file="ant.properties" />
+
+ <!-- if sdk.dir was not set from one of the property file, then
+ get it from the ANDROID_HOME env var.
+ This must be done before we load project.properties since
+ the proguard config can use sdk.dir -->
+ <property environment="env" />
+ <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+ <isset property="env.ANDROID_HOME" />
+ </condition>
+
+ <!-- The project.properties file is created and updated by the 'android'
+ tool, as well as ADT.
+
+ This contains project specific properties such as project target, and library
+ dependencies. Lower level build properties are stored in ant.properties
+ (or in .classpath for Eclipse projects).
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems. -->
+ <loadproperties srcFile="project.properties" />
+
+ <!-- quick check on sdk.dir -->
+ <fail
+ message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+ unless="sdk.dir"
+ />
+
+ <!--
+ Import per project custom build rules if present at the root of the project.
+ This is the place to put custom intermediary targets such as:
+ -pre-build
+ -pre-compile
+ -post-compile (This is typically used for code obfuscation.
+ Compiled code location: ${out.classes.absolute.dir}
+ If this is not done in place, override ${out.dex.input.absolute.dir})
+ -post-package
+ -post-build
+ -pre-clean
+ -->
+ <import file="custom_rules.xml" optional="true" />
+
+ <!-- Import the actual build file.
+
+ To customize existing targets, there are two options:
+ - Customize only one target:
+ - copy/paste the target into this file, *before* the
+ <import> task.
+ - customize it to your needs.
+ - Customize the whole content of build.xml
+ - copy/paste the content of the rules files (minus the top node)
+ into this file, replacing the <import> task.
+ - customize to your needs.
+
+ ***********************
+ ****** IMPORTANT ******
+ ***********************
+ In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+ in order to avoid having your file be overridden by tools such as "android update project"
+ -->
+ <!-- version-tag: 1 -->
+ <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/libs/wpcomrest/test/app/proguard-project.txt b/libs/wpcomrest/test/app/proguard-project.txt
new file mode 100644
index 000000000..f2fe1559a
--- /dev/null
+++ b/libs/wpcomrest/test/app/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libs/wpcomrest/test/app/project.properties b/libs/wpcomrest/test/app/project.properties
new file mode 100644
index 000000000..fe8b84557
--- /dev/null
+++ b/libs/wpcomrest/test/app/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-16
+android.library.reference.1=../../
diff --git a/libs/wpcomrest/test/build.xml b/libs/wpcomrest/test/build.xml
new file mode 100644
index 000000000..7f26967c7
--- /dev/null
+++ b/libs/wpcomrest/test/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="NoteReaderTest" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked into
+ Version Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The ant.properties file can be created by you. It is only edited by the
+ 'android' tool to add properties to it.
+ This is the place to change some Ant specific build properties.
+ Here are some properties you may want to change/update:
+
+ source.dir
+ The name of the source directory. Default is 'src'.
+ out.dir
+ The name of the output directory. Default is 'bin'.
+
+ For other overridable properties, look at the beginning of the rules
+ files in the SDK, at tools/ant/build.xml
+
+ Properties related to the SDK location or the project target should
+ be updated using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems.
+
+ -->
+ <property file="ant.properties" />
+
+ <!-- if sdk.dir was not set from one of the property file, then
+ get it from the ANDROID_HOME env var.
+ This must be done before we load project.properties since
+ the proguard config can use sdk.dir -->
+ <property environment="env" />
+ <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+ <isset property="env.ANDROID_HOME" />
+ </condition>
+
+ <!-- The project.properties file is created and updated by the 'android'
+ tool, as well as ADT.
+
+ This contains project specific properties such as project target, and library
+ dependencies. Lower level build properties are stored in ant.properties
+ (or in .classpath for Eclipse projects).
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems. -->
+ <loadproperties srcFile="project.properties" />
+
+ <!-- quick check on sdk.dir -->
+ <fail
+ message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+ unless="sdk.dir"
+ />
+
+ <!--
+ Import per project custom build rules if present at the root of the project.
+ This is the place to put custom intermediary targets such as:
+ -pre-build
+ -pre-compile
+ -post-compile (This is typically used for code obfuscation.
+ Compiled code location: ${out.classes.absolute.dir}
+ If this is not done in place, override ${out.dex.input.absolute.dir})
+ -post-package
+ -post-build
+ -pre-clean
+ -->
+ <import file="custom_rules.xml" optional="true" />
+
+ <!-- Import the actual build file.
+
+ To customize existing targets, there are two options:
+ - Customize only one target:
+ - copy/paste the target into this file, *before* the
+ <import> task.
+ - customize it to your needs.
+ - Customize the whole content of build.xml
+ - copy/paste the content of the rules files (minus the top node)
+ into this file, replacing the <import> task.
+ - customize to your needs.
+
+ ***********************
+ ****** IMPORTANT ******
+ ***********************
+ In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+ in order to avoid having your file be overridden by tools such as "android update project"
+ -->
+ <!-- version-tag: 1 -->
+ <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/libs/wpcomrest/test/proguard-project.txt b/libs/wpcomrest/test/proguard-project.txt
new file mode 100644
index 000000000..f2fe1559a
--- /dev/null
+++ b/libs/wpcomrest/test/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libs/wpcomrest/test/project.properties b/libs/wpcomrest/test/project.properties
new file mode 100644
index 000000000..9b84a6b4b
--- /dev/null
+++ b/libs/wpcomrest/test/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-16
diff --git a/libs/wpcomrest/tools/deploy-mvn-artifact.conf-example b/libs/wpcomrest/tools/deploy-mvn-artifact.conf-example
new file mode 100644
index 000000000..5aaf644ad
--- /dev/null
+++ b/libs/wpcomrest/tools/deploy-mvn-artifact.conf-example
@@ -0,0 +1 @@
+LOCAL_GH_PAGES=file:///Users/max/work/automattic/WordPress-Android-gh-pages/
diff --git a/libs/wpcomrest/tools/deploy-mvn-artifact.sh b/libs/wpcomrest/tools/deploy-mvn-artifact.sh
new file mode 100755
index 000000000..e76998f93
--- /dev/null
+++ b/libs/wpcomrest/tools/deploy-mvn-artifact.sh
@@ -0,0 +1,19 @@
+#!/bin/sh +v
+
+. tools/deploy-mvn-artifact.conf
+PROJECT=WordPressComRest
+VERSION=`grep -E 'versionName' $PROJECT/build.gradle \
+ | sed s/versionName// \
+ | grep -Eo "[a-zA-Z0-9.-]+"`
+GROUPID=com.automattic
+ARTIFACTID=wordpresscom-rest
+AARFILE=$PROJECT/build/outputs/aar/WordPressComRest.aar
+
+# Deploy release build
+mvn deploy:deploy-file -Dfile=$AARFILE \
+ -Durl=$LOCAL_GH_PAGES -DgroupId=$GROUPID \
+ -DartifactId=$ARTIFACTID -Dversion=$VERSION
+
+echo ========================================
+echo
+echo \"$GROUPID:$ARTIFACTID:$VERSION\" deployed \ No newline at end of file