diff --git a/WordPressUtils/build.gradle b/WordPressUtils/build.gradle index 86f124cff9b7..dc4230cfe1d4 100644 --- a/WordPressUtils/build.gradle +++ b/WordPressUtils/build.gradle @@ -1,9 +1,9 @@ buildscript { repositories { - mavenCentral() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:1.5.0' } } @@ -12,25 +12,29 @@ apply plugin: 'maven' apply plugin: 'signing' repositories { - mavenCentral() + jcenter() } dependencies { - compile 'commons-lang:commons-lang:2.6' - compile 'com.mcxiaoke.volley:library:1.0.10' - compile 'com.android.support:support-v13:21.0.3' + 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:23.1.1' } android { + useLibrary 'org.apache.http.legacy' + publishNonDefault true - compileSdkVersion 19 - buildToolsVersion "21.1.1" + compileSdkVersion 23 + buildToolsVersion "23.0.2" defaultConfig { - versionName "1.3.0" + versionName "1.9.0" minSdkVersion 14 - targetSdkVersion 19 + targetSdkVersion 23 } } @@ -87,3 +91,17 @@ uploadArchives { } } } + +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' + } +} diff --git a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java new file mode 100644 index 000000000000..f7c747ff7024 --- /dev/null +++ b/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/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java new file mode 100644 index 000000000000..c506a452ee5d --- /dev/null +++ b/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/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java new file mode 100644 index 000000000000..abf7a8fae526 --- /dev/null +++ b/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 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 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/WordPressUtils/src/main/AndroidManifest.xml b/WordPressUtils/src/main/AndroidManifest.xml index 4f3bd125a3c5..44b1dcddc8bf 100644 --- a/WordPressUtils/src/main/AndroidManifest.xml +++ b/WordPressUtils/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java similarity index 61% rename from WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java index 76800de4cd6d..79b2dbcedaa7 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtil.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java @@ -21,7 +21,7 @@ import android.content.Context; import android.content.DialogInterface; -public class AlertUtil { +public class AlertUtils { /** * Show Alert Dialog * @param context @@ -30,10 +30,10 @@ public class AlertUtil { */ 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(); + .setTitle(titleId) + .setPositiveButton(android.R.string.ok, null) + .setMessage(messageId) + .create(); dlg.show(); } @@ -42,14 +42,14 @@ public static void showAlert(Context context, int titleId, int messageId) { * Show Alert Dialog * @param context * @param titleId - * @param messageId + * @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(); + .setTitle(titleId) + .setPositiveButton(android.R.string.ok, null) + .setMessage(message) + .create(); dlg.show(); } @@ -65,15 +65,15 @@ public static void showAlert(Context context, int titleId, String message) { * @param negativeListener */ public static void showAlert(Context context, int titleId, int messageId, - CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener, - CharSequence negativeButtontxt, DialogInterface.OnClickListener negativeListener) { + 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(); + .setTitle(titleId) + .setPositiveButton(positiveButtontxt, positiveListener) + .setNegativeButton(negativeButtontxt, negativeListener) + .setMessage(messageId) + .setCancelable(false) + .create(); dlg.show(); } @@ -82,20 +82,19 @@ public static void showAlert(Context context, int titleId, int messageId, * Show Alert Dialog * @param context * @param titleId - * @param messageId + * @param message * @param positiveButtontxt * @param positiveListener */ public static void showAlert(Context context, int titleId, String message, - CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener) { + CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener) { Dialog dlg = new AlertDialog.Builder(context) - .setTitle(titleId) - .setPositiveButton(positiveButtontxt, positiveListener) - .setMessage(message) - .setCancelable(false) - .create(); + .setTitle(titleId) + .setPositiveButton(positiveButtontxt, positiveListener) + .setMessage(message) + .setCancelable(false) + .create(); dlg.show(); } -} - +} \ No newline at end of file diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java index 7be3209f78f2..71242e1e805f 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java @@ -11,11 +11,12 @@ import java.util.NoSuchElementException; /** - * simple wrapper for Android log calls, enables recording & displaying log + * 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} + public enum T {READER, EDITOR, MEDIA, NUX, API, STATS, UTILS, NOTIFS, DB, POSTS, COMMENTS, THEMES, TESTS, PROFILING, + SIMPERIUM, SUGGESTION, MAIN} public static final String TAG = "WordPress"; public static final int HEADER_LINE_COUNT = 2; @@ -25,43 +26,81 @@ private AppLog() { throw new AssertionError(); } - /* - * defaults to false, pass true to capture log so it can be displayed by AppLogViewerActivity + /** + * 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); @@ -69,12 +108,23 @@ public static void e(T tag, String message, Throwable tr) { 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; @@ -113,22 +163,31 @@ private String toHtmlColor() { } private static class LogEntry { - LogLevel logLevel; - String logText; - T logTag; + 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() - .append("") - .append("[") - .append(logTag.name()) - .append("] ") - .append(logLevel.name()) - .append(": ") - .append(TextUtils.htmlEncode(logText).replace("\n", "
")) - .append("
"); + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append("["); + sb.append(mLogTag.name()); + sb.append("] "); + sb.append(mLogLevel.name()); + sb.append(": "); + sb.append(TextUtils.htmlEncode(mLogText).replace("\n", "
")); + sb.append("
"); return sb.toString(); } } @@ -155,12 +214,10 @@ private void removeFirstEntry() { private static void addEntry(T tag, LogLevel level, String text) { // skip if recording is disabled (default) - if (!mEnableRecording) + if (!mEnableRecording) { return; - LogEntry entry = new LogEntry(); - entry.logLevel = level; - entry.logText = text; - entry.logTag = tag; + } + LogEntry entry = new LogEntry(level, text, tag); mLogEntries.addEntry(entry); } @@ -170,9 +227,10 @@ private static String getStringStackTrace(Throwable throwable) { return errors.toString(); } - - /* - * returns entire log as html for display (see AppLogViewerActivity) + /** + * Returns entire log as html for display (see AppLogViewerActivity) + * @param context + * @return Arraylist of Strings containing log messages */ public static ArrayList toHtmlList(Context context) { ArrayList items = new ArrayList(); @@ -188,25 +246,26 @@ public static ArrayList toHtmlList(Context context) { return items; } - - /* - * returns entire log as plain text + /** + * 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"); + .append("Android device name: " + DeviceUtils.getInstance().getDeviceName(context)).append("\n\n"); Iterator it = mLogEntries.iterator(); int lineNum = 1; while (it.hasNext()) { - sb.append(String.format("%02d - ", lineNum)) - .append(it.next().logText) - .append("\n"); + sb.append(String.format("%02d - ", lineNum)) + .append(it.next().mLogText) + .append("\n"); lineNum++; } return sb.toString(); } -} +} \ No newline at end of file diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java index 4944bf0a8a2d..0d21c0c2f1f2 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java @@ -6,30 +6,21 @@ public class BlogUtils { public static Comparator BlogNameComparator = new Comparator() { public int compare(Object blog1, Object blog2) { - Map blogMap1 = (Map) blog1; - Map blogMap2 = (Map) blog2; - - String blogName1 = MapUtils.getMapStr(blogMap1, "blogName"); - if (blogName1.length() == 0) { - blogName1 = MapUtils.getMapStr(blogMap1, "url"); - } - - String blogName2 = MapUtils.getMapStr(blogMap2, "blogName"); - if (blogName2.length() == 0) { - blogName2 = MapUtils.getMapStr(blogMap2, "url"); - } - + Map blogMap1 = (Map) blog1; + Map blogMap2 = (Map) blog2; + String blogName1 = getBlogNameOrHomeURLFromAccountMap(blogMap1); + String blogName2 = getBlogNameOrHomeURLFromAccountMap(blogMap2); return blogName1.compareToIgnoreCase(blogName2); } }; /** - * Return a blog name or blog url (host part only) if trimmed name is an empty string + * Return a blog name or blog home URL if trimmed name is an empty string */ - public static String getBlogNameOrHostNameFromAccountMap(Map account) { + public static String getBlogNameOrHomeURLFromAccountMap(Map account) { String blogName = getBlogNameFromAccountMap(account); if (blogName.trim().length() == 0) { - blogName = StringUtils.getHost(MapUtils.getMapStr(account, "url")); + blogName = BlogUtils.getHomeURLOrHostNameFromAccountMap(account); } return blogName; } @@ -42,9 +33,16 @@ public static String getBlogNameFromAccountMap(Map account) { } /** - * Return blog url (host part only) if trimmed name is an empty string + * Return the blog home URL setting or the host name if home URL is an empty string. */ - public static String getHostNameFromAccountMap(Map account) { - return StringUtils.getHost(MapUtils.getMapStr(account, "url")); + public static String getHomeURLOrHostNameFromAccountMap(Map 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; } } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java index fd4cbb5df5be..40a017e9a336 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java @@ -38,6 +38,12 @@ public static int getDisplayPixelHeight(Context 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()); diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java index 64ee67e566a9..66a0c77fdf8f 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java @@ -4,6 +4,7 @@ import android.text.TextUtils; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import android.widget.TextView; /** * EditText utils @@ -14,13 +15,10 @@ private EditTextUtils() { } /** - * returns text string from passed EditText + * returns non-null text string from passed TextView */ - public static String getText(EditText edit) { - if (edit.getText() == null) { - return ""; - } - return edit.getText().toString(); + public static String getText(TextView textView) { + return (textView != null) ? textView.getText().toString() : ""; } /** diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java b/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java similarity index 97% rename from WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java index 5a7566a967cf..45661d98000a 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/Emoticons.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java @@ -14,7 +14,7 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES; -public class Emoticons { +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 wpSmilies; @@ -82,7 +82,7 @@ public static String lookupImageSmiley(String url, String ifNone){ public static Spanned replaceEmoticonsWithEmoji(SpannableStringBuilder html){ ImageSpan imgs[] = html.getSpans(0, html.length(), ImageSpan.class); for (ImageSpan img : imgs) { - String emoticon = Emoticons.lookupImageSmiley(img.getSource()); + String emoticon = EmoticonsUtils.lookupImageSmiley(img.getSource()); if (!emoticon.equals("")) { int start = html.getSpanStart(img); html.replace(start, html.getSpanEnd(img), emoticon); @@ -103,4 +103,4 @@ public static String replaceEmoticonsWithEmoji(final String text) { return text; } } -} \ No newline at end of file +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java index c10ce69c81e8..1fbfb3e56025 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java @@ -2,21 +2,83 @@ 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"; + } + } + } + /* - * see https://en.gravatar.com/site/implement/images/ - */ - public static String gravatarUrlFromEmail(final String email, int size) { - if (TextUtils.isEmpty(email)) + * 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 ""; + } - String url = "http://gravatar.com/avatar/" - + StringUtils.getMd5Hash(email) - + "?d=mm"; + // if this isn't a gravatar image, return as resized photon image url + if (!imageUrl.contains("gravatar.com")) { + return PhotonUtils.getPhotonImageUrl(imageUrl, avatarSz, avatarSz); + } - if (size > 0) - url += "&s=" + Integer.toString(size); + // remove all other params, then add query string for size and default image + return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=" + defaultImage.toString(); + } - return url; + 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/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java new file mode 100644 index 000000000000..9773d45d7629 --- /dev/null +++ b/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 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 entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + + return conn; + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java index c79fe0ecb079..b5319372a234 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java @@ -10,11 +10,17 @@ 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, + + /** + * 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)) { @@ -23,9 +29,11 @@ public static String stripHtml(final String text) { return Html.fromHtml(text).toString().trim(); } - /* - * this is much faster than stripHtml() but should only be used when we know the html is valid + /** + * 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)) { @@ -47,7 +55,7 @@ public static String fastStripHtml(String str) { } /* - * same as apache.commons.lang.StringUtils.stripStart() but also removes non-breaking + * Same as apache.commons.lang.StringUtils.stripStart() but also removes non-breaking * space (160) chars */ private static String trimStart(final String str) { @@ -62,8 +70,10 @@ private static String trimStart(final String str) { return str.substring(start); } - /* - * convert html entities to actual Unicode characters - relies on commons apache lang + /** + * 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("&")) { @@ -72,8 +82,11 @@ public static String fastUnescapeHtml(final String text) { return StringEscapeUtils.unescapeHtml(text); } - /* - * converts an R.color.xxx resource to an HTML hex color + /** + * 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 { @@ -83,12 +96,14 @@ public static String colorResToHtmlColor(Context context, int resId) { } } - /* - * remove blocks from the passed string - added to project after noticing + /** + * Remove {@code } blocks from the passed string - added to project after noticing * comments on posts that use the "Sociable" plugin ( http://wordpress.org/plugins/sociable/ ) - * may have a script block which contains followed by a CDATA section followed by , - * all of which will show up if we don't strip it here (example: http://cl.ly/image/0J0N3z3h1i04 ) - * first seen at http://houseofgeekery.com/2013/11/03/13-terrible-x-men-we-wont-see-in-the-movies/ + * 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 Wordpress Sociable Plugin + * @return String without {@code }, {@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) { @@ -111,7 +126,10 @@ public static String stripScript(final String text) { } /** - * an alternative to Html.fromHtml() supporting
    ,
      ,
      tags and replacing Emoticons with Emojis + * An alternative to Html.fromHtml() supporting {@code
        }, {@code
          }, {@code
          } + * tags and replacing EmoticonsUtils with Emojis + * @param source + * @param wpImageGetter */ public static SpannableStringBuilder fromHtml(String source, WPImageGetter wpImageGetter) { SpannableStringBuilder html; @@ -121,7 +139,7 @@ public static SpannableStringBuilder fromHtml(String source, WPImageGetter wpIma // In case our tag handler fails html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, null); } - Emoticons.replaceEmoticonsWithEmoji(html); + 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)); diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java index fdba9fe5aad7..e0335c0af57a 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java @@ -9,6 +9,7 @@ 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; @@ -18,6 +19,8 @@ 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; @@ -29,6 +32,7 @@ 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; @@ -41,13 +45,17 @@ public static int[] getImageSize(Uri uri, Context context){ if (uri.toString().contains("content:")) { String[] projection = new String[] { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA }; - Cursor cur = context.getContentResolver().query(uri, projection, null, null, null); - if (cur != null) { - if (cur.moveToFirst()) { + 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); } - cur.close(); + } catch (IllegalStateException stateException) { + Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + uri); + } finally { + SqlUtils.closeCursor(cur); } } @@ -390,6 +398,60 @@ public static Bitmap getScaledBitmapAtLongestSide(Bitmap bitmap, int targetSize) 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[] @@ -405,13 +467,17 @@ public static byte[] createThumbnailFromUri(Context context, String filePath = null; if (imageUri.toString().contains("content:")) { String[] projection = new String[] { MediaStore.Images.Media.DATA }; - Cursor cur = context.getContentResolver().query(imageUri, projection, null, null, null); - if (cur != null) { - if (cur.moveToFirst()) { + 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); } - cur.close(); + } catch (IllegalStateException stateException) { + Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + imageUri); + } finally { + SqlUtils.closeCursor(cur); } } @@ -557,4 +623,19 @@ public static Bitmap getRoundedEdgeBitmap(final Bitmap bitmap, int radius) { 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/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java b/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java similarity index 88% rename from WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java index f5644adbb606..4166a1f687cb 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtil.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java @@ -8,23 +8,30 @@ import org.wordpress.android.util.AppLog.T; import java.util.ArrayList; -import java.util.Iterator; -public class JSONUtil { - private static String QUERY_SEPERATOR="."; - private static String QUERY_ARRAY_INDEX_START="["; - private static String QUERY_ARRAY_INDEX_END="]"; - private static String QUERY_ARRAY_FIRST="first"; - private static String QUERY_ARRAY_LAST="last"; +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"; - private static final String TAG="JSONUtil"; /** * Given a JSONObject and a key path (e.g property.child) and a default it will * traverse the object graph and pull out the desired property */ public static U 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) { @@ -37,6 +44,8 @@ public static U queryJSON(JSONObject source, String query, U defaultObject) 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) { @@ -56,9 +65,6 @@ public static U queryJSON(JSONObject source, String query, U defaultObject) String nextQuery = query.substring(endQuery); String key = query.substring(0, endQuery); try { - if (source == null) { - return defaultObject; - } if (nextQuery.indexOf(QUERY_SEPERATOR) == 0) { return queryJSON(source.getJSONObject(key), nextQuery.substring(1), defaultObject); } else if (nextQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) { @@ -89,7 +95,15 @@ public static U queryJSON(JSONObject source, String query, U defaultObject) * 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 queryJSON(JSONArray source, String query, U defaultObject){ + public static 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); @@ -232,4 +246,4 @@ public static JSONObject getJSONChild(final JSONObject jsonParent, final String } return jsonChild; } -} \ No newline at end of file +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java b/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java deleted file mode 100644 index 12439fd28c87..000000000000 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/LocationHelper.java +++ /dev/null @@ -1,132 +0,0 @@ -//This Handy-Dandy class acquired and tweaked from http://stackoverflow.com/a/3145655/309558 -package org.wordpress.android.util; - -import java.util.Timer; -import java.util.TimerTask; - -import android.content.Context; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; - -public class LocationHelper { - Timer timer1; - LocationManager lm; - LocationResult locationResult; - boolean gps_enabled = false; - boolean network_enabled = false; - - public boolean getLocation(Context context, LocationResult result) { - locationResult = result; - if (lm == null) - lm = (LocationManager) context - .getSystemService(Context.LOCATION_SERVICE); - - // exceptions will be thrown if provider is not permitted. - try { - gps_enabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER); - } catch (Exception ex) { - } - try { - network_enabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER); - } catch (Exception ex) { - } - - // don't start listeners if no provider is enabled - if (!gps_enabled && !network_enabled) - return false; - - if (gps_enabled) - lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps); - - if (network_enabled) - lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork); - - timer1 = new Timer(); - timer1.schedule(new GetLastLocation(), 30000); - return true; - } - - LocationListener locationListenerGps = new LocationListener() { - public void onLocationChanged(Location location) { - timer1.cancel(); - locationResult.gotLocation(location); - lm.removeUpdates(this); - lm.removeUpdates(locationListenerNetwork); - } - - public void onProviderDisabled(String provider) { - } - - public void onProviderEnabled(String provider) { - } - - public void onStatusChanged(String provider, int status, Bundle extras) { - } - }; - - LocationListener locationListenerNetwork = new LocationListener() { - public void onLocationChanged(Location location) { - timer1.cancel(); - locationResult.gotLocation(location); - lm.removeUpdates(this); - lm.removeUpdates(locationListenerGps); - } - - public void onProviderDisabled(String provider) { - } - - public void onProviderEnabled(String provider) { - } - - public void onStatusChanged(String provider, int status, Bundle extras) { - } - }; - - class GetLastLocation extends TimerTask { - @Override - public void run() { - lm.removeUpdates(locationListenerGps); - lm.removeUpdates(locationListenerNetwork); - - Location net_loc = null, gps_loc = null; - if (gps_enabled) - gps_loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); - if (network_enabled) - net_loc = lm - .getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - - // if there are both values use the latest one - if (gps_loc != null && net_loc != null) { - if (gps_loc.getTime() > net_loc.getTime()) - locationResult.gotLocation(gps_loc); - else - locationResult.gotLocation(net_loc); - return; - } - - if (gps_loc != null) { - locationResult.gotLocation(gps_loc); - return; - } - if (net_loc != null) { - locationResult.gotLocation(net_loc); - return; - } - locationResult.gotLocation(null); - } - } - - public static abstract class LocationResult { - public abstract void gotLocation(Location location); - } - - public void cancelTimer() { - if (timer1 != null) { - timer1.cancel(); - lm.removeUpdates(locationListenerGps); - lm.removeUpdates(locationListenerNetwork); - } - } -} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java new file mode 100644 index 000000000000..cea4ce917bea --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java @@ -0,0 +1,307 @@ +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 { + public static boolean isValidImage(String url) { + if (url == null) { + return false; + } + + return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".gif"); + } + + public static boolean isDocument(String url) { + if (url == null) { + return false; + } + + return url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".odt") || url.endsWith(".pdf"); + } + + public static boolean isPowerpoint(String url) { + if (url == null) { + return false; + } + + 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; + } + + return url.endsWith(".xls") || url.endsWith(".xlsx"); + } + + public static boolean isVideo(String url) { + if (url == null) { + return false; + } + 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; + } + 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)); + } + + // Calculate the minimun width between the blog setting and picture real width + public static int getMinimumImageWidth(Context context, Uri curStream, String imageWidthBlogSettingString) { + int imageWidthBlogSetting = Integer.MAX_VALUE; + + if (!imageWidthBlogSettingString.equals("Original Size")) { + try { + imageWidthBlogSetting = Integer.valueOf(imageWidthBlogSettingString); + } catch (NumberFormatException e) { + AppLog.e(T.POSTS, e); + } + } + + int[] dimensions = ImageUtils.getImageSize(curStream, context); + int imageWidthPictureSetting = dimensions[0] == 0 ? Integer.MAX_VALUE : dimensions[0]; + + if (Math.min(imageWidthPictureSetting, imageWidthBlogSetting) == Integer.MAX_VALUE) { + // Default value in case of errors reading the picture size and the blog settings is set to Original size + return 1024; + } else { + return Math.min(imageWidthPictureSetting, imageWidthBlogSetting); + } + } + + 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/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java index 1ea1a60f2d77..240c93f50e78 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java @@ -77,6 +77,9 @@ public static boolean isAirplaneModeOn(Context context) { * and returns false */ public static boolean checkConnection(Context context) { + if (context == null) { + return false; + } if (isNetworkAvailable(context)) { return true; } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java new file mode 100644 index 000000000000..24c4fbdd7360 --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java @@ -0,0 +1,52 @@ +package org.wordpress.android.util; + +import android.Manifest.permission; +import android.app.Activity; +import android.content.pm.PackageManager; +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 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; + } + + public static boolean checkAndRequestCameraAndStoragePermissions(Activity activity, int requestCode) { + return checkAndRequestPermissions(activity, 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 checkLocationPermissions(Activity activity, int requestCode) { + return checkAndRequestPermissions(activity, requestCode, new String[]{ + permission.ACCESS_FINE_LOCATION, + permission.ACCESS_COARSE_LOCATION + }); + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java index 7de378b64e78..a9975cc341ab 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java @@ -7,27 +7,11 @@ * http://developer.wordpress.com/docs/photon/ */ public class PhotonUtils { + private PhotonUtils() { throw new AssertionError(); } - /* - * gravatars often contain the ?s= parameter which determines their size - detect this and - * replace it with a new ?s= parameter which requests the avatar at the exact size needed - */ - public static String fixAvatar(final String imageUrl, int avatarSz) { - if (TextUtils.isEmpty(imageUrl)) - return ""; - - // if this isn't a gravatar image, return as resized photon image url - if (!imageUrl.contains("gravatar.com")) { - return getPhotonImageUrl(imageUrl, avatarSz, avatarSz); - } - - // remove all other params, then add query string for size and "mystery man" default - return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=mm"; - } - /* * returns true if the passed url is an obvious "mshots" url */ @@ -39,7 +23,7 @@ public static boolean isMshotsUrl(final String imageUrl) { * 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 static enum Quality { + public enum Quality { HIGH, MEDIUM, LOW @@ -61,42 +45,34 @@ public static String getPhotonImageUrl(String imageUrl, int width, int height, Q // remove existing query string since it may contain params that conflict with the passed ones imageUrl = UrlUtils.removeQuery(imageUrl); - // don't use with GIFs - photon breaks animated GIFs, and sometimes returns a GIF that - // can't be read by BitmapFactory.decodeByteArray (used by Volley in ImageRequest.java - // to decode the downloaded image) - if (imageUrl.endsWith(".gif")) { - return imageUrl; - } - // if this is an "mshots" url, skip photon and return it with a query that sets the width/height if (isMshotsUrl(imageUrl)) { return imageUrl + "?w=" + width + "&h=" + height; } - final String qualityParam; + // strip=all removes EXIF and other non-visual data from JPEGs + String query = "?strip=all"; + switch (quality) { case HIGH: - qualityParam = "quality=100"; + query += "&quality=100"; break; case LOW: - qualityParam = "quality=35"; + query += "&quality=35"; break; default: // medium - qualityParam = "quality=65"; + 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 - final String query; if (width > 0 && height > 0) { - query = "?resize=" + width + "," + height + "&" + qualityParam; + query += "&resize=" + width + "," + height; } else if (width > 0) { - query = "?w=" + width + "&" + qualityParam; + query += "&w=" + width; } else if (height > 0) { - query = "?h=" + height + "&" + qualityParam; - } else { - query = "?" + qualityParam; + query += "&h=" + height; } // return passed url+query if it's already a photon url @@ -105,6 +81,12 @@ public static String getPhotonImageUrl(String imageUrl, int width, int height, Q 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; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java index 991c7680b478..4660a3500b25 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java @@ -28,6 +28,10 @@ public static void dump() { getInstance().dumpToLog(); } + public static void stop() { + getInstance().reset(null); + } + private static ProfilingUtils getInstance() { if (sInstance == null) { sInstance = new ProfilingUtils(); @@ -56,12 +60,18 @@ public void reset() { } 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; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java new file mode 100644 index 000000000000..6bcfde06b892 --- /dev/null +++ b/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/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java new file mode 100644 index 000000000000..09480f156364 --- /dev/null +++ b/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/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java index 8d1b4b4379c9..38b4b74a921a 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java @@ -7,6 +7,8 @@ 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; @@ -116,6 +118,25 @@ public static boolean dropAllTables(SQLiteDatabase db) throws SQLiteException { 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/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java index eca31ffd169d..25ddd2a41226 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java @@ -180,6 +180,14 @@ public static String capitalize(final String 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/ @@ -189,24 +197,6 @@ public static String getPhotonUrl(String imageUrl, int size) { return "http://i0.wp.com/" + imageUrl + "?w=" + size; } - public static String getHost(String url) { - if (TextUtils.isEmpty(url)) { - return ""; - } - - int doubleslash = url.indexOf("//"); - if (doubleslash == -1) { - doubleslash = 0; - } else { - doubleslash += 2; - } - - int end = url.indexOf('/', doubleslash); - end = (end >= 0) ? end : url.length(); - - return url.substring(doubleslash, end); - } - public static String replaceUnicodeSurrogateBlocksWithHTMLEntities(final String inputString) { final int length = inputString.length(); StringBuilder out = new StringBuilder(); // Used to hold the output. @@ -214,8 +204,8 @@ public static String replaceUnicodeSurrogateBlocksWithHTMLEntities(final String final int codepoint = inputString.codePointAt(offset); final char current = inputString.charAt(offset); if (Character.isHighSurrogate(current) || Character.isLowSurrogate(current)) { - if (Emoticons.wpSmiliesCodePointToText.get(codepoint) != null) { - out.append(Emoticons.wpSmiliesCodePointToText.get(codepoint)); + if (EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint) != null) { + out.append(EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint)); } else { final String htmlEscapedChar = "&#x" + Integer.toHexString(codepoint) + ";"; out.append(htmlEscapedChar); @@ -275,4 +265,17 @@ public static int stringToInt(String s, int defaultValue) { return defaultValue; } } -} \ No newline at end of file + + 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; + } + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java index 4438b8950158..6843a87fb676 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java @@ -1,15 +1,21 @@ 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) { @@ -28,12 +34,18 @@ public static String urlDecode(final String text) { } } - public static String getDomainFromUrl(final String urlString) { - if (urlString == null) { - return ""; + /** + * @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(); + } } - Uri uri = Uri.parse(urlString); - return uri.getHost(); + return ""; } /** @@ -52,11 +64,40 @@ public static String convertUrlToPunycodeIfNeeded(String 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 isHTTPS true will make the url starts with https;// + * @return transformed url prefixed by its http;// or https;// scheme + */ public static String addUrlSchemeIfNeeded(String url, boolean isHTTPS) { 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, (isHTTPS ? "https" : "http") + "://"); + if (!URLUtil.isValidUrl(url)) { if (!(url.toLowerCase().startsWith("http://")) && !(url.toLowerCase().startsWith("https://"))) { url = (isHTTPS ? "https" : "http") + "://" + url; @@ -78,7 +119,8 @@ public static String normalizeUrl(final String urlString) { // 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")) { + 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); @@ -95,6 +137,25 @@ public static String normalizeUrl(final String 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 */ @@ -102,11 +163,7 @@ public static String removeQuery(final String urlString) { if (urlString == null) { return null; } - int pos = urlString.indexOf("?"); - if (pos == -1) { - return urlString; - } - return urlString.substring(0, pos); + return Uri.parse(urlString).buildUpon().clearQuery().toString(); } /** @@ -116,6 +173,17 @@ 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 */ @@ -162,4 +230,28 @@ public static boolean isValidUrlAndHostNotNull(String url) { } 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 parameters = new HashMap<>(); + parameters.put(paramName, paramValue); + return appendUrlParameters(url, parameters); + } + + public static String appendUrlParameters(String url, Map parameters) { + Uri.Builder uriBuilder = Uri.parse(url).buildUpon(); + for (Map.Entry parameter : parameters.entrySet()) { + uriBuilder.appendQueryParameter(parameter.getKey(), parameter.getValue()); + } + return uriBuilder.build().toString(); + } } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java b/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java similarity index 88% rename from WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java index dae02b4f01b3..3859605589be 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmail.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java @@ -5,9 +5,11 @@ import android.content.Context; import android.util.Patterns; +import org.wordpress.android.util.AppLog.T; + import java.util.regex.Pattern; -public class UserEmail { +public class UserEmailUtils { /** * Get primary account and return its name if it matches the email address pattern. * @@ -29,6 +31,7 @@ public static String getPrimaryEmail(Context context) { 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/WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java similarity index 98% rename from WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java index e111a626fc72..914373c8f2e1 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ListScrollPositionManager.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java @@ -1,4 +1,4 @@ -package org.wordpress.android.util; +package org.wordpress.android.util.helpers; import android.content.Context; import android.content.SharedPreferences; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java new file mode 100644 index 000000000000..ff472c2ed429 --- /dev/null +++ b/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/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java new file mode 100644 index 000000000000..b14aa90daf28 --- /dev/null +++ b/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("\"image\"", + fullSizeUrl, mediaTitle, alignmentCSS, resizedPictureURL); + + if (!TextUtils.isEmpty(getCaption())) { + content = String.format("[caption id=\"\" align=\"%s\" width=\"%d\" caption=\"%s\"]%s[/caption]", + alignment, getWidth(), TextUtils.htmlEncode(getCaption()), content); + } + + return content; + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java new file mode 100644 index 000000000000..ab7326a170e6 --- /dev/null +++ b/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 ids; + + public MediaGallery(boolean isRandom, String type, int numColumns, ArrayList 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(); + 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 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 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/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java new file mode 100644 index 000000000000..588b98141c27 --- /dev/null +++ b/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/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/SwipeToRefreshHelper.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java similarity index 57% rename from WordPressUtils/src/main/java/org/wordpress/android/util/ptr/SwipeToRefreshHelper.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java index 7488c2c85c78..b6eb6c6c395d 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ptr/SwipeToRefreshHelper.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java @@ -1,39 +1,53 @@ -package org.wordpress.android.util.ptr; +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; 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 SwipeRefreshLayout mSwipeRefreshLayout; + private CustomSwipeRefreshLayout mSwipeRefreshLayout; private RefreshListener mRefreshListener; + private boolean mRefreshing; public interface RefreshListener { public void onRefreshStarted(); } - public SwipeToRefreshHelper(Activity activity, SwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) { + public SwipeToRefreshHelper(Activity activity, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) { init(activity, swipeRefreshLayout, listener); } - public void init(Activity activity, SwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) { + public void init(Activity activity, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) { mRefreshListener = listener; mSwipeRefreshLayout = swipeRefreshLayout; mSwipeRefreshLayout.setOnRefreshListener(this); final TypedArray styleAttrs = obtainStyledAttrsFromThemeAttr(activity, R.attr.swipeToRefreshStyle, R.styleable.RefreshIndicator); - int color = styleAttrs.getColor(R.styleable.RefreshIndicator_refreshIndicatorColor, - android.R.color.holo_blue_dark); + int color = styleAttrs.getColor(R.styleable.RefreshIndicator_refreshIndicatorColor, activity.getResources() + .getColor(android.R.color.holo_blue_dark)); mSwipeRefreshLayout.setColorSchemeColors(color, color, color, color); } public void setRefreshing(boolean refreshing) { - mSwipeRefreshLayout.setRefreshing(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() { diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/Version.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java similarity index 96% rename from WordPressUtils/src/main/java/org/wordpress/android/util/Version.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java index 6e695db454da..b35f84757e82 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/Version.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java @@ -1,4 +1,4 @@ -package org.wordpress.android.util; +package org.wordpress.android.util.helpers; //See: http://stackoverflow.com/a/11024200 public class Version implements Comparable { @@ -44,4 +44,4 @@ public Version(String version) { return false; return this.compareTo((Version) that) == 0; } -} \ No newline at end of file +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java similarity index 97% rename from WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java index fa96a998a23c..da333b24e993 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/WPHtmlTagHandler.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java @@ -1,4 +1,4 @@ -package org.wordpress.android.util; +package org.wordpress.android.util.helpers; import android.text.Editable; import android.text.Html; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java similarity index 97% rename from WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java index 9705b0c25698..b03d740457e8 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/WPImageGetter.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java @@ -1,4 +1,4 @@ -package org.wordpress.android.util; +package org.wordpress.android.util.helpers; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; @@ -10,7 +10,9 @@ 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; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java new file mode 100644 index 000000000000..fa0a0b4aa98b --- /dev/null +++ b/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 CREATOR + = new Parcelable.Creator() { + 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/WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java similarity index 96% rename from WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java index 37d5dfe6dee2..33cdc009364a 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/WPQuoteSpan.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java @@ -1,4 +1,4 @@ -package org.wordpress.android.util; +package org.wordpress.android.util.helpers; import android.graphics.Canvas; import android.graphics.Paint; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java new file mode 100644 index 000000000000..4b6805ccfd2e --- /dev/null +++ b/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/WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java similarity index 83% rename from WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java index 6d640fc3f27a..1418e79ea295 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/WPWebChromeClient.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java @@ -1,6 +1,7 @@ -package org.wordpress.android.util; +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; @@ -26,7 +27,10 @@ public WPWebChromeClient(Activity activity, } public void onProgressChanged(WebView webView, int progress) { - if (mActivity != null && !mActivity.isFinishing() && mAutoUpdateActivityTitle) { + if (mActivity != null + && !mActivity.isFinishing() + && mAutoUpdateActivityTitle + && !TextUtils.isEmpty(webView.getTitle())) { mActivity.setTitle(webView.getTitle()); } if (mProgressBar != null) { @@ -38,4 +42,4 @@ public void onProgressChanged(WebView webView, int progress) { } } } -} \ No newline at end of file +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AutoResizeTextView.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java similarity index 99% rename from WordPressUtils/src/main/java/org/wordpress/android/util/AutoResizeTextView.java rename to WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java index 5f55f6058a97..b0b4dc017545 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AutoResizeTextView.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java @@ -1,4 +1,4 @@ -package org.wordpress.android.util; +package org.wordpress.android.util.widgets; import android.content.Context; import android.text.Layout; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java new file mode 100644 index 000000000000..356268922dc1 --- /dev/null +++ b/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/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java new file mode 100644 index 000000000000..ae83d6821871 --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java @@ -0,0 +1,57 @@ +package org.wordpress.android.util.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +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(); + } +}