diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java')
-rw-r--r-- | WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java new file mode 100644 index 000000000..15467db20 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java @@ -0,0 +1,558 @@ +package org.wordpress.android.ui.stats; + +import android.annotation.SuppressLint; +import android.content.Context; + +import com.android.volley.NetworkResponse; +import com.android.volley.VolleyError; + +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.ui.WPWebViewActivity; +import org.wordpress.android.ui.reader.ReaderActivityLauncher; +import org.wordpress.android.ui.stats.exceptions.StatsError; +import org.wordpress.android.ui.stats.models.AuthorsModel; +import org.wordpress.android.ui.stats.models.BaseStatsModel; +import org.wordpress.android.ui.stats.models.ClicksModel; +import org.wordpress.android.ui.stats.models.CommentFollowersModel; +import org.wordpress.android.ui.stats.models.CommentsModel; +import org.wordpress.android.ui.stats.models.FollowersModel; +import org.wordpress.android.ui.stats.models.GeoviewsModel; +import org.wordpress.android.ui.stats.models.InsightsAllTimeModel; +import org.wordpress.android.ui.stats.models.InsightsLatestPostDetailsModel; +import org.wordpress.android.ui.stats.models.InsightsLatestPostModel; +import org.wordpress.android.ui.stats.models.InsightsPopularModel; +import org.wordpress.android.ui.stats.models.InsightsTodayModel; +import org.wordpress.android.ui.stats.models.PostModel; +import org.wordpress.android.ui.stats.models.PublicizeModel; +import org.wordpress.android.ui.stats.models.ReferrersModel; +import org.wordpress.android.ui.stats.models.SearchTermsModel; +import org.wordpress.android.ui.stats.models.TagsContainerModel; +import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel; +import org.wordpress.android.ui.stats.models.VideoPlaysModel; +import org.wordpress.android.ui.stats.models.VisitsModel; +import org.wordpress.android.ui.stats.service.StatsService; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; + +import java.io.Serializable; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +public class StatsUtils { + @SuppressLint("SimpleDateFormat") + private static long toMs(String date, String pattern) { + if (date == null || date.equals("null")) { + AppLog.w(T.UTILS, "Trying to parse a 'null' Stats Date."); + return -1; + } + + if (pattern == null) { + AppLog.w(T.UTILS, "Trying to parse a Stats date with a null pattern"); + return -1; + } + + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + try { + return sdf.parse(date).getTime(); + } catch (ParseException e) { + AppLog.e(T.UTILS, e); + } + return -1; + } + + /** + * Converts date in the form of 2013-07-18 to ms * + */ + public static long toMs(String date) { + return toMs(date, StatsConstants.STATS_INPUT_DATE_FORMAT); + } + + public static String msToString(long ms, String format) { + SimpleDateFormat sdf = new SimpleDateFormat(format); + return sdf.format(new Date(ms)); + } + + /** + * Get the current date of the blog in the form of yyyy-MM-dd (EX: 2013-07-18) * + */ + public static String getCurrentDateTZ(int localTableBlogID) { + String timezone = StatsUtils.getBlogTimezone(WordPress.getBlog(localTableBlogID)); + if (timezone == null) { + AppLog.w(T.UTILS, "Timezone is null. Returning the device time!!"); + return getCurrentDate(); + } + + return getCurrentDateTimeTZ(timezone, StatsConstants.STATS_INPUT_DATE_FORMAT); + } + + /** + * Get the current datetime of the blog * + */ + public static String getCurrentDateTimeTZ(int localTableBlogID) { + String timezone = StatsUtils.getBlogTimezone(WordPress.getBlog(localTableBlogID)); + if (timezone == null) { + AppLog.w(T.UTILS, "Timezone is null. Returning the device time!!"); + return getCurrentDatetime(); + } + String pattern = "yyyy-MM-dd HH:mm:ss"; // precision to seconds + return getCurrentDateTimeTZ(timezone, pattern); + } + + /** + * Get the current datetime of the blog in Ms * + */ + public static long getCurrentDateTimeMsTZ(int localTableBlogID) { + String timezone = StatsUtils.getBlogTimezone(WordPress.getBlog(localTableBlogID)); + if (timezone == null) { + AppLog.w(T.UTILS, "Timezone is null. Returning the device time!!"); + return new Date().getTime(); + } + String pattern = "yyyy-MM-dd HH:mm:ss"; // precision to seconds + return toMs(getCurrentDateTimeTZ(timezone, pattern), pattern); + } + + /** + * Get the current date in the form of yyyy-MM-dd (EX: 2013-07-18) * + */ + public static String getCurrentDate() { + SimpleDateFormat sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT); + return sdf.format(new Date()); + } + + /** + * Get the current date in the form of "yyyy-MM-dd HH:mm:ss" + */ + private static String getCurrentDatetime() { + String pattern = "yyyy-MM-dd HH:mm:ss"; // precision to seconds + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + return sdf.format(new Date()); + } + + private static String getBlogTimezone(Blog blog) { + if (blog == null) { + AppLog.w(T.UTILS, "Blog object is null!! Can't read timezone opt then."); + return null; + } + + JSONObject jsonOptions = blog.getBlogOptionsJSONObject(); + String timezone = null; + if (jsonOptions != null && jsonOptions.has("time_zone")) { + try { + timezone = jsonOptions.getJSONObject("time_zone").getString("value"); + } catch (JSONException e) { + AppLog.e(T.UTILS, "Cannot load time_zone from options: " + jsonOptions, e); + } + } else { + AppLog.w(T.UTILS, "Blog options are null, or doesn't contain time_zone"); + } + return timezone; + } + + private static String getCurrentDateTimeTZ(String blogTimeZoneOption, String pattern) { + Date date = new Date(); + SimpleDateFormat gmtDf = new SimpleDateFormat(pattern); + + if (blogTimeZoneOption == null) { + AppLog.w(T.UTILS, "blogTimeZoneOption is null. getCurrentDateTZ() will return the device time!"); + return gmtDf.format(date); + } + + /* + Convert the timezone to a form that is compatible with Java TimeZone class + WordPress returns something like the following: + UTC+0:30 ----> 0.5 + UTC+1 ----> 1.0 + UTC-0:30 ----> -1.0 + */ + + AppLog.v(T.STATS, "Parsing the following Timezone received from WP: " + blogTimeZoneOption); + String timezoneNormalized; + if (blogTimeZoneOption.equals("0") || blogTimeZoneOption.equals("0.0")) { + timezoneNormalized = "GMT"; + } else { + String[] timezoneSplitted = org.apache.commons.lang.StringUtils.split(blogTimeZoneOption, "."); + timezoneNormalized = timezoneSplitted[0]; + if(timezoneSplitted.length > 1 && timezoneSplitted[1].equals("5")){ + timezoneNormalized += ":30"; + } + if (timezoneNormalized.startsWith("-")) { + timezoneNormalized = "GMT" + timezoneNormalized; + } else { + if (timezoneNormalized.startsWith("+")) { + timezoneNormalized = "GMT" + timezoneNormalized; + } else { + timezoneNormalized = "GMT+" + timezoneNormalized; + } + } + } + + AppLog.v(T.STATS, "Setting the following Timezone: " + timezoneNormalized); + gmtDf.setTimeZone(TimeZone.getTimeZone(timezoneNormalized)); + return gmtDf.format(date); + } + + public static String parseDate(String timestamp, String fromFormat, String toFormat) { + SimpleDateFormat from = new SimpleDateFormat(fromFormat); + SimpleDateFormat to = new SimpleDateFormat(toFormat); + try { + Date date = from.parse(timestamp); + return to.format(date); + } catch (ParseException e) { + AppLog.e(T.STATS, e); + } + return ""; + } + + /** + * Get a diff between two dates + * @param date1 the oldest date in Ms + * @param date2 the newest date in Ms + * @param timeUnit the unit in which you want the diff + * @return the diff value, in the provided unit + */ + public static long getDateDiff(Date date1, Date date2, TimeUnit timeUnit) { + long diffInMillies = date2.getTime() - date1.getTime(); + return timeUnit.convert(diffInMillies, TimeUnit.MILLISECONDS); + } + + + //Calculate the correct start/end date for the selected period + public static String getPublishedEndpointPeriodDateParameters(StatsTimeframe timeframe, String date) { + if (date == null) { + AppLog.w(AppLog.T.STATS, "Can't calculate start and end period without a reference date"); + return null; + } + + try { + SimpleDateFormat sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT); + Calendar c = Calendar.getInstance(); + c.setFirstDayOfWeek(Calendar.MONDAY); + Date parsedDate = sdf.parse(date); + c.setTime(parsedDate); + + + final String after; + final String before; + switch (timeframe) { + case DAY: + after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + c.add(Calendar.DAY_OF_YEAR, +1); + before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + break; + case WEEK: + c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + c.add(Calendar.DAY_OF_YEAR, +1); + before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + break; + case MONTH: + //first day of the next month + c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH)); + c.add(Calendar.DAY_OF_YEAR, +1); + before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + + //last day of the prev month + c.setTime(parsedDate); + c.set(Calendar.DAY_OF_MONTH, c.getActualMinimum(Calendar.DAY_OF_MONTH)); + after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + break; + case YEAR: + //first day of the next year + c.set(Calendar.MONTH, Calendar.DECEMBER); + c.set(Calendar.DAY_OF_MONTH, 31); + c.add(Calendar.DAY_OF_YEAR, +1); + before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + + c.setTime(parsedDate); + c.set(Calendar.MONTH, Calendar.JANUARY); + c.set(Calendar.DAY_OF_MONTH, 1); + after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT); + break; + default: + AppLog.w(AppLog.T.STATS, "Can't calculate start and end period without a reference timeframe"); + return null; + } + return "&after=" + after + "&before=" + before; + } catch (ParseException e) { + AppLog.e(AppLog.T.UTILS, e); + return null; + } + } + + public static int getSmallestWidthDP() { + return WordPress.getContext().getResources().getInteger(R.integer.smallest_width_dp); + } + + public static int getLocalBlogIdFromRemoteBlogId(int remoteBlogID) { + // workaround: There are 2 entries in the DB for each Jetpack blog linked with + // the current wpcom account. We need to load the correct localID here, otherwise options are + // blank + int localId = WordPress.wpDB.getLocalTableBlogIdForJetpackRemoteID( + remoteBlogID, + null); + if (localId == 0) { + localId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId( + remoteBlogID + ); + } + + return localId; + } + + public static synchronized void logVolleyErrorDetails(final VolleyError volleyError) { + if (volleyError == null) { + AppLog.e(T.STATS, "Tried to log a VolleyError, but the error obj was null!"); + return; + } + if (volleyError.networkResponse != null) { + NetworkResponse networkResponse = volleyError.networkResponse; + AppLog.e(T.STATS, "Network status code: " + networkResponse.statusCode); + if (networkResponse.data != null) { + AppLog.e(T.STATS, "Network data: " + new String(networkResponse.data)); + } + } + AppLog.e(T.STATS, "Volley Error Message: " + volleyError.getMessage(), volleyError); + } + + public static synchronized boolean isRESTDisabledError(final Serializable error) { + if (error == null || !(error instanceof com.android.volley.AuthFailureError)) { + return false; + } + com.android.volley.AuthFailureError volleyError = (com.android.volley.AuthFailureError) error; + if (volleyError.networkResponse != null && volleyError.networkResponse.data != null) { + String errorMessage = new String(volleyError.networkResponse.data).toLowerCase(); + return errorMessage.contains("api calls") && errorMessage.contains("disabled"); + } else { + AppLog.e(T.STATS, "Network response is null in Volley. Can't check if it is a Rest Disabled error."); + return false; + } + } + + public static synchronized BaseStatsModel parseResponse(StatsService.StatsEndpointsEnum endpointName, String blogID, JSONObject response) + throws JSONException { + BaseStatsModel model = null; + switch (endpointName) { + case VISITS: + model = new VisitsModel(blogID, response); + break; + case TOP_POSTS: + model = new TopPostsAndPagesModel(blogID, response); + break; + case REFERRERS: + model = new ReferrersModel(blogID, response); + break; + case CLICKS: + model = new ClicksModel(blogID, response); + break; + case GEO_VIEWS: + model = new GeoviewsModel(blogID, response); + break; + case AUTHORS: + model = new AuthorsModel(blogID, response); + break; + case VIDEO_PLAYS: + model = new VideoPlaysModel(blogID, response); + break; + case COMMENTS: + model = new CommentsModel(blogID, response); + break; + case FOLLOWERS_WPCOM: + model = new FollowersModel(blogID, response); + break; + case FOLLOWERS_EMAIL: + model = new FollowersModel(blogID, response); + break; + case COMMENT_FOLLOWERS: + model = new CommentFollowersModel(blogID, response); + break; + case TAGS_AND_CATEGORIES: + model = new TagsContainerModel(blogID, response); + break; + case PUBLICIZE: + model = new PublicizeModel(blogID, response); + break; + case SEARCH_TERMS: + model = new SearchTermsModel(blogID, response); + break; + case INSIGHTS_ALL_TIME: + model = new InsightsAllTimeModel(blogID, response); + break; + case INSIGHTS_POPULAR: + model = new InsightsPopularModel(blogID, response); + break; + case INSIGHTS_TODAY: + model = new InsightsTodayModel(blogID, response); + break; + case INSIGHTS_LATEST_POST_SUMMARY: + model = new InsightsLatestPostModel(blogID, response); + break; + case INSIGHTS_LATEST_POST_VIEWS: + model = new InsightsLatestPostDetailsModel(blogID, response); + break; + } + return model; + } + + public static void openPostInReaderOrInAppWebview(Context ctx, final String remoteBlogID, + final String remoteItemID, + final String itemType, + final String itemURL) { + final long blogID = Long.parseLong(remoteBlogID); + final long itemID = Long.parseLong(remoteItemID); + if (itemType == null) { + // If we don't know the type of the item, open it with the browser. + AppLog.d(AppLog.T.UTILS, "Type of the item is null. Opening it in the in-app browser: " + itemURL); + WPWebViewActivity.openURL(ctx, itemURL); + } else if (itemType.equals(StatsConstants.ITEM_TYPE_POST) + || itemType.equals(StatsConstants.ITEM_TYPE_PAGE)) { + // If the post/page has ID == 0 is the home page, and we need to load the blog preview, + // otherwise 404 is returned if we try to show the post in the reader + if (itemID == 0) { + ReaderActivityLauncher.showReaderBlogPreview( + ctx, + blogID + ); + } else { + ReaderActivityLauncher.showReaderPostDetail( + ctx, + blogID, + itemID + ); + } + } else if (itemType.equals(StatsConstants.ITEM_TYPE_HOME_PAGE)) { + ReaderActivityLauncher.showReaderBlogPreview( + ctx, + blogID + ); + } else { + AppLog.d(AppLog.T.UTILS, "Opening the in-app browser: " + itemURL); + WPWebViewActivity.openURL(ctx, itemURL); + } + } + + public static void openPostInReaderOrInAppWebview(Context ctx, final PostModel post) { + final String postType = post.getPostType(); + final String url = post.getUrl(); + final String blogID = post.getBlogID(); + final String itemID = post.getItemID(); + openPostInReaderOrInAppWebview(ctx, blogID, itemID, postType, url); + } + + /* + * This function rewrites a VolleyError into a simple Stats Error by getting the error message. + * This is a FIX for https://github.com/wordpress-mobile/WordPress-Android/issues/2228 where + * VolleyErrors cannot be serializable. + */ + public static StatsError rewriteVolleyError(VolleyError volleyError, String defaultErrorString) { + if (volleyError != null && volleyError.getMessage() != null) { + return new StatsError(volleyError.getMessage()); + } + + if (defaultErrorString != null) { + return new StatsError(defaultErrorString); + } + + // Error string should be localized here, but don't want to pass a context + return new StatsError("Stats couldn't be refreshed at this time"); + } + + + private static int roundUp(double num, double divisor) { + double unrounded = num / divisor; + //return (int) Math.ceil(unrounded); + return (int) (unrounded + 0.5); + } + + public static String getSinceLabel(Context ctx, String dataSubscribed) { + + Date currentDateTime = new Date(); + + try { + SimpleDateFormat from = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + Date date = from.parse(dataSubscribed); + + // See http://momentjs.com/docs/#/displaying/fromnow/ + long currentDifference = Math.abs( + StatsUtils.getDateDiff(date, currentDateTime, TimeUnit.SECONDS) + ); + + if (currentDifference <= 45 ) { + return ctx.getString(R.string.stats_followers_seconds_ago); + } + if (currentDifference < 90 ) { + return ctx.getString(R.string.stats_followers_a_minute_ago); + } + + // 90 seconds to 45 minutes + if (currentDifference <= 2700 ) { + long minutes = StatsUtils.roundUp(currentDifference, 60); + String followersMinutes = ctx.getString(R.string.stats_followers_minutes); + return String.format(followersMinutes, minutes); + } + + // 45 to 90 minutes + if (currentDifference <= 5400 ) { + return ctx.getString(R.string.stats_followers_an_hour_ago); + } + + // 90 minutes to 22 hours + if (currentDifference <= 79200 ) { + long hours = StatsUtils.roundUp(currentDifference, 60 * 60); + String followersHours = ctx.getString(R.string.stats_followers_hours); + return String.format(followersHours, hours); + } + + // 22 to 36 hours + if (currentDifference <= 129600 ) { + return ctx.getString(R.string.stats_followers_a_day); + } + + // 36 hours to 25 days + // 86400 secs in a day - 2160000 secs in 25 days + if (currentDifference <= 2160000 ) { + long days = StatsUtils.roundUp(currentDifference, 86400); + String followersDays = ctx.getString(R.string.stats_followers_days); + return String.format(followersDays, days); + } + + // 25 to 45 days + // 3888000 secs in 45 days + if (currentDifference <= 3888000 ) { + return ctx.getString(R.string.stats_followers_a_month); + } + + // 45 to 345 days + // 2678400 secs in a month - 29808000 secs in 345 days + if (currentDifference <= 29808000 ) { + long months = StatsUtils.roundUp(currentDifference, 2678400); + String followersMonths = ctx.getString(R.string.stats_followers_months); + return String.format(followersMonths, months); + } + + // 345 to 547 days (1.5 years) + if (currentDifference <= 47260800 ) { + return ctx.getString(R.string.stats_followers_a_year); + } + + // 548 days+ + // 31536000 secs in a year + long years = StatsUtils.roundUp(currentDifference, 31536000); + String followersYears = ctx.getString(R.string.stats_followers_years); + return String.format(followersYears, years); + + } catch (ParseException e) { + AppLog.e(AppLog.T.STATS, e); + } + + return ""; + } +} |