aboutsummaryrefslogtreecommitdiff
path: root/libs/utils/WordPressUtils
diff options
context:
space:
mode:
Diffstat (limited to 'libs/utils/WordPressUtils')
-rw-r--r--libs/utils/WordPressUtils/README.md1
-rw-r--r--libs/utils/WordPressUtils/build.gradle65
-rw-r--r--libs/utils/WordPressUtils/gradle.properties-example6
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java32
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java25
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java108
-rw-r--r--libs/utils/WordPressUtils/src/main/AndroidManifest.xml5
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java16
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java100
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java272
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java74
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java246
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java94
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java91
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java75
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java106
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java35
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java116
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java84
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java31
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java156
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java649
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java251
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java52
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java107
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java334
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java89
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java45
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java97
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java104
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java87
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java16
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java31
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java142
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java327
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java14
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java7
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java9
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java37
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java257
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java38
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java58
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java144
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java339
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java87
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java21
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java72
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java47
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java59
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java177
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java140
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java44
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java34
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java45
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java299
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java33
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java62
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/attrs.xml7
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/strings.xml5
59 files changed, 6104 insertions, 0 deletions
diff --git a/libs/utils/WordPressUtils/README.md b/libs/utils/WordPressUtils/README.md
new file mode 100644
index 000000000..62a759585
--- /dev/null
+++ b/libs/utils/WordPressUtils/README.md
@@ -0,0 +1 @@
+# org.wordpress.android.util \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/build.gradle b/libs/utils/WordPressUtils/build.gradle
new file mode 100644
index 000000000..96191a5d0
--- /dev/null
+++ b/libs/utils/WordPressUtils/build.gradle
@@ -0,0 +1,65 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ classpath 'com.novoda:bintray-release:0.3.4'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.novoda.bintray-release'
+
+repositories {
+ jcenter()
+}
+
+dependencies {
+ compile('commons-lang:commons-lang:2.6') {
+ exclude group: 'commons-logging'
+ }
+ compile 'com.mcxiaoke.volley:library:1.0.18'
+ compile 'com.android.support:support-v13:24.2.1'
+}
+
+android {
+ useLibrary 'org.apache.http.legacy'
+
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionName "1.14.0"
+ minSdkVersion 14
+ targetSdkVersion 24
+ }
+}
+
+android.libraryVariants.all { variant ->
+ task("generate${variant.name}Javadoc", type: Javadoc) {
+ description "Generates Javadoc for $variant.name."
+ source = variant.javaCompile.source
+ classpath = files(variant.javaCompile.classpath.files, android.getBootClasspath())
+
+ options {
+ links "http://docs.oracle.com/javase/7/docs/api/"
+ }
+ exclude '**/R.java'
+ }
+}
+
+publish {
+ artifactId = 'utils'
+ userOrg = 'wordpress-mobile'
+ groupId = 'org.wordpress'
+ uploadName = 'utils'
+ description = 'Utils library for Android'
+ publishVersion = android.defaultConfig.versionName
+ licences = ['MIT', 'GPL']
+ website = 'https://github.com/wordpress-mobile/WordPress-Utils-Android/'
+ dryRun = 'false'
+ autoPublish = 'true'
+}
diff --git a/libs/utils/WordPressUtils/gradle.properties-example b/libs/utils/WordPressUtils/gradle.properties-example
new file mode 100644
index 000000000..5281d935c
--- /dev/null
+++ b/libs/utils/WordPressUtils/gradle.properties-example
@@ -0,0 +1,6 @@
+ossrhUsername=hello
+ossrhPassword=world
+
+signing.keyId=byebye
+signing.password=secret
+signing.secretKeyRingFile=/home/user/secret.gpg
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java
new file mode 100644
index 000000000..f7c747ff7
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java
@@ -0,0 +1,32 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class JSONUtilsTest extends InstrumentationTestCase {
+ public void testQueryJSONNullSource1() {
+ JSONUtils.queryJSON((JSONObject) null, "", "");
+ }
+
+ public void testQueryJSONNullSource2() {
+ JSONUtils.queryJSON((JSONArray) null, "", "");
+ }
+
+ public void testQueryJSONNullQuery1() {
+ JSONUtils.queryJSON(new JSONObject(), null, "");
+ }
+
+ public void testQueryJSONNullQuery2() {
+ JSONUtils.queryJSON(new JSONArray(), null, "");
+ }
+
+ public void testQueryJSONNullReturnValue1() {
+ JSONUtils.queryJSON(new JSONObject(), "", null);
+ }
+
+ public void testQueryJSONNullReturnValue2() {
+ JSONUtils.queryJSON(new JSONArray(), "", null);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java
new file mode 100644
index 000000000..c506a452e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+public class ShortcodeUtilsTest extends InstrumentationTestCase {
+ public void testGetVideoPressShortcodeFromId() {
+ assertEquals("[wpvideo abcd1234]", ShortcodeUtils.getVideoPressShortcodeFromId("abcd1234"));
+ }
+
+ public void testGetVideoPressShortcodeFromNullId() {
+ assertEquals("", ShortcodeUtils.getVideoPressShortcodeFromId(null));
+ }
+
+ public void testGetVideoPressIdFromCorrectShortcode() {
+ assertEquals("abcd1234", ShortcodeUtils.getVideoPressIdFromShortCode("[wpvideo abcd1234]"));
+ }
+
+ public void testGetVideoPressIdFromInvalidShortcode() {
+ assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode("[other abcd1234]"));
+ }
+
+ public void testGetVideoPressIdFromNullShortcode() {
+ assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode(null));
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
new file mode 100644
index 000000000..abf7a8fae
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
@@ -0,0 +1,108 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UrlUtilsTest extends InstrumentationTestCase {
+ public void testGetDomainFromUrlWithEmptyStringDoesNotReturnNull() {
+ assertNotNull(UrlUtils.getHost(""));
+ }
+
+ public void testGetDomainFromUrlWithNoHostDoesNotReturnNull() {
+ assertNotNull(UrlUtils.getHost("wordpress"));
+ }
+
+ public void testGetDomainFromUrlWithHostReturnsHost() {
+ String url = "http://www.wordpress.com";
+ String host = UrlUtils.getHost(url);
+
+ assertTrue(host.equals("www.wordpress.com"));
+ }
+
+ public void testAppendUrlParameter1() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test", "preview", "true");
+ assertEquals("http://wp.com/test?preview=true", url);
+ }
+
+ public void testAppendUrlParameter2() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony", "preview", "true");
+ assertEquals("http://wp.com/test?q=pony&preview=true", url);
+ }
+
+ public void testAppendUrlParameter3() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony#unicorn", "preview", "true");
+ assertEquals("http://wp.com/test?q=pony&preview=true#unicorn", url);
+ }
+
+ public void testAppendUrlParameter4() {
+ String url = UrlUtils.appendUrlParameter("/relative/test", "preview", "true");
+ assertEquals("/relative/test?preview=true", url);
+ }
+
+ public void testAppendUrlParameter5() {
+ String url = UrlUtils.appendUrlParameter("/relative/", "preview", "true");
+ assertEquals("/relative/?preview=true", url);
+ }
+
+ public void testAppendUrlParameter6() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test/", "preview", "true");
+ assertEquals("http://wp.com/test/?preview=true", url);
+ }
+
+ public void testAppendUrlParameter7() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test/?q=pony", "preview", "true");
+ assertEquals("http://wp.com/test/?q=pony&preview=true", url);
+ }
+
+ public void testAppendUrlParameters1() {
+ Map<String, String> params = new HashMap<>();
+ params.put("w", "200");
+ params.put("h", "300");
+ String url = UrlUtils.appendUrlParameters("http://wp.com/test", params);
+ if (!url.equals("http://wp.com/test?h=300&w=200") && !url.equals("http://wp.com/test?w=200&h=300")) {
+ assertTrue("failed test on url: " + url, false);
+ }
+ }
+
+ public void testAppendUrlParameters2() {
+ Map<String, String> params = new HashMap<>();
+ params.put("h", "300");
+ params.put("w", "200");
+ String url = UrlUtils.appendUrlParameters("/relative/test", params);
+ if (!url.equals("/relative/test?h=300&w=200") && !url.equals("/relative/test?w=200&h=300")) {
+ assertTrue("failed test on url: " + url, false);
+ }
+ }
+
+ public void testHttps1() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php")));
+ }
+
+ public void testHttps2() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com#.b.com/test")));
+ }
+
+ public void testHttps3() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php")));
+ }
+
+ public void testHttps4() {
+ assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com")));
+ }
+
+ public void testHttps5() {
+ assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com/test#test")));
+ }
+
+ private URL buildURL(String address) {
+ URL url = null;
+ try {
+ url = new URL(address);
+ } catch (MalformedURLException e) {}
+ return url;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/AndroidManifest.xml b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44b1dcddc
--- /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">
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+</manifest>
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java
new file mode 100644
index 000000000..396e06c37
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java
@@ -0,0 +1,16 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.inputmethod.InputMethodManager;
+
+public class ActivityUtils {
+ public static void hideKeyboard(Activity activity) {
+ if (activity != null && activity.getCurrentFocus() != null) {
+ InputMethodManager inputManager = (InputMethodManager) activity.getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ inputManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java
new file mode 100644
index 000000000..79b2dbced
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java
@@ -0,0 +1,100 @@
+/*
+ * 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 AlertUtils {
+ /**
+ * 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 message
+ */
+ 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 message
+ * @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();
+ }
+} \ No newline at end of file
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..be60e748e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
@@ -0,0 +1,272 @@
+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 and 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, SUGGESTION, MAIN, SETTINGS, PLANS, PEOPLE}
+
+ public static final String TAG = "WordPress";
+ public static final int HEADER_LINE_COUNT = 2;
+
+ private static boolean mEnableRecording = false;
+
+ private AppLog() {
+ throw new AssertionError();
+ }
+
+ /**
+ * Capture log so it can be displayed by AppLogViewerActivity
+ * @param enable A boolean flag to capture log. Default is false, pass true to enable recording
+ */
+ public static void enableRecording(boolean enable) {
+ mEnableRecording = enable;
+ }
+
+ /**
+ * Sends a VERBOSE log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void v(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.v(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.v, message);
+ }
+
+ /**
+ * Sends a DEBUG log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void d(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.d(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.d, message);
+ }
+
+ /**
+ * Sends a INFO log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void i(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.i(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.i, message);
+ }
+
+ /**
+ * Sends a WARN log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void w(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.w(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.w, message);
+ }
+
+ /**
+ * Sends a ERROR log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void e(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.e(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.e, message);
+ }
+
+ /**
+ * Send a ERROR log message and log the exception.
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static void e(T tag, String message, Throwable tr) {
+ message = StringUtils.notNullStr(message);
+ Log.e(TAG + "-" + tag.toString(), message, tr);
+ addEntry(tag, LogLevel.e, message + " - exception: " + tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getStringStackTrace(tr));
+ }
+
+ /**
+ * Sends a ERROR log message and the exception with StackTrace
+ * @param tag Used to identify the source of a log message. It usually identifies the class or activity where the log call occurs.
+ * @param tr An exception to log to get StackTrace
+ */
+ 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: " + getStringStackTrace(tr));
+ }
+
+ /**
+ * Sends a ERROR log message
+ * @param tag Used to identify the source of a log message. It usually identifies the class or activity where the log call occurs.
+ * @param volleyErrorMsg
+ * @param statusCode
+ */
+ 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 mLogLevel;
+ String mLogText;
+ T mLogTag;
+
+ public LogEntry(LogLevel logLevel, String logText, T logTag) {
+ mLogLevel = logLevel;
+ mLogText = logText;
+ if (mLogText == null) {
+ mLogText = "null";
+ }
+ mLogTag = logTag;
+ }
+
+ private String toHtml() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<font color=\"");
+ sb.append(mLogLevel.toHtmlColor());
+ sb.append("\">");
+ sb.append("[");
+ sb.append(mLogTag.name());
+ sb.append("] ");
+ sb.append(mLogLevel.name());
+ sb.append(": ");
+ sb.append(TextUtils.htmlEncode(mLogText).replace("\n", "<br />"));
+ sb.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(level, text, tag);
+ mLogEntries.addEntry(entry);
+ }
+
+ private static String getStringStackTrace(Throwable throwable) {
+ StringWriter errors = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(errors));
+ return errors.toString();
+ }
+
+ /**
+ * Returns entire log as html for display (see AppLogViewerActivity)
+ * @param context
+ * @return Arraylist of Strings containing log messages
+ */
+ public static ArrayList<String> toHtmlList(Context context) {
+ ArrayList<String> items = new ArrayList<String>();
+
+ // add version & device info - be sure to change HEADER_LINE_COUNT if additional lines are added
+ items.add("<strong>WordPress Android version: " + PackageUtils.getVersionName(context) + "</strong>");
+ items.add("<strong>Android device name: " + DeviceUtils.getInstance().getDeviceName(context) + "</strong>");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ while (it.hasNext()) {
+ items.add(it.next().toHtml());
+ }
+ return items;
+ }
+
+ /**
+ * Converts the entire log to plain text
+ * @param context
+ * @return The log as plain text
+ */
+ public static String toPlainText(Context context) {
+ StringBuilder sb = new StringBuilder();
+
+ // add version & device info
+ sb.append("WordPress Android version: " + PackageUtils.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().mLogText)
+ .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..c81ec64a5
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.util;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+public class BlogUtils {
+ public static Comparator<Object> BlogNameComparator = new Comparator<Object>() {
+ public int compare(Object blog1, Object blog2) {
+ Map<String, Object> blogMap1 = (Map<String, Object>) blog1;
+ Map<String, Object> blogMap2 = (Map<String, Object>) blog2;
+ String blogName1 = getBlogNameOrHomeURLFromAccountMap(blogMap1);
+ String blogName2 = getBlogNameOrHomeURLFromAccountMap(blogMap2);
+ return blogName1.compareToIgnoreCase(blogName2);
+ }
+ };
+
+ /**
+ * Return a blog name or blog home URL if trimmed name is an empty string
+ */
+ public static String getBlogNameOrHomeURLFromAccountMap(Map<String, Object> account) {
+ String blogName = getBlogNameFromAccountMap(account);
+ if (blogName.trim().length() == 0) {
+ blogName = BlogUtils.getHomeURLOrHostNameFromAccountMap(account);
+ }
+ return blogName;
+ }
+
+ /**
+ * Return a blog name or blog url (host part only) if trimmed name is an empty string
+ */
+ public static String getBlogNameFromAccountMap(Map<String, Object> account) {
+ return StringUtils.unescapeHTML(MapUtils.getMapStr(account, "blogName"));
+ }
+
+ /**
+ * Return the blog home URL setting or the host name if home URL is an empty string.
+ */
+ public static String getHomeURLOrHostNameFromAccountMap(Map<String, Object> account) {
+ String homeURL = UrlUtils.removeScheme(MapUtils.getMapStr(account, "homeURL"));
+ homeURL = StringUtils.removeTrailingSlash(homeURL);
+
+ if (homeURL.length() == 0) {
+ return UrlUtils.getHost(MapUtils.getMapStr(account, "url"));
+ }
+
+ return homeURL;
+ }
+
+ public static String[] getBlogNamesFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> blogNames = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ blogNames.add(getBlogNameOrHomeURLFromAccountMap(account));
+ }
+ return blogNames.toArray(new String[blogNames.size()]);
+ }
+
+ public static String[] getHomeURLOrHostNamesFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> urls = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ urls.add(getHomeURLOrHostNameFromAccountMap(account));
+ }
+ return urls.toArray(new String[urls.size()]);
+ }
+
+ public static String[] getBlogIdsFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> ids = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ ids.add(MapUtils.getMapStr(account, "blogId"));
+ }
+ return ids.toArray(new String[ids.size()]);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java
new file mode 100644
index 000000000..2a796f3ee
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java
@@ -0,0 +1,246 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class DateTimeUtils {
+ private DateTimeUtils() {
+ throw new AssertionError();
+ }
+
+ // See http://drdobbs.com/java/184405382
+ private static final ThreadLocal<DateFormat> ISO8601_FORMAT = new ThreadLocal<DateFormat>() {
+ @Override
+ protected DateFormat initialValue() {
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
+ }
+ };
+
+ /**
+ * Converts a date to a relative time span ("8h", "3d", etc.) - similar to
+ * DateUtils.getRelativeTimeSpanString but returns shorter result
+ */
+ public static String javaDateToTimeSpan(final Date date, Context context) {
+ if (date == null) {
+ return "";
+ }
+
+ long passedTime = date.getTime();
+ long currentTime = System.currentTimeMillis();
+
+ // return "now" if less than a minute has elapsed
+ long secondsSince = (currentTime - passedTime) / 1000;
+ if (secondsSince < 60) {
+ return context.getString(R.string.timespan_now);
+ }
+
+ // less than an hour (ex: 12m)
+ long minutesSince = secondsSince / 60;
+ if (minutesSince < 60) {
+ return Long.toString(minutesSince) + "m";
+ }
+
+ // less than a day (ex: 17h)
+ long hoursSince = minutesSince / 60;
+ if (hoursSince < 24) {
+ return Long.toString(hoursSince) + "h";
+ }
+
+ // less than a week (ex: 5d)
+ long daysSince = hoursSince / 24;
+ if (daysSince < 7) {
+ return Long.toString(daysSince) + "d";
+ }
+
+ // less than a year old, so return day/month without year (ex: Jan 30)
+ if (daysSince < 365) {
+ return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_NO_YEAR |
+ DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ // date is older, so include year (ex: Jan 30, 2013)
+ return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns a {@link Date}.
+ */
+ public static Date dateFromIso8601(final String strDate) {
+ try {
+ DateFormat formatter = ISO8601_FORMAT.get();
+ return formatter.parse(strDate);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns a {@link Date} in UTC.
+ */
+ public static Date dateUTCFromIso8601(String iso8601date) {
+ try {
+ iso8601date = iso8601date.replace("Z", "+0000").replace("+00:00", "+0000");
+ DateFormat formatter = ISO8601_FORMAT.get();
+ formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return formatter.parse(iso8601date);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Given a {@link Date}, returns an ISO 8601-formatted String.
+ */
+ public static String iso8601FromDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ DateFormat formatter = ISO8601_FORMAT.get();
+ return formatter.format(date);
+ }
+
+ /**
+ * Given a {@link Date}, returns an ISO 8601-formatted String in UTC.
+ */
+ public static String iso8601UTCFromDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ TimeZone tz = TimeZone.getTimeZone("UTC");
+ DateFormat formatter = ISO8601_FORMAT.get();
+ formatter.setTimeZone(tz);
+
+ String iso8601date = formatter.format(date);
+
+ // Use "+00:00" notation rather than "+0000" to be consistent with the WP.COM API
+ return iso8601date.replace("+0000", "+00:00");
+ }
+
+ /**
+ * Returns the current UTC date
+ */
+ public static Date nowUTC() {
+ Date dateTimeNow = new Date();
+ return localDateToUTC(dateTimeNow);
+ }
+
+ public static Date localDateToUTC(Date dtLocal) {
+ if (dtLocal == null) {
+ return null;
+ }
+ TimeZone tz = TimeZone.getDefault();
+ int currentOffsetFromUTC = tz.getRawOffset() + (tz.inDaylightTime(dtLocal) ? tz.getDSTSavings() : 0);
+ return new Date(dtLocal.getTime() - currentOffsetFromUTC);
+ }
+
+ // Routines to return a diff between two dates - always return a positive number
+
+ public static int daysBetween(Date dt1, Date dt2) {
+ long hrDiff = hoursBetween(dt1, dt2);
+ if (hrDiff == 0) {
+ return 0;
+ }
+ return (int) (hrDiff / 24);
+ }
+
+ public static int hoursBetween(Date dt1, Date dt2) {
+ long minDiff = minutesBetween(dt1, dt2);
+ if (minDiff == 0) {
+ return 0;
+ }
+ return (int) (minDiff / 60);
+ }
+
+ public static int minutesBetween(Date dt1, Date dt2) {
+ long msDiff = millisecondsBetween(dt1, dt2);
+ if (msDiff == 0) {
+ return 0;
+ }
+ return (int) (msDiff / 60000);
+ }
+
+ public static int secondsBetween(Date dt1, Date dt2) {
+ long msDiff = millisecondsBetween(dt1, dt2);
+ if (msDiff == 0) {
+ return 0;
+ }
+ return (int) (msDiff / 1000);
+ }
+
+ public static long millisecondsBetween(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return 0;
+ }
+ return Math.abs(dt1.getTime() - dt2.getTime());
+ }
+
+ public static boolean isSameYear(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return false;
+ }
+ return dt1.getYear() == dt2.getYear();
+ }
+
+ public static boolean isSameMonthAndYear(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return false;
+ }
+ return dt1.getYear() == dt2.getYear() && dt1.getMonth() == dt2.getMonth();
+ }
+
+ // Routines involving Unix timestamps (GMT assumed)
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns the corresponding UNIX timestamp.
+ */
+ public static long timestampFromIso8601(final String strDate) {
+ return (timestampFromIso8601Millis(strDate) / 1000);
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns the corresponding timestamp in milliseconds.
+ */
+ public static long timestampFromIso8601Millis(final String strDate) {
+ Date date = dateFromIso8601(strDate);
+ if (date == null) {
+ return 0;
+ }
+ return (date.getTime());
+ }
+
+ /**
+ * Given a UNIX timestamp, returns the corresponding {@link Date}.
+ */
+ public static Date dateFromTimestamp(long timestamp) {
+ return new java.util.Date(timestamp * 1000);
+ }
+
+ /**
+ * Given a UNIX timestamp, returns an ISO 8601-formatted date as a String.
+ */
+ public static String iso8601FromTimestamp(long timestamp) {
+ return iso8601FromDate(dateFromTimestamp(timestamp));
+ }
+
+ /**
+ * Given a UNIX timestamp, returns an ISO 8601-formatted date in UTC as a String.
+ */
+ public static String iso8601UTCFromTimestamp(long timestamp) {
+ return iso8601UTCFromDate(dateFromTimestamp(timestamp));
+ }
+
+ /**
+ * Given a UNIX timestamp, returns a relative time span ("8h", "3d", etc.).
+ */
+ public static String timeSpanFromTimestamp(long timestamp, Context context) {
+ Date dateGMT = dateFromTimestamp(timestamp);
+ return javaDateToTimeSpan(dateGMT, context);
+ }
+}
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..40a017e9a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
@@ -0,0 +1,91 @@
+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 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 float spToPx(Context context, float sp){
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ final float scale = displayMetrics.scaledDensity;
+ return sp * scale;
+ }
+
+ 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 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..66a0c77fd
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
@@ -0,0 +1,75 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * EditText utils
+ */
+public class EditTextUtils {
+ private EditTextUtils() {
+ throw new AssertionError();
+ }
+
+ /**
+ * returns non-null text string from passed TextView
+ */
+ public static String getText(TextView textView) {
+ return (textView != null) ? textView.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/EmoticonsUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java
new file mode 100644
index 000000000..45661d980
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.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 EmoticonsUtils {
+ 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 = EmoticonsUtils.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;
+ }
+ }
+}
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..372473e15
--- /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, LanguageUtils.getCurrentDeviceLanguage(context));
+ } 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..1fbfb3e56
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+/**
+ * see https://en.gravatar.com/site/implement/images/
+ */
+public class GravatarUtils {
+
+ // by default tell gravatar to respond to non-existent images with a 404 - this means
+ // it's up to the caller to catch the 404 and provide a suitable default image
+ private static final DefaultImage DEFAULT_GRAVATAR = DefaultImage.STATUS_404;
+
+ public static enum DefaultImage {
+ MYSTERY_MAN,
+ STATUS_404,
+ IDENTICON,
+ MONSTER,
+ WAVATAR,
+ RETRO,
+ BLANK;
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case MYSTERY_MAN:
+ return "mm";
+ case STATUS_404:
+ return "404";
+ case IDENTICON:
+ return "identicon";
+ case MONSTER:
+ return "monsterid";
+ case WAVATAR:
+ return "wavatar";
+ case RETRO:
+ return "retro";
+ default:
+ return "blank";
+ }
+ }
+ }
+
+ /*
+ * 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 fixGravatarUrl(final String imageUrl, int avatarSz) {
+ return fixGravatarUrl(imageUrl, avatarSz, DEFAULT_GRAVATAR);
+ }
+ public static String fixGravatarUrl(final String imageUrl, int avatarSz, DefaultImage defaultImage) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // if this isn't a gravatar image, return as resized photon image url
+ if (!imageUrl.contains("gravatar.com")) {
+ return PhotonUtils.getPhotonImageUrl(imageUrl, avatarSz, avatarSz);
+ }
+
+ // remove all other params, then add query string for size and default image
+ return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=" + defaultImage.toString();
+ }
+
+ public static String gravatarFromEmail(final String email, int size) {
+ return gravatarFromEmail(email, size, DEFAULT_GRAVATAR);
+ }
+ public static String gravatarFromEmail(final String email, int size, DefaultImage defaultImage) {
+ return "http://gravatar.com/avatar/"
+ + StringUtils.getMd5Hash(StringUtils.notNullStr(email))
+ + "?d=" + defaultImage.toString()
+ + "&size=" + Integer.toString(size);
+ }
+
+ public static String blavatarFromUrl(final String url, int size) {
+ return blavatarFromUrl(url, size, DEFAULT_GRAVATAR);
+ }
+ public static String blavatarFromUrl(final String url, int size, DefaultImage defaultImage) {
+ return "http://gravatar.com/blavatar/"
+ + StringUtils.getMd5Hash(UrlUtils.getHost(url))
+ + "?d=" + defaultImage.toString()
+ + "&size=" + Integer.toString(size);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java
new file mode 100644
index 000000000..9773d45d7
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.util;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+public class HTTPUtils {
+ public static final int REQUEST_TIMEOUT_MS = 30000;
+
+ /**
+ * Builds an HttpURLConnection from a URL and header map. Will force HTTPS usage if given an Authorization header.
+ * @throws IOException
+ */
+ public static HttpURLConnection setupUrlConnection(String url, Map<String, String> headers) throws IOException {
+ // Force HTTPS usage if an authorization header was specified
+ if (headers.keySet().contains("Authorization")) {
+ url = UrlUtils.makeHttps(url);
+ }
+
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setReadTimeout(REQUEST_TIMEOUT_MS);
+ conn.setConnectTimeout(REQUEST_TIMEOUT_MS);
+
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ conn.setRequestProperty(entry.getKey(), entry.getValue());
+ }
+
+ return conn;
+ }
+}
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..b5319372a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
@@ -0,0 +1,156 @@
+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;
+import org.wordpress.android.util.helpers.WPHtmlTagHandler;
+import org.wordpress.android.util.helpers.WPImageGetter;
+import org.wordpress.android.util.helpers.WPQuoteSpan;
+
+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
+ * @param text String containing html
+ * @return String without HTML
+ */
+ 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
+ * @param str String containing only valid html
+ * @return String without 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
+ * @param text String to be decoded to Unicode
+ * @return String containing unicode characters
+ */
+ 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
+ * @param context Android Context
+ * @param resId Android R.color.xxx
+ * @return A String HTML hex color code
+ */
+ 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 {@code <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 {@code <!--//-->} followed by a CDATA section followed by {@code <!]]>,}
+ * all of which will show up if we don't strip it here.
+ * @see <a href="http://wordpress.org/plugins/sociable/">Wordpress Sociable Plugin</a>
+ * @return String without {@code <script>..</script>}, {@code <!--//-->} blocks followed by a CDATA section followed by {@code <!]]>,}
+ * @param text String containing script tags
+ */
+ 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 {@code <ul>}, {@code <ol>}, {@code <blockquote>}
+ * tags and replacing EmoticonsUtils with Emojis
+ * @param source
+ * @param wpImageGetter
+ */
+ 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);
+ }
+ EmoticonsUtils.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..2fd4449b8
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
@@ -0,0 +1,649 @@
+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.Point;
+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.util.Log;
+import android.webkit.MimeTypeMap;
+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.FileOutputStream;
+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 = null;
+ try {
+ cur = context.getContentResolver().query(uri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ path = cur.getString(dataColumn);
+ }
+ } catch (IllegalStateException stateException) {
+ Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + uri);
+ } finally {
+ SqlUtils.closeCursor(cur);
+ }
+ }
+
+ 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);
+ }
+
+ /**
+ * Given the path to an image, resize the image down to within a maximum width
+ * @param path the path to the original image
+ * @param maxWidth the maximum allowed width
+ * @return the path to the resized image
+ */
+ public static String createResizedImageWithMaxWidth(Context context, String path, int maxWidth) {
+ File file = new File(path);
+ if (!file.exists()) {
+ return path;
+ }
+
+ String mimeType = MediaUtils.getMediaFileMimeType(file);
+ if (mimeType.equals("image/gif")) {
+ // Don't rescale gifs to maintain their quality
+ return path;
+ }
+
+ String fileName = MediaUtils.getMediaFileName(file, mimeType);
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileName).toLowerCase();
+
+ int[] dimensions = getImageSize(Uri.fromFile(file), context);
+ int orientation = getImageOrientation(context, path);
+
+ if (dimensions[0] <= maxWidth) {
+ // Image width is within limits; don't resize
+ return path;
+ }
+
+ // Create resized image
+ byte[] bytes = ImageUtils.createThumbnailFromUri(context, Uri.parse(path), maxWidth, fileExtension, orientation);
+
+ if (bytes != null) {
+ try {
+ File resizedImageFile = File.createTempFile("wp-image-", fileExtension);
+ FileOutputStream out = new FileOutputStream(resizedImageFile);
+ out.write(bytes);
+ out.close();
+
+ String tempFilePath = resizedImageFile.getPath();
+
+ if (!TextUtils.isEmpty(tempFilePath)) {
+ return tempFilePath;
+ } else {
+ AppLog.e(AppLog.T.POSTS, "Failed to create resized image");
+ }
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, "Failed to create image temp file");
+ }
+ }
+
+ return path;
+ }
+
+ /**
+ * 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 = null;
+ try {
+ cur = context.getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ filePath = cur.getString(dataColumn);
+ }
+ } catch (IllegalStateException stateException) {
+ Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + imageUri);
+ } finally {
+ SqlUtils.closeCursor(cur);
+ }
+ }
+
+ 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
+ final 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;
+ } catch (NullPointerException e) {
+ // See: https://github.com/wordpress-mobile/WordPress-Android/issues/1844
+ AppLog.e(AppLog.T.UTILS, "Bitmap.createBitmap has thrown a NPE internally. This should never happen: " + e);
+ return null;
+ }
+
+ if (bmpRotated == null) {
+ // Fix an issue where bmpRotated is null even if the documentation doesn't say Bitmap.createBitmap can return null.
+ // See: https://github.com/wordpress-mobile/WordPress-Android/issues/1848
+ 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);
+
+ return output;
+ }
+
+ /**
+ * Returns the passed bitmap with rounded corners
+ * @param bitmap - the bitmap to modify
+ * @param radius - the radius of the corners
+ * @param borderColor - the border to apply (use Color.TRANSPARENT for none)
+ */
+ public static Bitmap getRoundedEdgeBitmap(final Bitmap bitmap, int radius, int borderColor) {
+ 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);
+
+ if (borderColor != Color.TRANSPARENT) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1f);
+ paint.setColor(borderColor);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+ }
+
+ return output;
+ }
+
+ /**
+ * Get the maximum size a thumbnail can be to fit in either portrait or landscape orientations.
+ */
+ public static int getMaximumThumbnailWidthForEditor(Context context) {
+ int maximumThumbnailWidthForEditor;
+ Point size = DisplayUtils.getDisplayPixelSize(context);
+ int screenWidth = size.x;
+ int screenHeight = size.y;
+ maximumThumbnailWidthForEditor = (screenWidth > screenHeight) ? screenHeight : screenWidth;
+ // 48dp of padding on each side so you can still place the cursor next to the image.
+ int padding = DisplayUtils.dpToPx(context, 48) * 2;
+ maximumThumbnailWidthForEditor -= padding;
+ return maximumThumbnailWidthForEditor;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java
new file mode 100644
index 000000000..196a7b1f3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java
@@ -0,0 +1,251 @@
+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 JSONUtils {
+ 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 = "JSONUtils";
+
+ /**
+ * 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) {
+ if (source == null) {
+ AppLog.e(T.UTILS, "Parameter source is null, can't query a null object");
+ return defaultObject;
+ }
+ if (query == null) {
+ AppLog.e(T.UTILS, "Parameter query is null");
+ return 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 {
+ 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;
+ }
+ }
+ 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 (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) {
+ 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) {
+ if (source == null) {
+ AppLog.e(T.UTILS, "Parameter source is null, can't query a null object");
+ return defaultObject;
+ }
+ if (query == null) {
+ AppLog.e(T.UTILS, "Parameter query is null");
+ return 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) {
+ 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;
+ if (value.equalsIgnoreCase("no"))
+ 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;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java
new file mode 100644
index 000000000..515b044b3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+import java.util.Locale;
+
+/**
+ * Methods for dealing with i18n messages
+ */
+public class LanguageUtils {
+
+ public static Locale getCurrentDeviceLanguage(Context context) {
+ //better use getConfiguration as it has the latest locale configuration change.
+ //Otherwise Locale.getDefault().getLanguage() gets
+ //the config upon application launch.
+ Locale deviceLocale = context != null ? context.getResources().getConfiguration().locale : Locale.getDefault();
+ return deviceLocale;
+ }
+
+ public static String getCurrentDeviceLanguageCode(Context context) {
+ String deviceLanguageCode = getCurrentDeviceLanguage(context).toString();
+ return deviceLanguageCode;
+ }
+
+ public static String getPatchedCurrentDeviceLanguage(Context context) {
+ return patchDeviceLanguageCode(getCurrentDeviceLanguageCode(context));
+ }
+
+ /**
+ * Patches a deviceLanguageCode if any of deprecated values iw, id, or yi
+ */
+ public static String patchDeviceLanguageCode(String deviceLanguageCode){
+ String patchedCode = deviceLanguageCode;
+ /*
+ <p>Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language
+ * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This
+ * rewriting happens even if you construct your own {@code Locale} object, not just for
+ * instances returned by the various lookup methods.
+ */
+ if (deviceLanguageCode != null) {
+ if (deviceLanguageCode.startsWith("iw"))
+ patchedCode = deviceLanguageCode.replace("iw", "he");
+ else if (deviceLanguageCode.startsWith("in"))
+ patchedCode = deviceLanguageCode.replace("in", "id");
+ else if (deviceLanguageCode.startsWith("ji"))
+ patchedCode = deviceLanguageCode.replace("ji", "yi");
+ }
+
+ return patchedCode;
+ }
+
+}
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..c6e72dc5b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
@@ -0,0 +1,107 @@
+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;
+ }
+ }
+
+ /*
+ * float version of above
+ */
+ public static float getMapFloat(final Map<?, ?> map, final String key) {
+ return getMapFloat(map, key, 0);
+ }
+ public static float getMapFloat(final Map<?, ?> map, final String key, float defaultValue) {
+ try {
+ return Float.parseFloat(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * double version of above
+ */
+ public static double getMapDouble(final Map<?, ?> map, final String key) {
+ return getMapDouble(map, key, 0);
+ }
+ public static double getMapDouble(final Map<?, ?> map, final String key, double defaultValue) {
+ try {
+ return Double.parseDouble(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/MediaUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java
new file mode 100644
index 000000000..a96dadc74
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java
@@ -0,0 +1,334 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class MediaUtils {
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ public static boolean isValidImage(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".gif");
+ }
+
+ public static boolean isDocument(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".odt") || url.endsWith(".pdf");
+ }
+
+ public static boolean isPowerpoint(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".ppt") || url.endsWith(".pptx") || url.endsWith(".pps") || url.endsWith(".ppsx") ||
+ url.endsWith(".key");
+ }
+
+ public static boolean isSpreadsheet(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".xls") || url.endsWith(".xlsx");
+ }
+
+ public static boolean isVideo(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".ogv") || url.endsWith(".mp4") || url.endsWith(".m4v") || url.endsWith(".mov") ||
+ url.endsWith(".wmv") || url.endsWith(".avi") || url.endsWith(".mpg") || url.endsWith(".3gp") ||
+ url.endsWith(".3g2") || url.contains("video");
+ }
+
+ public static boolean isAudio(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".wav") || url.endsWith(".wma") ||
+ url.endsWith(".aiff") || url.endsWith(".aif") || url.endsWith(".aac") || url.endsWith(".m4a");
+ }
+
+ /**
+ * E.g. Jul 2, 2013 @ 21:57
+ */
+ public static String getDate(long ms) {
+ Date date = new Date(ms);
+ SimpleDateFormat sdf = new SimpleDateFormat("MMM d, yyyy '@' HH:mm", Locale.ENGLISH);
+
+ // The timezone on the website is at GMT
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+ return sdf.format(date);
+ }
+
+ public static boolean isLocalFile(String state) {
+ if (state == null) {
+ return false;
+ }
+
+ return (state.equals("queued") || state.equals("uploading") || state.equals("retry")
+ || state.equals("failed"));
+ }
+
+ public static Uri getLastRecordedVideoUri(Activity activity) {
+ String[] proj = { MediaStore.Video.Media._ID };
+ Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ String sortOrder = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC";
+ CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, sortOrder);
+ Cursor cursor = loader.loadInBackground();
+ cursor.moveToFirst();
+
+ return Uri.parse(contentUri.toString() + "/" + cursor.getLong(0));
+ }
+
+ /**
+ * Get image width setting from the image width site setting string. This string can be an int, in this case it's
+ * the maximum image width defined by the site.
+ * Examples:
+ * "1000" will return 1000
+ * "Original Size" will return Integer.MAX_VALUE
+ * "Largeur originale" will return Integer.MAX_VALUE
+ * null will return Integer.MAX_VALUE
+ * @param imageWidthSiteSettingString Image width site setting string
+ * @return Integer.MAX_VALUE if image width is not defined or invalid, maximum image width in other cases.
+ */
+ public static int getImageWidthSettingFromString(String imageWidthSiteSettingString) {
+ if (imageWidthSiteSettingString == null) {
+ return Integer.MAX_VALUE;
+ }
+ try {
+ return Integer.valueOf(imageWidthSiteSettingString);
+ } catch (NumberFormatException e) {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Calculate and return the maximum allowed image width by comparing the width of the image at its full size with
+ * the maximum upload width set in the blog settings
+ * @param imageWidth the image's natural (full) width
+ * @param imageWidthSiteSettingString the maximum upload width set in the site settings
+ * @return maximum allowed image width
+ */
+ public static int getMaximumImageWidth(int imageWidth, String imageWidthSiteSettingString) {
+ int imageWidthBlogSetting = getImageWidthSettingFromString(imageWidthSiteSettingString);
+ int imageWidthPictureSetting = imageWidth == 0 ? Integer.MAX_VALUE : imageWidth;
+
+ if (Math.min(imageWidthPictureSetting, imageWidthBlogSetting) == Integer.MAX_VALUE) {
+ // Default value in case of errors reading the picture size or the blog settings is set to Original size
+ return DEFAULT_MAX_IMAGE_WIDTH;
+ } else {
+ return Math.min(imageWidthPictureSetting, imageWidthBlogSetting);
+ }
+ }
+
+ public static int getMaximumImageWidth(Context context, Uri curStream, String imageWidthBlogSettingString) {
+ int[] dimensions = ImageUtils.getImageSize(curStream, context);
+ return getMaximumImageWidth(dimensions[0], imageWidthBlogSettingString);
+ }
+
+ public static boolean isInMediaStore(Uri mediaUri) {
+ // Check if the image is externally hosted (Picasa/Google Photos for example)
+ if (mediaUri != null && mediaUri.toString().startsWith("content://media/")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static Uri downloadExternalMedia(Context context, Uri imageUri) {
+ if (context == null || imageUri == null) {
+ return null;
+ }
+ File cacheDir = null;
+
+ String mimeType = context.getContentResolver().getType(imageUri);
+ boolean isVideo = (mimeType != null && mimeType.contains("video"));
+
+ // If the device has an SD card
+ if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
+ String mediaFolder = isVideo ? "video" : "images";
+ cacheDir = new File(android.os.Environment.getExternalStorageDirectory() + "/WordPress/" + mediaFolder);
+ } else {
+ if (context.getApplicationContext() != null) {
+ cacheDir = context.getApplicationContext().getCacheDir();
+ }
+ }
+
+ if (cacheDir != null && !cacheDir.exists()) {
+ cacheDir.mkdirs();
+ }
+ try {
+ InputStream input;
+ // Download the file
+ if (imageUri.toString().startsWith("content://")) {
+ input = context.getContentResolver().openInputStream(imageUri);
+ if (input == null) {
+ AppLog.e(T.UTILS, "openInputStream returned null");
+ return null;
+ }
+ } else {
+ input = new URL(imageUri.toString()).openStream();
+ }
+
+ String fileName = "wp-" + System.currentTimeMillis();
+ if (isVideo) {
+ fileName += "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ }
+
+ File f = new File(cacheDir, fileName);
+
+ OutputStream output = new FileOutputStream(f);
+
+ byte data[] = new byte[1024];
+ int count;
+ while ((count = input.read(data)) != -1) {
+ output.write(data, 0, count);
+ }
+
+ output.flush();
+ output.close();
+ input.close();
+
+ return Uri.fromFile(f);
+ } catch (FileNotFoundException e) {
+ AppLog.e(T.UTILS, e);
+ } catch (MalformedURLException e) {
+ AppLog.e(T.UTILS, e);
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e);
+ }
+
+ return null;
+ }
+
+ public static String getMimeTypeOfInputStream(InputStream stream) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(stream, null, options);
+ return options.outMimeType;
+ }
+
+ public static String getMediaFileMimeType(File mediaFile) {
+ String originalFileName = mediaFile.getName().toLowerCase();
+ String mimeType = UrlUtils.getUrlMimeType(originalFileName);
+
+ if (TextUtils.isEmpty(mimeType)) {
+ try {
+ String filePathForGuessingMime;
+ if (mediaFile.getPath().contains("://")) {
+ filePathForGuessingMime = Uri.encode(mediaFile.getPath(), ":/");
+ } else {
+ filePathForGuessingMime = "file://"+ Uri.encode(mediaFile.getPath(), "/");
+ }
+ URL urlForGuessingMime = new URL(filePathForGuessingMime);
+ URLConnection uc = urlForGuessingMime.openConnection();
+ String guessedContentType = uc.getContentType(); //internally calls guessContentTypeFromName(url.getFile()); and guessContentTypeFromStream(is);
+ // check if returned "content/unknown"
+ if (!TextUtils.isEmpty(guessedContentType) && !guessedContentType.equals("content/unknown")) {
+ mimeType = guessedContentType;
+ }
+ } catch (MalformedURLException e) {
+ AppLog.e(AppLog.T.API, "MalformedURLException while trying to guess the content type for the file here " + mediaFile.getPath() + " with URLConnection", e);
+ }
+ catch (IOException e) {
+ AppLog.e(AppLog.T.API, "Error while trying to guess the content type for the file here " + mediaFile.getPath() +" with URLConnection", e);
+ }
+ }
+
+ // No mimeType yet? Try to decode the image and get the mimeType from there
+ if (TextUtils.isEmpty(mimeType)) {
+ try {
+ DataInputStream inputStream = new DataInputStream(new FileInputStream(mediaFile));
+ String mimeTypeFromStream = getMimeTypeOfInputStream(inputStream);
+ if (!TextUtils.isEmpty(mimeTypeFromStream)) {
+ mimeType = mimeTypeFromStream;
+ }
+ inputStream.close();
+ } catch (FileNotFoundException e) {
+ AppLog.e(AppLog.T.API, "FileNotFoundException while trying to guess the content type for the file " + mediaFile.getPath(), e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.API, "IOException while trying to guess the content type for the file " + mediaFile.getPath(), e);
+ }
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = "";
+ } else {
+ if (mimeType.equalsIgnoreCase("video/mp4v-es")) { //Fixes #533. See: http://tools.ietf.org/html/rfc3016
+ mimeType = "video/mp4";
+ }
+ }
+
+ return mimeType;
+ }
+
+ public static String getMediaFileName(File mediaFile, String mimeType) {
+ String originalFileName = mediaFile.getName().toLowerCase();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(originalFileName);
+ if (!TextUtils.isEmpty(extension)) //File name already has the extension in it
+ return originalFileName;
+
+ if (!TextUtils.isEmpty(mimeType)) { //try to get the extension from mimeType
+ String fileExtension = getExtensionForMimeType(mimeType);
+ if (!TextUtils.isEmpty(fileExtension)) {
+ originalFileName += "." + fileExtension;
+ }
+ } else {
+ //No mimetype and no extension!!
+ AppLog.e(AppLog.T.API, "No mimetype and no extension for " + mediaFile.getPath());
+ }
+
+ return originalFileName;
+ }
+
+ public static String getExtensionForMimeType(String mimeType) {
+ if (TextUtils.isEmpty(mimeType))
+ return "";
+
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String fileExtensionFromMimeType = mimeTypeMap.getExtensionFromMimeType(mimeType);
+ if (TextUtils.isEmpty(fileExtensionFromMimeType)) {
+ // We're still without an extension - split the mime type and retrieve it
+ String[] split = mimeType.split("/");
+ fileExtensionFromMimeType = split.length > 1 ? split[1] : split[0];
+ }
+
+ return fileExtensionFromMimeType.toLowerCase();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java
new file mode 100644
index 000000000..240c93f50
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java
@@ -0,0 +1,89 @@
+package org.wordpress.android.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+
+/**
+ * requires android.permission.ACCESS_NETWORK_STATE
+ */
+
+public class NetworkUtils {
+ public static final int TYPE_UNKNOWN = -1;
+
+ /**
+ * returns information on the active network connection
+ */
+ private static NetworkInfo getActiveNetworkInfo(Context context) {
+ if (context == null) {
+ return null;
+ }
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm == null) {
+ return null;
+ }
+ // note that this may return null if no network is currently active
+ return cm.getActiveNetworkInfo();
+ }
+
+ /**
+ * returns the ConnectivityManager.TYPE_xxx if there's an active connection, otherwise
+ * returns TYPE_UNKNOWN
+ */
+ private static int getActiveNetworkType(Context context) {
+ NetworkInfo info = getActiveNetworkInfo(context);
+ if (info == null || !info.isConnected()) {
+ return TYPE_UNKNOWN;
+ }
+ return info.getType();
+ }
+
+ /**
+ * returns true if a network connection is available
+ */
+ public static boolean isNetworkAvailable(Context context) {
+ NetworkInfo info = getActiveNetworkInfo(context);
+ return (info != null && info.isConnected());
+ }
+
+ /**
+ * returns true if the user is connected to WiFi
+ */
+ public static boolean isWiFiConnected(Context context) {
+ return (getActiveNetworkType(context) == ConnectivityManager.TYPE_WIFI);
+ }
+
+ /**
+ * returns true if airplane mode has been enabled
+ */
+ @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
+ @SuppressWarnings("deprecation")
+ public static boolean isAirplaneModeOn(Context context) {
+ // prior to JellyBean 4.2 this was Settings.System.AIRPLANE_MODE_ON, JellyBean 4.2
+ // moved it to Settings.Global
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return Settings.System.getInt(context.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) != 0;
+ } else {
+ return Settings.Global.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+ }
+ }
+
+ /**
+ * returns true if there's an active network connection, otherwise displays a toast error
+ * and returns false
+ */
+ public static boolean checkConnection(Context context) {
+ if (context == null) {
+ return false;
+ }
+ if (isNetworkAvailable(context)) {
+ return true;
+ }
+ ToastUtils.showToast(context, R.string.no_network_message);
+ return false;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java
new file mode 100644
index 000000000..52900a0bf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class PackageUtils {
+ /**
+ * Return true if Debug build. false otherwise.
+ */
+ public static boolean isDebugBuild() {
+ return BuildConfig.DEBUG;
+ }
+
+ public static PackageInfo getPackageInfo(Context context) {
+ try {
+ PackageManager manager = context.getPackageManager();
+ return manager.getPackageInfo(context.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Return version code, or 0 if it can't be read
+ */
+ public static int getVersionCode(Context context) {
+ PackageInfo packageInfo = getPackageInfo(context);
+ if (packageInfo != null) {
+ return packageInfo.versionCode;
+ }
+ return 0;
+ }
+
+ /**
+ * Return version name, or the string "0" if it can't be read
+ */
+ public static String getVersionName(Context context) {
+ PackageInfo packageInfo = getPackageInfo(context);
+ if (packageInfo != null) {
+ return packageInfo.versionName;
+ }
+ return "0";
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java
new file mode 100644
index 000000000..bf30103d2
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java
@@ -0,0 +1,97 @@
+package org.wordpress.android.util;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionUtils {
+ /**
+ * Check for permissions, request them if they're not granted.
+ *
+ * @return true if permissions are already granted, else request them and return false.
+ */
+ private static boolean checkAndRequestPermissions(Activity activity, int requestCode, String[] permissionList) {
+ List<String> toRequest = new ArrayList<>();
+ for (String permission : permissionList) {
+ if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
+ toRequest.add(permission);
+ }
+ }
+ if (toRequest.size() > 0) {
+ String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]);
+ ActivityCompat.requestPermissions(activity, requestedPermissions, requestCode);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check for permissions, request them if they're not granted.
+ *
+ * @return true if permissions are already granted, else request them and return false.
+ */
+ private static boolean checkAndRequestPermissions(Fragment fragment, int requestCode, String[] permissionList) {
+ List<String> toRequest = new ArrayList<>();
+ for (String permission : permissionList) {
+ Context context = fragment.getActivity();
+ if (context != null && ContextCompat.checkSelfPermission(context, permission) != PackageManager
+ .PERMISSION_GRANTED) {
+ toRequest.add(permission);
+ }
+ }
+ if (toRequest.size() > 0) {
+ String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]);
+ FragmentCompat.requestPermissions(fragment, requestedPermissions, requestCode);
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean checkAndRequestCameraAndStoragePermissions(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE,
+ permission.CAMERA
+ });
+ }
+
+ public static boolean checkAndRequestCameraAndStoragePermissions(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE,
+ permission.CAMERA
+ });
+ }
+
+ public static boolean checkAndRequestStoragePermission(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE
+ });
+ }
+
+ public static boolean checkAndRequestStoragePermission(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE
+ });
+ }
+
+ public static boolean checkLocationPermissions(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.ACCESS_FINE_LOCATION,
+ permission.ACCESS_COARSE_LOCATION
+ });
+ }
+
+ public static boolean checkLocationPermissions(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.ACCESS_FINE_LOCATION,
+ permission.ACCESS_COARSE_LOCATION
+ });
+ }
+}
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..85a2adc93
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
@@ -0,0 +1,104 @@
+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();
+ }
+
+ /*
+ * 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 - note that the passed quality parameter will only affect JPEGs
+ */
+ public enum Quality {
+ HIGH,
+ MEDIUM,
+ LOW
+ }
+ public static String getPhotonImageUrl(String imageUrl, int width, int height) {
+ return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM);
+ }
+ public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // make sure it's valid
+ int schemePos = imageUrl.indexOf("://");
+ if (schemePos == -1) {
+ return imageUrl;
+ }
+
+ // we have encountered some image urls that incorrectly have a # fragment part, which
+ // must be removed before removing the query string
+ int fragmentPos = imageUrl.indexOf("#");
+ if (fragmentPos > 0) {
+ imageUrl = imageUrl.substring(0, fragmentPos);
+ }
+
+ // remove existing query string since it may contain params that conflict with the passed ones
+ imageUrl = UrlUtils.removeQuery(imageUrl);
+
+ // if this is an "mshots" url, skip photon and return it with a query that sets the width/height
+ if (isMshotsUrl(imageUrl)) {
+ return imageUrl + "?w=" + width + "&h=" + height;
+ }
+
+ // strip=all removes EXIF and other non-visual data from JPEGs
+ String query = "?strip=all";
+
+ switch (quality) {
+ case HIGH:
+ query += "&quality=100";
+ break;
+ case LOW:
+ query += "&quality=35";
+ break;
+ default: // medium
+ query += "&quality=65";
+ break;
+ }
+
+ // if both width & height are passed use the "resize" param, use only "w" or "h" if just
+ // one of them is set
+ if (width > 0 && height > 0) {
+ query += "&resize=" + width + "," + height;
+ } else if (width > 0) {
+ query += "&w=" + width;
+ } else if (height > 0) {
+ query += "&h=" + height;
+ }
+
+ // 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;
+ }
+
+ // use wordpress.com as the host if image is on wordpress.com since it supports the same
+ // query params and, more importantly, can handle images in private blogs
+ if (imageUrl.contains("wordpress.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..4660a3500
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.util;
+
+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();
+ }
+
+ public static void stop() {
+ getInstance().reset(null);
+ }
+
+ 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) {
+ if (mLabel == null) {
+ return;
+ }
+ long now = SystemClock.elapsedRealtime();
+ mSplits.add(now);
+ mSplitLabels.add(splitLabel);
+ }
+
+ public void dumpToLog() {
+ if (mLabel == null) {
+ return;
+ }
+ 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");
+ }
+}
+
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java
new file mode 100644
index 000000000..6bcfde06b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java
@@ -0,0 +1,16 @@
+package org.wordpress.android.util;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+public class ServiceUtils {
+ public static boolean isServiceRunning(Context context, Class<?> serviceClass) {
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
+ if (serviceClass.getName().equals(service.service.getClassName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java
new file mode 100644
index 000000000..09480f156
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ShortcodeUtils {
+ public static String getVideoPressShortcodeFromId(String videoPressId) {
+ if (videoPressId == null || videoPressId.isEmpty()) {
+ return "";
+ }
+
+ return "[wpvideo " + videoPressId + "]";
+ }
+
+ public static String getVideoPressIdFromShortCode(String shortcode) {
+ String videoPressId = "";
+
+ if (shortcode != null) {
+ String videoPressShortcodeRegex = "^\\[wpvideo (.*)]$";
+
+ Pattern pattern = Pattern.compile(videoPressShortcodeRegex);
+ Matcher matcher = pattern.matcher(shortcode);
+
+ if (matcher.find()) {
+ videoPressId = matcher.group(1);
+ }
+ }
+
+ return videoPressId;
+ }
+}
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..38b4b74a9
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
@@ -0,0 +1,142 @@
+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 org.wordpress.android.util.AppLog.T;
+
+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();
+ closeCursor(cursor);
+ }
+ }
+
+ /*
+ * Android's CursorWindow has a max size of 2MB per row which can be exceeded
+ * with a very large text column, causing an IllegalStateException when the
+ * row is read - prevent this by limiting the amount of text that's stored in
+ * the text column.
+ * https://github.com/android/platform_frameworks_base/blob/b77bc869241644a662f7e615b0b00ecb5aee373d/core/res/res/values/config.xml#L1268
+ * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java/android/database/CursorWindow.java#L103
+ */
+ // Max 512K characters (a UTF-8 char is 4 bytes max, so a 512K characters string is always < 2Mb)
+ private static final int MAX_TEXT_LEN = 1024 * 1024 / 2;
+ public static String maxSQLiteText(final String text) {
+ if (text.length() <= MAX_TEXT_LEN) {
+ return text;
+ }
+ AppLog.w(T.UTILS, "sqlite > max text exceeded, storing truncated text");
+ return text.substring(0, MAX_TEXT_LEN);
+ }
+}
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..b15e8d824
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
@@ -0,0 +1,327 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.support.annotation.StringRes;
+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;
+import java.util.Locale;
+
+public class StringUtils {
+ public static String[] mergeStringArrays(String array1[], String array2[]) {
+ if (array1 == null || array1.length == 0) {
+ return array2;
+ }
+ if (array2 == null || array2.length == 0) {
+ return array1;
+ }
+ List<String> array1List = Arrays.asList(array1);
+ List<String> array2List = Arrays.asList(array2);
+ List<String> result = new ArrayList<String>(array1List);
+ List<String> tmp = new ArrayList<String>(array1List);
+ tmp.retainAll(array2List);
+ result.addAll(array2List);
+ return ((String[]) result.toArray(new String[result.size()]));
+ }
+
+ public static String convertHTMLTagsForUpload(String source) {
+ // bold
+ source = source.replace("<b>", "<strong>");
+ source = source.replace("</b>", "</strong>");
+
+ // italics
+ source = source.replace("<i>", "<em>");
+ source = source.replace("</i>", "</em>");
+
+ return source;
+ }
+
+ public static String convertHTMLTagsForDisplay(String source) {
+ // bold
+ source = source.replace("<strong>", "<b>");
+ source = source.replace("</strong>", "</b>");
+
+ // italics
+ source = source.replace("<em>", "<i>");
+ source = source.replace("</em>", "</i>");
+
+ return source;
+ }
+
+ public static String addPTags(String source) {
+ String[] asploded = source.split("\n\n");
+
+ if (asploded.length > 0) {
+ StringBuilder wrappedHTML = new StringBuilder();
+ for (int i = 0; i < asploded.length; i++) {
+ String trimmed = asploded[i].trim();
+ if (trimmed.length() > 0) {
+ trimmed = trimmed.replace("<br />", "<br>").replace("<br/>", "<br>").replace("<br>\n", "<br>")
+ .replace("\n", "<br>");
+ wrappedHTML.append("<p>");
+ wrappedHTML.append(trimmed);
+ wrappedHTML.append("</p>");
+ }
+ }
+ return wrappedHTML.toString();
+ } else {
+ return source;
+ }
+ }
+
+ public static BigInteger getMd5IntHash(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] messageDigest = md.digest(input.getBytes());
+ BigInteger number = new BigInteger(1, messageDigest);
+ return number;
+ } catch (NoSuchAlgorithmException e) {
+ AppLog.e(T.UTILS, e);
+ return null;
+ }
+ }
+
+ public static String getMd5Hash(String input) {
+ BigInteger number = getMd5IntHash(input);
+ String md5 = number.toString(16);
+ while (md5.length() < 32) {
+ md5 = "0" + md5;
+ }
+ return md5;
+ }
+
+ public static String unescapeHTML(String html) {
+ if (html != null) {
+ return Html.fromHtml(html).toString();
+ } else {
+ return "";
+ }
+ }
+
+ /*
+ * nbradbury - adapted from Html.escapeHtml(), which was added in API Level 16
+ * TODO: not thoroughly tested yet, so marked as private - not sure I like the way
+ * this replaces two spaces with "&nbsp;"
+ */
+ private static String escapeHtml(final String text) {
+ if (text == null) {
+ return "";
+ }
+
+ StringBuilder out = new StringBuilder();
+ int length = text.length();
+
+ for (int i = 0; i < length; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("&lt;");
+ } else if (c == '>') {
+ out.append("&gt;");
+ } else if (c == '&') {
+ out.append("&amp;");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("&#").append((int) c).append(";");
+ } else if (c == ' ') {
+ while (i + 1 < length && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+
+ return out.toString();
+ }
+
+ /*
+ * returns empty string if passed string is null, otherwise returns passed string
+ */
+ public static String notNullStr(String s) {
+ if (s == null) {
+ return "";
+ }
+ return s;
+ }
+
+ /**
+ * returns true if two strings are equal or two strings are null
+ */
+ public static boolean equals(String s1, String s2) {
+ if (s1 == null) {
+ return s2 == null;
+ }
+ return s1.equals(s2);
+ }
+
+ /*
+ * capitalizes the first letter in the passed string - based on Apache commons/lang3/StringUtils
+ * http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/StringUtils.java?revision=1497829&view=markup
+ */
+ public static String capitalize(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return str;
+ }
+
+ char firstChar = str.charAt(0);
+ if (Character.isTitleCase(firstChar)) {
+ return str;
+ }
+
+ return new StringBuilder(strLen).append(Character.toTitleCase(firstChar)).append(str.substring(1)).toString();
+ }
+
+ public static String removeTrailingSlash(final String str) {
+ if (TextUtils.isEmpty(str) || !str.endsWith("/")) {
+ return str;
+ }
+
+ return str.substring(0, str.length() - 1);
+ }
+
+ /*
+ * 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 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 (EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint) != null) {
+ out.append(EmoticonsUtils.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();
+ }
+
+ /**
+ * Used to convert a language code ([lc]_[rc] where lc is language code (en, fr, es, etc...)
+ * and rc is region code (zh-CN, zh-HK, zh-TW, etc...) to a displayable string with the languages
+ * name.
+ *
+ * The input string must be between 2 and 6 characters, inclusive. An empty string is returned
+ * if that is not the case.
+ *
+ * If the input string is recognized by {@link Locale} the result of this method is the given
+ *
+ * @return non-null
+ */
+ public static String getLanguageString(String languagueCode, Locale displayLocale) {
+ if (languagueCode == null || languagueCode.length() < 2 || languagueCode.length() > 6) {
+ return "";
+ }
+
+ Locale languageLocale = new Locale(languagueCode.substring(0, 2));
+ return languageLocale.getDisplayLanguage(displayLocale) + languagueCode.substring(2);
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ public static long stringToLong(String s) {
+ return stringToLong(s, 0L);
+ }
+
+ public static long stringToLong(String s, long defaultValue) {
+ if (s == null)
+ return defaultValue;
+ try {
+ return Long.valueOf(s);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Formats the string for the given quantity, using the given arguments.
+ * We need this because our translation platform doesn't support Android plurals.
+ *
+ * @param zero The desired string identifier to get when quantity is exactly 0
+ * @param one The desired string identifier to get when quantity is exactly 1
+ * @param other The desired string identifier to get when quantity is not (0 or 1)
+ * @param quantity The number used to get the correct string
+ */
+ public static String getQuantityString(Context context, @StringRes int zero, @StringRes int one,
+ @StringRes int other, int quantity) {
+ if (quantity == 0) {
+ return context.getString(zero);
+ }
+ if (quantity == 1) {
+ return context.getString(one);
+ }
+ return String.format(context.getString(other), quantity);
+ }
+}
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..e3fee7fc3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public class SystemServiceFactory {
+ private static SystemServiceFactoryAbstract sFactory;
+
+ public static Object get(Context context, String name) {
+ if (sFactory == null) {
+ sFactory = new SystemServiceFactoryDefault();
+ }
+ 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..cad48ef8e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
@@ -0,0 +1,257 @@
+package org.wordpress.android.util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.UnsupportedEncodingException;
+import java.net.IDN;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+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;
+ }
+ }
+
+ /**
+ * @param urlString url to get host from
+ * @return host of uri if available. Empty string otherwise.
+ */
+ public static String getHost(final String urlString) {
+ if (urlString != null) {
+ Uri uri = Uri.parse(urlString);
+ if (uri.getHost() != null) {
+ return uri.getHost();
+ }
+ }
+ return "";
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Remove leading double slash, and inherit protocol scheme
+ */
+ public static String removeLeadingDoubleSlash(String url, String scheme) {
+ if (url != null && url.startsWith("//")) {
+ url = url.substring(2);
+ if (scheme != null) {
+ if (scheme.endsWith("://")){
+ url = scheme + url;
+ } else {
+ AppLog.e(T.UTILS, "Invalid scheme used: " + scheme);
+ }
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Add scheme prefix to an URL. This method must be called on all user entered or server fetched URLs to ensure
+ * http client will work as expected.
+ *
+ * @param url url entered by the user or fetched from a server
+ * @param addHttps true and the url is not starting with http://, it will make the url starts with https://
+ * @return url prefixed by http:// or https://
+ */
+ public static String addUrlSchemeIfNeeded(String url, boolean addHttps) {
+ if (url == null) {
+ return null;
+ }
+
+ // Remove leading double slash (eg. //example.com), needed for some wporg instances configured to
+ // switch between http or https
+ url = removeLeadingDoubleSlash(url, (addHttps ? "https" : "http") + "://");
+
+ // If the URL is a valid http or https URL, we're good to go
+ if (URLUtil.isHttpUrl(url) || URLUtil.isHttpsUrl(url)) {
+ return url;
+ }
+
+ // Else, remove the old scheme and prefix it by https:// or http://
+ return (addHttps ? "https" : "http") + "://" + removeScheme(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 scheme
+ */
+ public static String removeScheme(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ int doubleslash = urlString.indexOf("//");
+ if (doubleslash == -1) {
+ doubleslash = 0;
+ } else {
+ doubleslash += 2;
+ }
+
+ return urlString.substring(doubleslash, urlString.length());
+ }
+
+ /**
+ * returns the passed url without the query parameters
+ */
+ public static String removeQuery(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+ return Uri.parse(urlString).buildUpon().clearQuery().toString();
+ }
+
+ /**
+ * returns true if passed url is https:
+ */
+ public static boolean isHttps(final String urlString) {
+ return (urlString != null && urlString.startsWith("https:"));
+ }
+
+ public static boolean isHttps(URL url) {
+ return url != null && "https".equals(url.getProtocol());
+ }
+
+ public static boolean isHttps(URI uri) {
+ if (uri == null) return false;
+
+ String protocol = uri.getScheme();
+ return protocol != null && protocol.equals("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;
+ }
+
+ // returns true if the passed url is for an image
+ public static boolean isImageUrl(String url) {
+ if (TextUtils.isEmpty(url)) return false;
+
+ String cleanedUrl = removeQuery(url.toLowerCase());
+
+ return cleanedUrl.endsWith("jpg") || cleanedUrl.endsWith("jpeg") ||
+ cleanedUrl.endsWith("gif") || cleanedUrl.endsWith("png");
+ }
+
+ public static String appendUrlParameter(String url, String paramName, String paramValue) {
+ Map<String, String> parameters = new HashMap<>();
+ parameters.put(paramName, paramValue);
+ return appendUrlParameters(url, parameters);
+ }
+
+ public static String appendUrlParameters(String url, Map<String, String> parameters) {
+ Uri.Builder uriBuilder = Uri.parse(url).buildUpon();
+ for (Map.Entry<String, String> parameter : parameters.entrySet()) {
+ uriBuilder.appendQueryParameter(parameter.getKey(), parameter.getValue());
+ }
+ return uriBuilder.build().toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java
new file mode 100644
index 000000000..385960558
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.util;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.util.Patterns;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.regex.Pattern;
+
+public class UserEmailUtils {
+ /**
+ * 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
+ AppLog.e(T.UTILS, "SecurityException - missing GET_ACCOUNTS permission");
+ return "";
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java
new file mode 100644
index 000000000..914373c8f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java
@@ -0,0 +1,58 @@
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+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);
+ }
+ }
+
+ public void saveToPreferences(Context context, String uniqueId) {
+ saveScrollOffset();
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ Editor editor = settings.edit();
+ editor.putInt("scroll-position-manager-index-" + uniqueId, mListViewScrollStateIndex);
+ editor.putInt("scroll-position-manager-offset-" + uniqueId, mListViewScrollStateOffset);
+ editor.putInt("scroll-position-manager-selected-position-" + uniqueId, mSelectedPosition);
+ editor.apply();
+ }
+
+ public void restoreFromPreferences(Context context, String uniqueId) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ mListViewScrollStateIndex = settings.getInt("scroll-position-manager-index-" + uniqueId, 0);
+ mListViewScrollStateOffset = settings.getInt("scroll-position-manager-offset-" + uniqueId, 0);
+ mSelectedPosition = settings.getInt("scroll-position-manager-selected-position-" + uniqueId, 0);
+ restoreScrollOffset();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java
new file mode 100644
index 000000000..ff472c2ed
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java
@@ -0,0 +1,144 @@
+//This Handy-Dandy class acquired and tweaked from http://stackoverflow.com/a/3145655/309558
+package org.wordpress.android.util.helpers;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class LocationHelper {
+ Timer mTimer;
+ LocationManager mLocationManager;
+ LocationResult mLocationResult;
+ boolean mGpsEnabled = false;
+ boolean mNetworkEnabled = false;
+
+ @SuppressLint("MissingPermission")
+ public boolean getLocation(Activity activity, LocationResult result) {
+ mLocationResult = result;
+ if (mLocationManager == null) {
+ mLocationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
+ }
+
+ // exceptions will be thrown if provider is not permitted.
+ try {
+ mGpsEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ } catch (Exception ex) {
+ }
+ try {
+ mNetworkEnabled = mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
+ } catch (Exception ex) {
+ }
+
+ // don't start listeners if no provider is enabled
+ if (!mGpsEnabled && !mNetworkEnabled) {
+ return false;
+ }
+
+ if (mGpsEnabled) {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps);
+ }
+
+ if (mNetworkEnabled) {
+ mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
+ }
+
+ mTimer = new Timer();
+ mTimer.schedule(new GetLastLocation(), 30000);
+ return true;
+ }
+
+ LocationListener locationListenerGps = new LocationListener() {
+ @SuppressLint("MissingPermission")
+ public void onLocationChanged(Location location) {
+ mTimer.cancel();
+ mLocationResult.gotLocation(location);
+ mLocationManager.removeUpdates(this);
+ mLocationManager.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() {
+ @SuppressLint("MissingPermission")
+ public void onLocationChanged(Location location) {
+ mTimer.cancel();
+ mLocationResult.gotLocation(location);
+ mLocationManager.removeUpdates(this);
+ mLocationManager.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
+ @SuppressLint("MissingPermission")
+ public void run() {
+ mLocationManager.removeUpdates(locationListenerGps);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+
+ Location net_loc = null, gps_loc = null;
+ if (mGpsEnabled) {
+ gps_loc = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ }
+ if (mNetworkEnabled) {
+ net_loc = mLocationManager.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()) {
+ mLocationResult.gotLocation(gps_loc);
+ } else {
+ mLocationResult.gotLocation(net_loc);
+ }
+ return;
+ }
+
+ if (gps_loc != null) {
+ mLocationResult.gotLocation(gps_loc);
+ return;
+ }
+ if (net_loc != null) {
+ mLocationResult.gotLocation(net_loc);
+ return;
+ }
+ mLocationResult.gotLocation(null);
+ }
+ }
+
+ public static abstract class LocationResult {
+ public abstract void gotLocation(Location location);
+ }
+
+ @SuppressLint("MissingPermission")
+ public void cancelTimer() {
+ if (mTimer != null) {
+ mTimer.cancel();
+ mLocationManager.removeUpdates(locationListenerGps);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java
new file mode 100644
index 000000000..b57ad0165
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java
@@ -0,0 +1,339 @@
+package org.wordpress.android.util.helpers;
+
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.Date;
+import java.util.Map;
+
+public class MediaFile {
+ protected int id;
+ protected long postID;
+ protected String filePath = null; //path of the file into disk
+ protected String fileName = null; //name of the file into the server
+ protected String title = null;
+ protected String description = null;
+ protected String caption = null;
+ protected int horizontalAlignment; //0 = none, 1 = left, 2 = center, 3 = right
+ protected boolean verticalAligment = false; //false = bottom, true = top
+ protected int width = 500, height;
+ protected String mimeType = "";
+ protected String videoPressShortCode = null;
+ protected boolean featured = false;
+ protected boolean isVideo = false;
+ protected boolean featuredInPost;
+ protected String fileURL = null; // url of the file to download
+ protected String thumbnailURL = null; // url of the thumbnail to download
+ private String blogId;
+ private long dateCreatedGmt;
+ private String uploadState = null;
+ private String mediaId;
+
+ public static String VIDEOPRESS_SHORTCODE_ID = "videopress_shortcode";
+
+ public MediaFile(String blogId, Map<?, ?> resultMap, boolean isDotCom) {
+ setBlogId(blogId);
+ setMediaId(MapUtils.getMapStr(resultMap, "attachment_id"));
+ setPostID(MapUtils.getMapLong(resultMap, "parent"));
+ setTitle(MapUtils.getMapStr(resultMap, "title"));
+ setCaption(MapUtils.getMapStr(resultMap, "caption"));
+ setDescription(MapUtils.getMapStr(resultMap, "description"));
+ setVideoPressShortCode(MapUtils.getMapStr(resultMap, VIDEOPRESS_SHORTCODE_ID));
+
+ // get the file name from the link
+ String link = MapUtils.getMapStr(resultMap, "link");
+ setFileName(new String(link).replaceAll("^.*/([A-Za-z0-9_-]+)\\.\\w+$", "$1"));
+
+ String fileType = new String(link).replaceAll(".*\\.(\\w+)$", "$1").toLowerCase();
+ String fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileType);
+ setMimeType(fileMimeType);
+
+ // make the file urls be https://... so that we can get these images with oauth when the blogs are private
+ // assume no https for images in self-hosted blogs
+ String fileUrl = MapUtils.getMapStr(resultMap, "link");
+ if (isDotCom) {
+ fileUrl = fileUrl.replace("http:", "https:");
+ }
+ setFileURL(fileUrl);
+
+ String thumbnailURL = MapUtils.getMapStr(resultMap, "thumbnail");
+ if (thumbnailURL.startsWith("http")) {
+ if (isDotCom) {
+ thumbnailURL = thumbnailURL.replace("http:", "https:");
+ }
+ setThumbnailURL(thumbnailURL);
+ }
+
+ Date date = MapUtils.getMapDate(resultMap, "date_created_gmt");
+ if (date != null) {
+ setDateCreatedGMT(date.getTime());
+ }
+
+ Object meta = resultMap.get("metadata");
+ if (meta != null && meta instanceof Map) {
+ Map<?, ?> metadata = (Map<?, ?>) meta;
+ setWidth(MapUtils.getMapInt(metadata, "width"));
+ setHeight(MapUtils.getMapInt(metadata, "height"));
+ }
+ }
+
+ public MediaFile() {
+ // default constructor
+ }
+
+ public MediaFile(MediaFile mediaFile) {
+ this.id = mediaFile.id;
+ this.postID = mediaFile.postID;
+ this.filePath = mediaFile.filePath;
+ this.fileName = mediaFile.fileName;
+ this.title = mediaFile.title;
+ this.description = mediaFile.description;
+ this.caption = mediaFile.caption;
+ this.horizontalAlignment = mediaFile.horizontalAlignment;
+ this.verticalAligment = mediaFile.verticalAligment;
+ this.width = mediaFile.width;
+ this.height = mediaFile.height;
+ this.mimeType = mediaFile.mimeType;
+ this.videoPressShortCode = mediaFile.videoPressShortCode;
+ this.featured = mediaFile.featured;
+ this.isVideo = mediaFile.isVideo;
+ this.featuredInPost = mediaFile.featuredInPost;
+ this.fileURL = mediaFile.fileURL;
+ this.thumbnailURL = mediaFile.thumbnailURL;
+ this.blogId = mediaFile.blogId;
+ this.dateCreatedGmt = mediaFile.dateCreatedGmt;
+ this.uploadState = mediaFile.uploadState;
+ this.mediaId = mediaFile.mediaId;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public void setMediaId(String id) {
+ mediaId = id;
+ }
+
+ public boolean isFeatured() {
+ return featured;
+ }
+
+ public void setFeatured(boolean featured) {
+ this.featured = featured;
+ }
+
+ public long getPostID() {
+ return postID;
+ }
+
+ public void setPostID(long postID) {
+ this.postID = postID;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public void setFilePath(String filePath) {
+ this.filePath = filePath;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getFileURL() {
+ return fileURL;
+ }
+
+ public void setFileURL(String fileURL) {
+ this.fileURL = fileURL;
+ }
+
+ public String getThumbnailURL() {
+ return thumbnailURL;
+ }
+
+ public void setThumbnailURL(String thumbnailURL) {
+ this.thumbnailURL = thumbnailURL;
+ }
+
+ public boolean isVerticalAlignmentOnTop() {
+ return verticalAligment;
+ }
+
+ public void setVerticalAlignmentOnTop(boolean verticalAligment) {
+ this.verticalAligment = verticalAligment;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public String getMimeType() {
+ return StringUtils.notNullStr(mimeType);
+ }
+
+ public void setMimeType(String type) {
+ mimeType = StringUtils.notNullStr(type);
+ }
+
+ public String getVideoPressShortCode() {
+ return videoPressShortCode;
+ }
+
+ public void setVideoPressShortCode(String videoPressShortCode) {
+ this.videoPressShortCode = videoPressShortCode;
+ }
+
+ public int getHorizontalAlignment() {
+ return horizontalAlignment;
+ }
+
+ public void setHorizontalAlignment(int horizontalAlignment) {
+ this.horizontalAlignment = horizontalAlignment;
+ }
+
+ public boolean isVideo() {
+ return isVideo;
+ }
+
+ public void setVideo(boolean isVideo) {
+ this.isVideo = isVideo;
+ }
+
+ public boolean isFeaturedInPost() {
+ return featuredInPost;
+ }
+
+ public void setFeaturedInPost(boolean featuredInPost) {
+ this.featuredInPost = featuredInPost;
+ }
+
+ public String getBlogId() {
+ return blogId;
+ }
+
+ public void setBlogId(String blogId) {
+ this.blogId = blogId;
+ }
+
+ public void setDateCreatedGMT(long date_created_gmt) {
+ this.dateCreatedGmt = date_created_gmt;
+ }
+
+ public long getDateCreatedGMT() {
+ return dateCreatedGmt;
+ }
+
+ public void setUploadState(String uploadState) {
+ this.uploadState = uploadState;
+ }
+
+ public String getUploadState() {
+ return uploadState;
+ }
+
+ /**
+ * Outputs the Html for an image
+ * If a fullSizeUrl exists, a link will be created to it from the resizedPictureUrl
+ */
+ public String getImageHtmlForUrls(String fullSizeUrl, String resizedPictureURL, boolean shouldAddImageWidthCSS) {
+ String alignment = "";
+ switch (getHorizontalAlignment()) {
+ case 0:
+ alignment = "alignnone";
+ break;
+ case 1:
+ alignment = "alignleft";
+ break;
+ case 2:
+ alignment = "aligncenter";
+ break;
+ case 3:
+ alignment = "alignright";
+ break;
+ }
+
+ String alignmentCSS = "class=\"" + alignment + " size-full\" ";
+
+ if (shouldAddImageWidthCSS) {
+ alignmentCSS += "style=\"max-width: " + getWidth() + "px\" ";
+ }
+
+ // Check if we uploaded a featured picture that is not added to the Post content (normal case)
+ if ((fullSizeUrl != null && fullSizeUrl.equalsIgnoreCase("")) ||
+ (resizedPictureURL != null && resizedPictureURL.equalsIgnoreCase(""))) {
+ return ""; // Not featured in Post. Do not add to the content.
+ }
+
+ if (fullSizeUrl == null && resizedPictureURL != null) {
+ fullSizeUrl = resizedPictureURL;
+ } else if (fullSizeUrl != null && resizedPictureURL == null) {
+ resizedPictureURL = fullSizeUrl;
+ }
+
+ String mediaTitle = StringUtils.notNullStr(getTitle());
+
+ String content = String.format("<a href=\"%s\"><img title=\"%s\" %s alt=\"image\" src=\"%s\" /></a>",
+ fullSizeUrl, mediaTitle, alignmentCSS, resizedPictureURL);
+
+ if (!TextUtils.isEmpty(getCaption())) {
+ content = String.format("[caption id=\"\" align=\"%s\" width=\"%d\"]%s%s[/caption]",
+ alignment, getWidth(), content, TextUtils.htmlEncode(getCaption()));
+ }
+
+ return content;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java
new file mode 100644
index 000000000..ab7326a17
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java
@@ -0,0 +1,87 @@
+
+package org.wordpress.android.util.helpers;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/**
+ * A model representing a Media Gallery.
+ * A unique id is not used on the website, but only in this app.
+ * It is used to uniquely determining the instance of the object, as it is
+ * passed between post and media gallery editor.
+ */
+public class MediaGallery implements Serializable {
+ private static final long serialVersionUID = 2359176987182027508L;
+
+ private long uniqueId;
+ private boolean isRandom;
+ private String type;
+ private int numColumns;
+ private ArrayList<String> ids;
+
+ public MediaGallery(boolean isRandom, String type, int numColumns, ArrayList<String> ids) {
+ this.isRandom = isRandom;
+ this.type = type;
+ this.numColumns = numColumns;
+ this.ids = ids;
+ this.uniqueId = System.currentTimeMillis();
+ }
+
+ public MediaGallery() {
+ isRandom = false;
+ type = "";
+ numColumns = 3;
+ ids = new ArrayList<String>();
+ this.uniqueId = System.currentTimeMillis();
+ }
+
+ public boolean isRandom() {
+ return isRandom;
+ }
+
+ public void setRandom(boolean isRandom) {
+ this.isRandom = isRandom;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public int getNumColumns() {
+ return numColumns;
+ }
+
+ public void setNumColumns(int numColumns) {
+ this.numColumns = numColumns;
+ }
+
+ public ArrayList<String> getIds() {
+ return ids;
+ }
+
+ public String getIdsStr() {
+ String ids_str = "";
+ if (ids.size() > 0) {
+ for (String id : ids) {
+ ids_str += id + ",";
+ }
+ ids_str = ids_str.substring(0, ids_str.length() - 1);
+ }
+ return ids_str;
+ }
+
+ public void setIds(ArrayList<String> ids) {
+ this.ids = ids;
+ }
+
+ /**
+ * An id to uniquely identify a media gallery object, so that the same object can be edited in the post editor
+ */
+ public long getUniqueId() {
+ return uniqueId;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java
new file mode 100644
index 000000000..588b98141
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.text.style.ImageSpan;
+
+public class MediaGalleryImageSpan extends ImageSpan {
+ private MediaGallery mMediaGallery;
+
+ public MediaGalleryImageSpan(Context context, MediaGallery mediaGallery, int placeHolder) {
+ super(context, placeHolder);
+ setMediaGallery(mediaGallery);
+ }
+
+ public MediaGallery getMediaGallery() {
+ return mMediaGallery;
+ }
+
+ public void setMediaGallery(MediaGallery mediaGallery) {
+ this.mMediaGallery = mediaGallery;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java
new file mode 100644
index 000000000..e6f4bf323
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java
@@ -0,0 +1,72 @@
+package org.wordpress.android.util.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
+import android.util.TypedValue;
+
+import org.wordpress.android.util.R;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+public class SwipeToRefreshHelper implements OnRefreshListener {
+ private CustomSwipeRefreshLayout mSwipeRefreshLayout;
+ private RefreshListener mRefreshListener;
+ private boolean mRefreshing;
+
+ public interface RefreshListener {
+ public void onRefreshStarted();
+ }
+
+ public SwipeToRefreshHelper(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) {
+ init(context, swipeRefreshLayout, listener);
+ }
+
+ public void init(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) {
+ mRefreshListener = listener;
+ mSwipeRefreshLayout = swipeRefreshLayout;
+ mSwipeRefreshLayout.setOnRefreshListener(this);
+ final TypedArray styleAttrs = obtainStyledAttrsFromThemeAttr(context, R.attr.swipeToRefreshStyle,
+ R.styleable.RefreshIndicator);
+ int color = styleAttrs.getColor(R.styleable.RefreshIndicator_refreshIndicatorColor, context.getResources()
+ .getColor(android.R.color.holo_blue_dark));
+ mSwipeRefreshLayout.setColorSchemeColors(color, color, color, color);
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mRefreshing = refreshing;
+ // Delayed refresh, it fixes https://code.google.com/p/android/issues/detail?id=77712
+ // 50ms seems a good compromise (always worked during tests) and fast enough so user can't notice the delay
+ if (refreshing) {
+ mSwipeRefreshLayout.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // use mRefreshing so if the refresh takes less than 50ms, loading indicator won't show up.
+ mSwipeRefreshLayout.setRefreshing(mRefreshing);
+ }
+ }, 50);
+ } else {
+ mSwipeRefreshLayout.setRefreshing(false);
+ }
+ }
+
+ public boolean isRefreshing() {
+ return mSwipeRefreshLayout.isRefreshing();
+ }
+
+ @Override
+ public void onRefresh() {
+ mRefreshListener.onRefreshStarted();
+ }
+
+ public void setEnabled(boolean enabled) {
+ mSwipeRefreshLayout.setEnabled(enabled);
+ }
+
+ public static TypedArray obtainStyledAttrsFromThemeAttr(Context context, int themeAttr, int[] styleAttrs) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(themeAttr, outValue, true);
+ int styleResId = outValue.resourceId;
+ return context.obtainStyledAttributes(styleResId, styleAttrs);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java
new file mode 100644
index 000000000..b35f84757
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.util.helpers;
+
+//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;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java
new file mode 100644
index 000000000..da333b24e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java
@@ -0,0 +1,59 @@
+package org.wordpress.android.util.helpers;
+
+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/helpers/WPImageGetter.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java
new file mode 100644
index 000000000..b03d74045
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java
@@ -0,0 +1,177 @@
+package org.wordpress.android.util.helpers;
+
+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;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.PhotonUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * ImageGetter for Html.fromHtml()
+ * adapted from existing ImageGetter code in NoteCommentFragment
+ */
+public class WPImageGetter implements Html.ImageGetter {
+ private final WeakReference<TextView> mWeakView;
+ private final 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;
+ }
+
+ 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);
+ }
+
+ 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) {
+ AppLog.w(T.UTILS, "WPImageGetter null bitmap");
+ }
+
+ TextView view = getView();
+ if (view == null) {
+ AppLog.w(T.UTILS, "WPImageGetter view is invalid");
+ return;
+ }
+
+ int maxWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
+ if (mMaxSize > 0 && (maxWidth > mMaxSize || maxWidth == 0)) {
+ maxWidth = mMaxSize;
+ }
+
+ Drawable drawable = new BitmapDrawable(view.getContext().getResources(), response.getBitmap());
+ remote.setRemoteDrawable(drawable, maxWidth);
+
+ // force textView to resize correctly if image isn't cached by resetting the content
+ // to itself - this way the textView will use the cached image, and resizing to
+ // accommodate the image isn't necessary
+ if (!isImmediate) {
+ view.setText(view.getText());
+ }
+ }
+ });
+
+ return remote;
+ }
+
+ public static class RemoteDrawable extends BitmapDrawable {
+ Drawable mRemoteDrawable;
+ final Drawable mLoadingDrawable;
+ final 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, 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/helpers/WPImageSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java
new file mode 100644
index 000000000..fa0a0b4aa
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java
@@ -0,0 +1,140 @@
+//Add WordPress image fields to ImageSpan object
+
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.style.ImageSpan;
+
+public class WPImageSpan extends ImageSpan implements Parcelable {
+ protected Uri mImageSource = null;
+ protected boolean mNetworkImageLoaded = false;
+ protected MediaFile mMediaFile;
+ protected int mStartPosition, mEndPosition;
+
+ protected WPImageSpan() {
+ super((Bitmap) null);
+ }
+
+ public WPImageSpan(Context context, Bitmap b, Uri src) {
+ super(context, b);
+ this.mImageSource = src;
+ mMediaFile = new MediaFile();
+ }
+
+ public WPImageSpan(Context context, int resId, Uri src) {
+ super(context, resId);
+ this.mImageSource = src;
+ mMediaFile = new MediaFile();
+ }
+
+ public void setPosition(int start, int end) {
+ mStartPosition = start;
+ mEndPosition = end;
+ }
+
+ public int getStartPosition() {
+ return mStartPosition >= 0 ? mStartPosition : 0;
+ }
+
+ public int getEndPosition() {
+ return mEndPosition < getStartPosition() ? getStartPosition() : mEndPosition;
+ }
+
+ public MediaFile getMediaFile() {
+ return mMediaFile;
+ }
+
+ public void setMediaFile(MediaFile mMediaFile) {
+ this.mMediaFile = mMediaFile;
+ }
+
+ public void setImageSource(Uri mImageSource) {
+ this.mImageSource = mImageSource;
+ }
+
+ public Uri getImageSource() {
+ return mImageSource;
+ }
+
+ public boolean isNetworkImageLoaded() {
+ return mNetworkImageLoaded;
+ }
+
+ public void setNetworkImageLoaded(boolean networkImageLoaded) {
+ this.mNetworkImageLoaded = networkImageLoaded;
+ }
+
+ protected void setupFromParcel(Parcel in) {
+ MediaFile mediaFile = new MediaFile();
+
+ boolean[] booleans = new boolean[2];
+ in.readBooleanArray(booleans);
+ setNetworkImageLoaded(booleans[0]);
+ mediaFile.setVideo(booleans[1]);
+
+ setImageSource(Uri.parse(in.readString()));
+ mediaFile.setMediaId(in.readString());
+ mediaFile.setBlogId(in.readString());
+ mediaFile.setPostID(in.readLong());
+ mediaFile.setCaption(in.readString());
+ mediaFile.setDescription(in.readString());
+ mediaFile.setTitle(in.readString());
+ mediaFile.setMimeType(in.readString());
+ mediaFile.setFileName(in.readString());
+ mediaFile.setThumbnailURL(in.readString());
+ mediaFile.setVideoPressShortCode(in.readString());
+ mediaFile.setFileURL(in.readString());
+ mediaFile.setFilePath(in.readString());
+ mediaFile.setDateCreatedGMT(in.readLong());
+ mediaFile.setWidth(in.readInt());
+ mediaFile.setHeight(in.readInt());
+ setPosition(in.readInt(), in.readInt());
+
+ setMediaFile(mediaFile);
+ }
+
+ public static final Parcelable.Creator<WPImageSpan> CREATOR
+ = new Parcelable.Creator<WPImageSpan>() {
+ public WPImageSpan createFromParcel(Parcel in) {
+ WPImageSpan imageSpan = new WPImageSpan();
+ imageSpan.setupFromParcel(in);
+ return imageSpan;
+ }
+
+ public WPImageSpan[] newArray(int size) {
+ return new WPImageSpan[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeBooleanArray(new boolean[] {mNetworkImageLoaded, mMediaFile.isVideo()});
+ parcel.writeString(mImageSource.toString());
+ parcel.writeString(mMediaFile.getMediaId());
+ parcel.writeString(mMediaFile.getBlogId());
+ parcel.writeLong(mMediaFile.getPostID());
+ parcel.writeString(mMediaFile.getCaption());
+ parcel.writeString(mMediaFile.getDescription());
+ parcel.writeString(mMediaFile.getTitle());
+ parcel.writeString(mMediaFile.getMimeType());
+ parcel.writeString(mMediaFile.getFileName());
+ parcel.writeString(mMediaFile.getThumbnailURL());
+ parcel.writeString(mMediaFile.getVideoPressShortCode());
+ parcel.writeString(mMediaFile.getFileURL());
+ parcel.writeString(mMediaFile.getFilePath());
+ parcel.writeLong(mMediaFile.getDateCreatedGMT());
+ parcel.writeInt(mMediaFile.getWidth());
+ parcel.writeInt(mMediaFile.getHeight());
+ parcel.writeInt(getStartPosition());
+ parcel.writeInt(getEndPosition());
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java
new file mode 100644
index 000000000..33cdc0093
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.util.helpers;
+
+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/helpers/WPUnderlineSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java
new file mode 100644
index 000000000..4b6805ccf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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.helpers;
+
+import android.os.Parcel;
+import android.text.style.UnderlineSpan;
+
+/**
+ * WPUnderlineSpan is used as an alternative class to UnderlineSpan. UnderlineSpan is used by EditText auto
+ * correct, so it can get mixed up with our formatting.
+ */
+public class WPUnderlineSpan extends UnderlineSpan {
+ public WPUnderlineSpan() {
+ super();
+ }
+
+ public WPUnderlineSpan(Parcel src) {
+ super(src);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java
new file mode 100644
index 000000000..1418e79ea
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util.helpers;
+
+import android.app.Activity;
+import android.text.TextUtils;
+import android.view.View;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.widget.ProgressBar;
+
+public class WPWebChromeClient extends WebChromeClient {
+ private final ProgressBar mProgressBar;
+ private final Activity mActivity;
+ private final boolean mAutoUpdateActivityTitle;
+
+ public WPWebChromeClient(Activity activity, ProgressBar progressBar) {
+ mActivity = activity;
+ mProgressBar = progressBar;
+ mAutoUpdateActivityTitle = true;
+ }
+
+ public WPWebChromeClient(Activity activity,
+ ProgressBar progressBar,
+ boolean autoUpdateActivityTitle) {
+ mActivity = activity;
+ mProgressBar = progressBar;
+ mAutoUpdateActivityTitle = autoUpdateActivityTitle;
+ }
+
+ public void onProgressChanged(WebView webView, int progress) {
+ if (mActivity != null
+ && !mActivity.isFinishing()
+ && mAutoUpdateActivityTitle
+ && !TextUtils.isEmpty(webView.getTitle())) {
+ mActivity.setTitle(webView.getTitle());
+ }
+ if (mProgressBar != null) {
+ if (progress == 100) {
+ mProgressBar.setVisibility(View.GONE);
+ } else {
+ mProgressBar.setVisibility(View.VISIBLE);
+ mProgressBar.setProgress(progress);
+ }
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java
new file mode 100644
index 000000000..b0b4dc017
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java
@@ -0,0 +1,299 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Text view that auto adjusts text size to fit within the view.
+ * If the text size equals the minimum text size and still does not
+ * fit, append with an ellipsis.
+ *
+ * See http://stackoverflow.com/a/5535672
+ *
+ */
+public class AutoResizeTextView extends TextView {
+ // Minimum text size for this text view
+ private static final float MIN_TEXT_SIZE = 20;
+
+ // Interface for resize notifications
+ public interface OnTextResizeListener {
+ void onTextResize(TextView textView, float oldSize, float newSize);
+ }
+
+ // Our ellipse string - Unicode Character 'HORIZONTAL ELLIPSIS'
+ private static final String M_ELLIPSIS = "\u2026";
+
+ // Registered resize listener
+ private OnTextResizeListener mTextResizeListener;
+
+ // Flag for text and/or size changes to force a resize
+ private boolean mNeedsResize = false;
+
+ // Text size that is set from code. This acts as a starting point for resizing
+ private float mTextSize;
+
+ // Temporary upper bounds on the starting text size
+ private float mMaxTextSize = 0;
+
+ // Lower bounds for text size
+ private float mMinTextSize = MIN_TEXT_SIZE;
+
+ // Text view line spacing multiplier
+ private float mSpacingMult = 1.0f;
+
+ // Text view additional line spacing
+ private float mSpacingAdd = 0.0f;
+
+ // Add ellipsis to text that overflows at the smallest text size
+ private boolean mAddEllipsis = true;
+
+ // Default constructor override
+ public AutoResizeTextView(Context context) {
+ this(context, null);
+ }
+
+ // Default constructor when inflating from XML file
+ public AutoResizeTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ // Default constructor override
+ public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * When text changes, set the force resize flag to true and reset the text size.
+ */
+ @Override
+ protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
+ mNeedsResize = true;
+ // Since this view may be reused, it is good to reset the text size
+ resetTextSize();
+ }
+
+ /**
+ * If the text view size changed, set the force resize flag to true
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (w != oldw || h != oldh) {
+ mNeedsResize = true;
+ }
+ }
+
+ /**
+ * Register listener to receive resize notifications
+ * @param listener
+ */
+ public void setOnResizeListener(OnTextResizeListener listener) {
+ mTextResizeListener = listener;
+ }
+
+ /**
+ * Override the set text size to update our internal reference values
+ */
+ @Override
+ public void setTextSize(float size) {
+ super.setTextSize(size);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * Override the set text size to update our internal reference values
+ */
+ @Override
+ public void setTextSize(int unit, float size) {
+ super.setTextSize(unit, size);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * Override the set line spacing to update our internal reference values
+ */
+ @Override
+ public void setLineSpacing(float add, float mult) {
+ super.setLineSpacing(add, mult);
+ mSpacingMult = mult;
+ mSpacingAdd = add;
+ }
+
+ /**
+ * Set the upper text size limit and invalidate the view
+ * @param maxTextSize
+ */
+ public void setMaxTextSize(float maxTextSize) {
+ mMaxTextSize = maxTextSize;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Return upper text size limit
+ * @return
+ */
+ public float getMaxTextSize() {
+ return mMaxTextSize;
+ }
+
+ /**
+ * Set the lower text size limit and invalidate the view
+ * @param minTextSize
+ */
+ public void setMinTextSize(float minTextSize) {
+ mMinTextSize = minTextSize;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Return lower text size limit
+ * @return
+ */
+ public float getMinTextSize() {
+ return mMinTextSize;
+ }
+
+ /**
+ * Set flag to add ellipsis to text that overflows at the smallest text size
+ * @param addEllipsis
+ */
+ public void setAddEllipsis(boolean addEllipsis) {
+ mAddEllipsis = addEllipsis;
+ }
+
+ /**
+ * Return flag to add ellipsis to text that overflows at the smallest text size
+ * @return
+ */
+ public boolean getAddEllipsis() {
+ return mAddEllipsis;
+ }
+
+ /**
+ * Reset the text to the original size
+ */
+ private void resetTextSize() {
+ if (mTextSize > 0) {
+ super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ mMaxTextSize = mTextSize;
+ }
+ }
+
+ /**
+ * Resize text after measuring
+ */
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (changed || mNeedsResize) {
+ int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
+ resizeText(widthLimit, heightLimit);
+ }
+ super.onLayout(changed, left, top, right, bottom);
+ }
+
+ /**
+ * Resize the text size with default width and height
+ */
+ public void resizeText() {
+ int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
+ int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
+ resizeText(widthLimit, heightLimit);
+ }
+
+ /**
+ * Resize the text size with specified width and height
+ * @param width
+ * @param height
+ */
+ public void resizeText(int width, int height) {
+ CharSequence text = getText();
+ // Do not resize if the view does not have dimensions or there is no text
+ if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
+ return;
+ }
+
+ // Get the text view's paint object
+ TextPaint textPaint = getPaint();
+
+ // Store the current text size
+ float oldTextSize = textPaint.getTextSize();
+ // If there is a max text size set, use the lesser of that and the default text size
+ float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
+
+ // Get the required text height
+ int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
+
+ // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
+ while (textHeight > height && targetTextSize > mMinTextSize) {
+ targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
+ textHeight = getTextHeight(text, textPaint, width, targetTextSize);
+ }
+
+ // If we had reached our minimum text size and still don't fit, append an ellipsis
+ if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
+ // Draw using a static layout
+ // modified: use a copy of TextPaint for measuring
+ TextPaint paint = new TextPaint(textPaint);
+ // Draw using a static layout
+ StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL,
+ mSpacingMult, mSpacingAdd, false);
+ // Check that we have a least one line of rendered text
+ if (layout.getLineCount() > 0) {
+ // Since the line at the specific vertical position would be cut off,
+ // we must trim up to the previous line
+ int lastLine = layout.getLineForVertical(height) - 1;
+ // If the text would not even fit on a single line, clear it
+ if (lastLine < 0) {
+ setText("");
+ } else {
+ // Otherwise, trim to the previous line and add an ellipsis
+ int start = layout.getLineStart(lastLine);
+ int end = layout.getLineEnd(lastLine);
+ float lineWidth = layout.getLineWidth(lastLine);
+ float ellipseWidth = paint.measureText(M_ELLIPSIS);
+
+ // Trim characters off until we have enough room to draw the ellipsis
+ while (width < lineWidth + ellipseWidth) {
+ lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString());
+ }
+ setText(text.subSequence(0, end) + M_ELLIPSIS);
+ }
+ }
+ }
+
+ // Some devices try to auto adjust line spacing, so force default line spacing
+ // and invalidate the layout as a side effect
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
+ setLineSpacing(mSpacingAdd, mSpacingMult);
+
+ // Notify the listener if registered
+ if (mTextResizeListener != null) {
+ mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
+ }
+
+ // Reset force resize flag
+ mNeedsResize = false;
+ }
+
+ // Set the text size of the text paint object and use a static layout to render text off screen before measuring
+ private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
+ // modified: make a copy of the original TextPaint object for measuring
+ // (apparently the object gets modified while measuring, see also the
+ // docs for TextView.getPaint() (which states to access it read-only)
+ TextPaint paintCopy = new TextPaint(paint);
+ // Update the text paint object
+ paintCopy.setTextSize(textSize);
+ // Measure using a static layout
+ StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.ALIGN_NORMAL,
+ mSpacingMult, mSpacingAdd, true);
+ return layout.getHeight();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java
new file mode 100644
index 000000000..356268922
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java
@@ -0,0 +1,33 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
+ public CustomSwipeRefreshLayout(Context context) {
+ super(context);
+ }
+
+ public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ try{
+ return super.onTouchEvent(event);
+ } catch(IllegalArgumentException e) {
+ // Fix for https://github.com/wordpress-mobile/WordPress-Android/issues/2373
+ // Catch IllegalArgumentException which can be fired by the underlying SwipeRefreshLayout.onTouchEvent()
+ // method.
+ // When android support-v4 fixes it, we'll have to remove that custom layout completely.
+ AppLog.e(T.UTILS, e);
+ return true;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java
new file mode 100644
index 000000000..0468cf807
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+/*
+ * @deprecated This custom EditText is used solely by the "legacy" editor in WP Android.
+ * It will be removed when we drop the legacy editor and should not be used in new code.
+ */
+@Deprecated
+public class WPEditText extends EditText {
+ private EditTextImeBackListener mOnImeBack;
+ private OnSelectionChangedListener onSelectionChangedListener;
+
+ public WPEditText(Context context) {
+ super(context);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ if (onSelectionChangedListener != null) {
+ onSelectionChangedListener.onSelectionChanged();
+ }
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ if (mOnImeBack != null)
+ mOnImeBack.onImeBack(this, this.getText().toString());
+ }
+
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ public void setOnEditTextImeBackListener(EditTextImeBackListener listener) {
+ mOnImeBack = listener;
+ }
+
+ public interface EditTextImeBackListener {
+ public abstract void onImeBack(WPEditText ctrl, String text);
+ }
+
+ public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
+ onSelectionChangedListener = listener;
+ }
+
+ public interface OnSelectionChangedListener {
+ public abstract void onSelectionChanged();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/res/values/attrs.xml b/libs/utils/WordPressUtils/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..dd1fa5cbf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/attrs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <attr name="swipeToRefreshStyle" format="reference"/>
+ <declare-styleable name="RefreshIndicator">
+ <attr name="refreshIndicatorColor" format="reference|color"/>
+ </declare-styleable>
+</resources>
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..34d25dada
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="no_network_message">There is no network available</string>
+ <string name="timespan_now">Now</string>
+</resources>