diff options
Diffstat (limited to 'libs')
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 Binary files differnew file mode 100644 index 000000000..8c0fb64a8 --- /dev/null +++ b/libs/networking/gradle/wrapper/gradle-wrapper.jar 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 " " + */ + 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("<"); + } else if (c == '>') { + out.append(">"); + } else if (c == '&') { + out.append("&"); + } else if (c > 0x7E || c < ' ') { + out.append("&#").append((int) c).append(";"); + } else if (c == ' ') { + while (i + 1 < length && text.charAt(i + 1) == ' ') { + out.append(" "); + 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 Binary files differnew file mode 100644 index 000000000..0087cd3b1 --- /dev/null +++ b/libs/utils/gradle/wrapper/gradle-wrapper.jar 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 Binary files differnew file mode 100644 index 000000000..96a442e5b --- /dev/null +++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000..99238729d --- /dev/null +++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-ldpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000..359047dfa --- /dev/null +++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000..71c6d760f --- /dev/null +++ b/libs/wpcomrest/WordPressComRest/src/androidTest/res/drawable-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000..0087cd3b1 --- /dev/null +++ b/libs/wpcomrest/gradle/wrapper/gradle-wrapper.jar 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 |