aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/stats
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/stats')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java118
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java159
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java361
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java85
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java297
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java1034
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java282
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java335
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java266
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java280
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java21
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java276
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java449
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java299
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java99
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java280
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java149
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java200
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java238
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java340
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java19
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java238
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java907
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java282
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java39
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java129
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java344
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java558
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java170
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java318
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java170
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java25
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java846
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java79
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java161
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java300
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java541
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java15
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java55
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java130
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java226
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java9
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java119
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java84
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java7
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java113
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java107
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java77
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java76
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java47
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java96
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java23
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java30
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java370
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java40
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java124
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java116
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java108
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java31
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java35
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java93
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java136
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java614
79 files changed, 14585 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java
new file mode 100644
index 000000000..cb6c20cd5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java
@@ -0,0 +1,118 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.PopupMenu;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+import java.lang.ref.WeakReference;
+
+class FollowHelper {
+
+ private final WeakReference<Activity> mActivityRef;
+
+ public FollowHelper(Activity activity) {
+ mActivityRef = new WeakReference<>(activity);
+ }
+
+
+ public void showPopup(View anchor, final FollowDataModel followData) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ final String workingText = followData.getFollowingText();
+ final String followText = followData.getFollowText();
+ final String unfollowText = followData.getFollowingHoverText();
+
+ final PopupMenu popup = new PopupMenu(mActivityRef.get(), anchor);
+ final MenuItem menuItem;
+
+ if (followData.isRestCallInProgress) {
+ menuItem = popup.getMenu().add(workingText);
+ } else {
+ menuItem = popup.getMenu().add(followData.isFollowing() ? unfollowText : followText);
+ }
+
+ menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ item.setTitle(workingText);
+ item.setOnMenuItemClickListener(null);
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtils();
+ final String restPath;
+ if (!followData.isFollowing()) {
+ restPath = String.format("/sites/%s/follows/new", followData.getSiteID());
+ } else {
+ restPath = String.format("/sites/%s/follows/mine/delete", followData.getSiteID());
+ }
+
+ followData.isRestCallInProgress = true;
+ FollowRestListener vListener = new FollowRestListener(mActivityRef.get(), followData);
+ restClientUtils.post(restPath, vListener, vListener);
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following REST request " + restPath);
+ return true;
+ }
+ });
+
+ popup.show();
+
+ }
+
+
+ private class FollowRestListener implements RestRequest.Listener, RestRequest.ErrorListener {
+ private final WeakReference<Activity> mActivityRef;
+ private final FollowDataModel mFollowData;
+
+ public FollowRestListener(Activity activity, final FollowDataModel followData) {
+ this.mActivityRef = new WeakReference<>(activity);
+ this.mFollowData = followData;
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mFollowData.isRestCallInProgress = false;
+ if (response!= null) {
+ try {
+ boolean isFollowing = response.getBoolean("is_following");
+ mFollowData.setIsFollowing(isFollowing);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ if (volleyError != null) {
+ AppLog.e(AppLog.T.STATS, "Error while following a blog "
+ + volleyError.getMessage(), volleyError);
+ }
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mFollowData.isRestCallInProgress = false;
+ ToastUtils.showToast(mActivityRef.get(),
+ mActivityRef.get().getString(R.string.reader_toast_err_follow_blog),
+ ToastUtils.Duration.LONG);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java
new file mode 100644
index 000000000..f601cdb38
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.support.v4.widget.NestedScrollView;
+import android.util.AttributeSet;
+
+public class NestedScrollViewExt extends NestedScrollView {
+ private ScrollViewListener mScrollViewListener = null;
+ public NestedScrollViewExt(Context context) {
+ super(context);
+ }
+
+ public NestedScrollViewExt(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public NestedScrollViewExt(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setScrollViewListener(ScrollViewListener scrollViewListener) {
+ this.mScrollViewListener = scrollViewListener;
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (mScrollViewListener != null) {
+ mScrollViewListener.onScrollChanged(this, l, t, oldl, oldt);
+ }
+ }
+
+ public interface ScrollViewListener {
+ void onScrollChanged(NestedScrollViewExt scrollView,
+ int x, int y, int oldx, int oldy);
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java
new file mode 100644
index 000000000..c8738cd9b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java
@@ -0,0 +1,159 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.PopupMenu;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.ui.stats.models.ReferrerGroupModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.lang.ref.WeakReference;
+
+class ReferrerSpamHelper {
+
+ private final WeakReference<Activity> mActivityRef;
+
+ public ReferrerSpamHelper(Activity activity) {
+ mActivityRef = new WeakReference<>(activity);
+ }
+
+ // return the domain of the passed ReferrerGroupModel or null.
+ private static String getDomain(ReferrerGroupModel group) {
+ // Use the URL value given in the JSON response, or use the groupID that doesn't contain the schema.
+ final String spamDomain = group.getUrl() != null ? group.getUrl() : "http://" + group.getGroupId();
+ return UrlUtils.isValidUrlAndHostNotNull(spamDomain) ? UrlUtils.getHost(spamDomain) : null;
+ }
+
+ public static boolean isSpamActionAvailable(ReferrerGroupModel group) {
+ String domain = getDomain(group);
+ return !TextUtils.isEmpty(domain) && !domain.equals("wordpress.com");
+ }
+
+ public void showPopup(View anchor, final ReferrerGroupModel referrerGroup) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ final PopupMenu popup = new PopupMenu(mActivityRef.get(), anchor);
+ final MenuItem menuItem;
+
+ if (referrerGroup.isRestCallInProgress) {
+ menuItem = popup.getMenu().add(
+ referrerGroup.isMarkedAsSpam ?
+ mActivityRef.get().getString(R.string.stats_referrers_marking_not_spam) :
+ mActivityRef.get().getString(R.string.stats_referrers_marking_spam)
+ );
+ } else {
+ menuItem = popup.getMenu().add(
+ referrerGroup.isMarkedAsSpam ?
+ mActivityRef.get().getString(R.string.stats_referrers_unspam) :
+ mActivityRef.get().getString(R.string.stats_referrers_spam)
+ );
+ }
+
+ menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ item.setTitle(
+ referrerGroup.isMarkedAsSpam ?
+ mActivityRef.get().getString(R.string.stats_referrers_marking_not_spam) :
+ mActivityRef.get().getString(R.string.stats_referrers_marking_spam)
+ );
+ item.setOnMenuItemClickListener(null);
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtilsV1_1();
+ final String restPath;
+ final boolean isMarkingAsSpamInProgress;
+ if (referrerGroup.isMarkedAsSpam) {
+ restPath = String.format("/sites/%s/stats/referrers/spam/delete/?domain=%s", referrerGroup.getBlogId(), getDomain(referrerGroup));
+ isMarkingAsSpamInProgress = false;
+ } else {
+ restPath = String.format("/sites/%s/stats/referrers/spam/new/?domain=%s", referrerGroup.getBlogId(), getDomain(referrerGroup));
+ isMarkingAsSpamInProgress = true;
+ }
+
+ referrerGroup.isRestCallInProgress = true;
+ ReferrerSpamRestListener vListener = new ReferrerSpamRestListener(mActivityRef.get(), referrerGroup, isMarkingAsSpamInProgress);
+ restClientUtils.post(restPath, vListener, vListener);
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following REST request " + restPath);
+ return true;
+ }
+ });
+
+ popup.show();
+ }
+
+
+ private class ReferrerSpamRestListener implements RestRequest.Listener, RestRequest.ErrorListener {
+ private final WeakReference<Activity> mActivityRef;
+ private final ReferrerGroupModel mReferrerGroup;
+ private final boolean isMarkingAsSpamInProgress;
+
+ public ReferrerSpamRestListener(Activity activity, final ReferrerGroupModel referrerGroup, final boolean isMarkingAsSpamInProgress) {
+ this.mActivityRef = new WeakReference<>(activity);
+ this.mReferrerGroup = referrerGroup;
+ this.isMarkingAsSpamInProgress = isMarkingAsSpamInProgress;
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mReferrerGroup.isRestCallInProgress = false;
+ if (response!= null) {
+ boolean success = response.optBoolean("success");
+ if (success) {
+ mReferrerGroup.isMarkedAsSpam = isMarkingAsSpamInProgress;
+ int localBlogID = StatsUtils.getLocalBlogIdFromRemoteBlogId(
+ Integer.parseInt(mReferrerGroup.getBlogId())
+ );
+ StatsTable.deleteStatsForBlog(mActivityRef.get(), localBlogID, StatsService.StatsEndpointsEnum.REFERRERS);
+ } else {
+ // It's not a success. Something went wrong on the server
+ String errorMessage = null;
+ if (response.has("error")) {
+ errorMessage = response.optString("message");
+ }
+
+ if (TextUtils.isEmpty(errorMessage)) {
+ errorMessage = mActivityRef.get().getString(R.string.stats_referrers_spam_generic_error);
+ }
+
+ ToastUtils.showToast(mActivityRef.get(), errorMessage, ToastUtils.Duration.LONG);
+ }
+ }
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ if (volleyError != null) {
+ AppLog.e(AppLog.T.STATS, "Error while marking the referrer " + getDomain(mReferrerGroup) + " as "
+ + (isMarkingAsSpamInProgress ? " spam " : " unspam ") +
+ volleyError.getMessage(), volleyError);
+ }
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mReferrerGroup.isRestCallInProgress = false;
+ ToastUtils.showToast(mActivityRef.get(),
+ mActivityRef.get().getString(R.string.stats_referrers_spam_generic_error),
+ ToastUtils.Duration.LONG);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java
new file mode 100644
index 000000000..4b734adef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+public class ScrollViewExt extends ScrollView {
+ private ScrollViewListener mScrollViewListener = null;
+ public ScrollViewExt(Context context) {
+ super(context);
+ }
+
+ public ScrollViewExt(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public ScrollViewExt(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setScrollViewListener(ScrollViewListener scrollViewListener) {
+ this.mScrollViewListener = scrollViewListener;
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (mScrollViewListener != null) {
+ mScrollViewListener.onScrollChanged(this, l, t, oldl, oldt);
+ }
+ }
+
+ public interface ScrollViewListener {
+ void onScrollChanged(ScrollViewExt scrollView,
+ int x, int y, int oldx, int oldy);
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java
new file mode 100644
index 000000000..1503a3631
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseBooleanArray;
+
+public class SparseBooleanArrayParcelable extends SparseBooleanArray implements Parcelable {
+ public static Parcelable.Creator<SparseBooleanArrayParcelable> CREATOR = new Parcelable.Creator<SparseBooleanArrayParcelable>() {
+ @Override
+ public SparseBooleanArrayParcelable createFromParcel(Parcel source) {
+ SparseBooleanArrayParcelable read = new SparseBooleanArrayParcelable();
+ int size = source.readInt();
+
+ int[] keys = new int[size];
+ boolean[] values = new boolean[size];
+
+ source.readIntArray(keys);
+ source.readBooleanArray(values);
+
+ for (int i = 0; i < size; i++) {
+ read.put(keys[i], values[i]);
+ }
+
+ return read;
+ }
+
+ @Override
+ public SparseBooleanArrayParcelable[] newArray(int size) {
+ return new SparseBooleanArrayParcelable[size];
+ }
+ };
+
+ public SparseBooleanArrayParcelable() {
+
+ }
+
+ public SparseBooleanArrayParcelable(SparseBooleanArray sparseBooleanArray) {
+ for (int i = 0; i < sparseBooleanArray.size(); i++) {
+ this.put(sparseBooleanArray.keyAt(i), sparseBooleanArray.valueAt(i));
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ int[] keys = new int[size()];
+ boolean[] values = new boolean[size()];
+
+ for (int i = 0; i < size(); i++) {
+ keys[i] = keyAt(i);
+ values[i] = valueAt(i);
+ }
+
+ dest.writeInt(size());
+ dest.writeIntArray(keys);
+ dest.writeBooleanArray(values);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java
new file mode 100644
index 000000000..a6d761b9c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java
@@ -0,0 +1,361 @@
+package org.wordpress.android.ui.stats;
+
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.volley.NoConnectionError;
+import com.android.volley.VolleyError;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+
+import de.greenrobot.event.EventBus;
+
+
+public abstract class StatsAbstractFragment extends Fragment {
+ public static final String TAG = StatsAbstractFragment.class.getSimpleName();
+
+ public static final String ARGS_VIEW_TYPE = "ARGS_VIEW_TYPE";
+ public static final String ARGS_TIMEFRAME = "ARGS_TIMEFRAME";
+ public static final String ARGS_SELECTED_DATE = "ARGS_SELECTED_DATE";
+ static final String ARG_REST_RESPONSE = "ARG_REST_RESPONSE";
+ static final String ARGS_IS_SINGLE_VIEW = "ARGS_IS_SINGLE_VIEW";
+
+ // The number of results to return for NON Paged REST endpoints.
+ private static final int MAX_RESULTS_REQUESTED = 100;
+
+ private String mDate;
+ private StatsTimeframe mStatsTimeframe = StatsTimeframe.DAY;
+
+ protected abstract StatsService.StatsEndpointsEnum[] sectionsToUpdate();
+ protected abstract void showPlaceholderUI();
+ protected abstract void updateUI();
+ protected abstract void showErrorUI(String label);
+
+ /**
+ * Wheter or not previous data is available.
+ * @return True if previous data is already available in the fragment
+ */
+ protected abstract boolean hasDataAvailable();
+
+ /**
+ * Called in onSaveIstance. Fragments should persist data here.
+ * @param outState Bundle in which to place fragment saved state.
+ */
+ protected abstract void saveStatsData(Bundle outState);
+
+ /**
+ * Called in OnCreate. Fragment should restore here previous saved data.
+ * @param savedInstanceState If the fragment is being re-created from a previous saved state, this is the state.
+ */
+ protected abstract void restoreStatsData(Bundle savedInstanceState); // called in onCreate
+
+ protected StatsResourceVars mResourceVars;
+
+ public void refreshStats() {
+ refreshStats(-1, null);
+ }
+ // call an update for the stats shown in the fragment
+ void refreshStats(int pageNumberRequested, StatsService.StatsEndpointsEnum[] sections) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // if no sections to update is passed to the method, default to fragment
+ if (sections == null) {
+ sections = sectionsToUpdate();
+ }
+
+ //AppLog.d(AppLog.T.STATS, this.getClass().getCanonicalName() + " > refreshStats");
+
+ final Blog currentBlog = WordPress.getBlog(getLocalTableBlogID());
+ if (currentBlog == null) {
+ AppLog.w(AppLog.T.STATS, "Current blog is null. This should never happen here.");
+ return;
+ }
+
+ final String blogId = currentBlog.getDotComBlogId();
+ // Make sure the blogId is available.
+ if (blogId == null) {
+ AppLog.e(AppLog.T.STATS, "remote blogID is null: " + currentBlog.getHomeURL());
+ return;
+ }
+
+ // Check credentials for jetpack blogs first
+ if (!currentBlog.isDotcomFlag()
+ && !currentBlog.hasValidJetpackCredentials() && !AccountHelper.isSignedInWordPressDotCom()) {
+ AppLog.w(AppLog.T.STATS, "Current blog is a Jetpack blog without valid .com credentials stored");
+ return;
+ }
+
+ // Do not pass the array of StatsEndpointsEnum to the Service. Otherwise we get
+ // java.lang.RuntimeException: Unable to start service org.wordpress.android.ui.stats.service.StatsService
+ // with Intent { cmp=org.wordpress.android/.ui.stats.service.StatsService (has extras) }: java.lang.ClassCastException:
+ // java.lang.Object[] cannot be cast to org.wordpress.android.ui.stats.service.StatsService$StatsEndpointsEnum[]
+ // on older devices.
+ // We should use Enumset, or array of int. Going for the latter, since we have an array and cannot create an Enumset easily.
+ int[] sectionsForTheService = new int[sections.length];
+ for (int i=0; i < sections.length; i++){
+ sectionsForTheService[i] = sections[i].ordinal();
+ }
+
+ // start service to get stats
+ Intent intent = new Intent(getActivity(), StatsService.class);
+ intent.putExtra(StatsService.ARG_BLOG_ID, blogId);
+ intent.putExtra(StatsService.ARG_PERIOD, mStatsTimeframe);
+ intent.putExtra(StatsService.ARG_DATE, mDate);
+ if (isSingleView()) {
+ // Single Item screen: request 20 items per page on paged requests. Default to the first 100 items otherwise.
+ int maxElementsToRetrieve = pageNumberRequested > 0 ? StatsService.MAX_RESULTS_REQUESTED_PER_PAGE : MAX_RESULTS_REQUESTED;
+ intent.putExtra(StatsService.ARG_MAX_RESULTS, maxElementsToRetrieve);
+ }
+ if (pageNumberRequested > 0) {
+ intent.putExtra(StatsService.ARG_PAGE_REQUESTED, pageNumberRequested);
+ }
+ intent.putExtra(StatsService.ARG_SECTION, sectionsForTheService);
+ getActivity().startService(intent);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // AppLog.d(AppLog.T.STATS, this.getClass().getCanonicalName() + " > onCreate");
+
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(ARGS_TIMEFRAME)) {
+ mStatsTimeframe = (StatsTimeframe) savedInstanceState.getSerializable(ARGS_TIMEFRAME);
+ }
+ if (savedInstanceState.containsKey(ARGS_SELECTED_DATE)) {
+ mDate = savedInstanceState.getString(ARGS_SELECTED_DATE);
+ }
+ restoreStatsData(savedInstanceState); // Each fragment will override this to restore fragment dependant data
+ }
+
+ // AppLog.d(AppLog.T.STATS, "mStatsTimeframe: " + mStatsTimeframe.getLabel());
+ // AppLog.d(AppLog.T.STATS, "mDate: " + mDate);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mResourceVars = new StatsResourceVars(activity);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ /* AppLog.d(AppLog.T.STATS, this.getClass().getCanonicalName() + " > saving instance state");
+ AppLog.d(AppLog.T.STATS, "mStatsTimeframe: " + mStatsTimeframe.getLabel());
+ AppLog.d(AppLog.T.STATS, "mDate: " + mDate); */
+
+ outState.putString(ARGS_SELECTED_DATE, mDate);
+ outState.putSerializable(ARGS_TIMEFRAME, mStatsTimeframe);
+ saveStatsData(outState); // Each fragment will override this
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Init the UI
+ if (hasDataAvailable()) {
+ updateUI();
+ } else {
+ showPlaceholderUI();
+ refreshStats();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ public boolean shouldUpdateFragmentOnUpdateEvent(StatsEvents.SectionUpdatedAbstract event) {
+ if (!isAdded()) {
+ return false;
+ }
+
+ if (!getDate().equals(event.mDate)) {
+ return false;
+ }
+
+ if (!isSameBlog(event)) {
+ return false;
+ }
+
+ if (!event.mTimeframe.equals(getTimeframe())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ boolean isSameBlog(StatsEvents.SectionUpdatedAbstract event) {
+ final Blog currentBlog = WordPress.getBlog(getLocalTableBlogID());
+ if (currentBlog != null && currentBlog.getDotComBlogId() != null) {
+ return event.mRequestBlogId.equals(currentBlog.getDotComBlogId());
+ }
+ return false;
+ }
+
+ protected void showErrorUI(VolleyError error) {
+ if (!isAdded()) {
+ return;
+ }
+
+ String label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+
+ if (error instanceof NoConnectionError) {
+ label += "<br/>" + getString(R.string.no_network_message);
+ }
+
+ if (StatsUtils.isRESTDisabledError(error)) {
+ label += "<br/>" + getString(R.string.stats_enable_rest_api_in_jetpack);
+ }
+
+ showErrorUI(label);
+ }
+
+ protected void showErrorUI() {
+ String label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+ showErrorUI(label);
+ }
+
+ public boolean shouldUpdateFragmentOnErrorEvent(StatsEvents.SectionUpdateError errorEvent) {
+ if (!shouldUpdateFragmentOnUpdateEvent(errorEvent)) {
+ return false;
+ }
+
+ StatsService.StatsEndpointsEnum sectionToUpdate = errorEvent.mEndPointName;
+ StatsService.StatsEndpointsEnum[] sectionsToUpdate = sectionsToUpdate();
+
+ for (int i = 0; i < sectionsToUpdate().length; i++) {
+ if (sectionToUpdate == sectionsToUpdate[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static StatsAbstractFragment newVisitorsAndViewsInstance(StatsViewType viewType, int localTableBlogID,
+ StatsTimeframe timeframe, String date, StatsVisitorsAndViewsFragment.OverviewLabel itemToSelect) {
+ StatsVisitorsAndViewsFragment fragment = (StatsVisitorsAndViewsFragment) newInstance(viewType, localTableBlogID, timeframe, date);
+ fragment.setSelectedOverviewItem(itemToSelect);
+ return fragment;
+ }
+
+ public static StatsAbstractFragment newInstance(StatsViewType viewType, int localTableBlogID,
+ StatsTimeframe timeframe, String date ) {
+ StatsAbstractFragment fragment = null;
+
+ switch (viewType) {
+ //case TIMEFRAME_SELECTOR:
+ // fragment = new StatsDateSelectorFragment();
+ // break;
+ case GRAPH_AND_SUMMARY:
+ fragment = new StatsVisitorsAndViewsFragment();
+ break;
+ case TOP_POSTS_AND_PAGES:
+ fragment = new StatsTopPostsAndPagesFragment();
+ break;
+ case REFERRERS:
+ fragment = new StatsReferrersFragment();
+ break;
+ case CLICKS:
+ fragment = new StatsClicksFragment();
+ break;
+ case GEOVIEWS:
+ fragment = new StatsGeoviewsFragment();
+ break;
+ case AUTHORS:
+ fragment = new StatsAuthorsFragment();
+ break;
+ case VIDEO_PLAYS:
+ fragment = new StatsVideoplaysFragment();
+ break;
+ case COMMENTS:
+ fragment = new StatsCommentsFragment();
+ break;
+ case TAGS_AND_CATEGORIES:
+ fragment = new StatsTagsAndCategoriesFragment();
+ break;
+ case PUBLICIZE:
+ fragment = new StatsPublicizeFragment();
+ break;
+ case FOLLOWERS:
+ fragment = new StatsFollowersFragment();
+ break;
+ case SEARCH_TERMS:
+ fragment = new StatsSearchTermsFragment();
+ break;
+ case INSIGHTS_MOST_POPULAR:
+ fragment = new StatsInsightsMostPopularFragment();
+ break;
+ case INSIGHTS_ALL_TIME:
+ fragment = new StatsInsightsAllTimeFragment();
+ break;
+ case INSIGHTS_TODAY:
+ fragment = new StatsInsightsTodayFragment();
+ break;
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ fragment = new StatsInsightsLatestPostSummaryFragment();
+ break;
+ }
+
+ fragment.setTimeframe(timeframe);
+ fragment.setDate(date);
+
+ Bundle args = new Bundle();
+ args.putSerializable(ARGS_VIEW_TYPE, viewType);
+ args.putInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, localTableBlogID);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ public void setDate(String newDate) {
+ mDate = newDate;
+ }
+
+ String getDate() {
+ return mDate;
+ }
+
+ public void setTimeframe(StatsTimeframe newTimeframe) {
+ mStatsTimeframe = newTimeframe;
+ }
+
+ StatsTimeframe getTimeframe() {
+ return mStatsTimeframe;
+ }
+
+ StatsViewType getViewType() {
+ return (StatsViewType) getArguments().getSerializable(ARGS_VIEW_TYPE);
+ }
+
+ int getLocalTableBlogID() {
+ return getArguments().getInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID);
+ }
+
+ boolean isSingleView() {
+ return getArguments().getBoolean(ARGS_IS_SINGLE_VIEW, false);
+ }
+
+ protected abstract String getTitle();
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java
new file mode 100644
index 000000000..8f24efd8f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java
@@ -0,0 +1,85 @@
+package org.wordpress.android.ui.stats;
+
+
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+
+public abstract class StatsAbstractInsightsFragment extends StatsAbstractFragment {
+ public static final String TAG = StatsAbstractInsightsFragment.class.getSimpleName();
+
+ private TextView mErrorLabel;
+ private LinearLayout mEmptyModulePlaceholder;
+ LinearLayout mResultContainer;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.stats_insights_generic_fragment, container, false);
+ TextView moduleTitleTextView = (TextView) view.findViewById(R.id.stats_module_title);
+ moduleTitleTextView.setText(getTitle());
+
+ mEmptyModulePlaceholder = (LinearLayout) view.findViewById(R.id.stats_empty_module_placeholder);
+ mResultContainer = (LinearLayout) view.findViewById(R.id.stats_module_result_container);
+ mErrorLabel = (TextView) view.findViewById(R.id.stats_error_text);
+ return view;
+ }
+
+ @Override
+ protected void showPlaceholderUI() {
+ mErrorLabel.setVisibility(View.GONE);
+ mResultContainer.setVisibility(View.GONE);
+ mEmptyModulePlaceholder.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ protected void showErrorUI(String label) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Use the generic error message when the string passed to this method is null.
+ if (TextUtils.isEmpty(label)) {
+ label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+ }
+
+ if (label.contains("<")) {
+ mErrorLabel.setText(Html.fromHtml(label));
+ } else {
+ mErrorLabel.setText(label);
+ }
+ mErrorLabel.setVisibility(View.VISIBLE);
+ mResultContainer.setVisibility(View.GONE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+ }
+
+ /**
+ * Insights module all have the same basic implementation of updateUI. Let's provide a common code here.
+ */
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Another check that the data is available. At this point it should be available.
+ if (!hasDataAvailable()) {
+ showErrorUI();
+ return;
+ }
+
+ // not an error - update the module UI here
+ mErrorLabel.setVisibility(View.GONE);
+ mResultContainer.setVisibility(View.VISIBLE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+
+ mResultContainer.removeAllViews();
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java
new file mode 100644
index 000000000..d78e1f4c4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java
@@ -0,0 +1,297 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.SparseBooleanArray;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CheckedTextView;
+import android.widget.LinearLayout;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.widgets.TypefaceCache;
+
+public abstract class StatsAbstractListFragment extends StatsAbstractFragment {
+
+ // Used when the fragment has 2 pages/kind of stats in it. Not meaning the bottom pagination.
+ static final String ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX = "ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX";
+ private static final String ARGS_EXPANDED_ROWS = "ARGS_EXPANDED_ROWS";
+ private static final int MAX_NUM_OF_ITEMS_DISPLAYED_IN_SINGLE_VIEW_LIST = 1000;
+ static final int MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST = 10;
+
+ private static final int NO_STRING_ID = -1;
+
+ private TextView mModuleTitleTextView;
+ private TextView mEmptyLabel;
+ TextView mTotalsLabel;
+ private LinearLayout mListContainer;
+ LinearLayout mList;
+ private Button mViewAll;
+
+ LinearLayout mTopPagerContainer;
+ int mTopPagerSelectedButtonIndex = 0;
+
+ // Bottom and Top Pagination for modules that has pagination enabled.
+ LinearLayout mBottomPaginationContainer;
+ Button mBottomPaginationGoBackButton;
+ Button mBottomPaginationGoForwardButton;
+ TextView mBottomPaginationText;
+ LinearLayout mTopPaginationContainer;
+ Button mTopPaginationGoBackButton;
+ Button mTopPaginationGoForwardButton;
+ TextView mTopPaginationText;
+
+ private LinearLayout mEmptyModulePlaceholder;
+
+ SparseBooleanArray mGroupIdToExpandedMap;
+
+ protected abstract int getEntryLabelResId();
+ protected abstract int getTotalsLabelResId();
+ protected abstract int getEmptyLabelTitleResId();
+ protected abstract int getEmptyLabelDescResId();
+ protected abstract boolean isExpandableList();
+ protected abstract boolean isViewAllOptionAvailable();
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view;
+ if (isExpandableList()) {
+ view = inflater.inflate(R.layout.stats_expandable_list_fragment, container, false);
+ } else {
+ view = inflater.inflate(R.layout.stats_list_fragment, container, false);
+ }
+
+ mEmptyModulePlaceholder = (LinearLayout) view.findViewById(R.id.stats_empty_module_placeholder);
+ mModuleTitleTextView = (TextView) view.findViewById(R.id.stats_module_title);
+ mModuleTitleTextView.setText(getTitle());
+
+ TextView entryLabel = (TextView) view.findViewById(R.id.stats_list_entry_label);
+ entryLabel.setText(getEntryLabelResId());
+ TextView totalsLabel = (TextView) view.findViewById(R.id.stats_list_totals_label);
+ totalsLabel.setText(getTotalsLabelResId());
+
+ mEmptyLabel = (TextView) view.findViewById(R.id.stats_list_empty_text);
+ mTotalsLabel = (TextView) view.findViewById(R.id.stats_module_totals_label);
+ mList = (LinearLayout) view.findViewById(R.id.stats_list_linearlayout);
+ mListContainer = (LinearLayout) view.findViewById(R.id.stats_list_container);
+ mViewAll = (Button) view.findViewById(R.id.btnViewAll);
+ mTopPagerContainer = (LinearLayout) view.findViewById(R.id.stats_pager_tabs);
+
+ // Load pagination items
+ mBottomPaginationContainer = (LinearLayout) view.findViewById(R.id.stats_bottom_pagination_container);
+ mBottomPaginationGoBackButton = (Button) mBottomPaginationContainer.findViewById(R.id.stats_pagination_go_back);
+ mBottomPaginationGoForwardButton = (Button) mBottomPaginationContainer.findViewById(R.id.stats_pagination_go_forward);
+ mBottomPaginationText = (TextView) mBottomPaginationContainer.findViewById(R.id.stats_pagination_text);
+ mTopPaginationContainer = (LinearLayout) view.findViewById(R.id.stats_top_pagination_container);
+ mTopPaginationContainer.setBackgroundResource(R.drawable.stats_pagination_item_background);
+ mTopPaginationGoBackButton = (Button) mTopPaginationContainer.findViewById(R.id.stats_pagination_go_back);
+ mTopPaginationGoForwardButton = (Button) mTopPaginationContainer.findViewById(R.id.stats_pagination_go_forward);
+ mTopPaginationText = (TextView) mTopPaginationContainer.findViewById(R.id.stats_pagination_text);
+
+ return view;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mGroupIdToExpandedMap = new SparseBooleanArray();
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(ARGS_EXPANDED_ROWS)) {
+ mGroupIdToExpandedMap = savedInstanceState.getParcelable(ARGS_EXPANDED_ROWS);
+ }
+ mTopPagerSelectedButtonIndex = savedInstanceState.getInt(ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mGroupIdToExpandedMap.size() > 0) {
+ outState.putParcelable(ARGS_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mGroupIdToExpandedMap));
+ }
+ outState.putInt(ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mTopPagerSelectedButtonIndex);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void showPlaceholderUI() {
+ mTopPagerContainer.setVisibility(View.GONE);
+ mEmptyLabel.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.GONE);
+ mList.setVisibility(View.GONE);
+ mViewAll.setVisibility(View.GONE);
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTopPaginationContainer.setVisibility(View.GONE);
+ mEmptyModulePlaceholder.setVisibility(View.VISIBLE);
+ }
+
+ void showHideNoResultsUI(boolean showNoResultsUI) {
+ mModuleTitleTextView.setVisibility(View.VISIBLE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+
+ if (showNoResultsUI) {
+ mGroupIdToExpandedMap.clear();
+ String label;
+ if (getEmptyLabelDescResId() == NO_STRING_ID) {
+ label = "<b>" + getString(getEmptyLabelTitleResId()) + "</b><br/><br/>";
+ } else {
+ label = "<b>" + getString(getEmptyLabelTitleResId()) + "</b><br/><br/>" + getString(getEmptyLabelDescResId());
+ }
+ if (label.contains("<")) {
+ mEmptyLabel.setText(Html.fromHtml(label));
+ } else {
+ mEmptyLabel.setText(label);
+ }
+ mEmptyLabel.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.GONE);
+ mList.setVisibility(View.GONE);
+ mViewAll.setVisibility(View.GONE);
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTopPaginationContainer.setVisibility(View.GONE);
+ } else {
+ mEmptyLabel.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.VISIBLE);
+ mList.setVisibility(View.VISIBLE);
+
+ if (!isSingleView() && isViewAllOptionAvailable()) {
+ // No view all button if already in single view
+ configureViewAllButton();
+ } else {
+ mViewAll.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void showErrorUI(String label) {
+ if (!isAdded()) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mModuleTitleTextView.setVisibility(View.VISIBLE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+
+ // Use the generic error message when the string passed to this method is null.
+ if (TextUtils.isEmpty(label)) {
+ label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+ }
+
+ if (label.contains("<")) {
+ mEmptyLabel.setText(Html.fromHtml(label));
+ } else {
+ mEmptyLabel.setText(label);
+ }
+ mEmptyLabel.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.GONE);
+ mList.setVisibility(View.GONE);
+ }
+
+ private void configureViewAllButton() {
+ if (isSingleView()) {
+ // No view all button if you're already in single view
+ mViewAll.setVisibility(View.GONE);
+ return;
+ }
+ mViewAll.setVisibility(View.VISIBLE);
+ mViewAll.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (isSingleView()) {
+ return; // already in single view
+ }
+
+ if (!hasDataAvailable()) {
+ return;
+ }
+
+ Intent viewAllIntent = new Intent(getActivity(), StatsViewAllActivity.class);
+ viewAllIntent.putExtra(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, getLocalTableBlogID());
+ viewAllIntent.putExtra(StatsAbstractFragment.ARGS_TIMEFRAME, getTimeframe());
+ viewAllIntent.putExtra(StatsAbstractFragment.ARGS_VIEW_TYPE, getViewType());
+ viewAllIntent.putExtra(StatsAbstractFragment.ARGS_SELECTED_DATE, getDate());
+ viewAllIntent.putExtra(ARGS_IS_SINGLE_VIEW, true);
+ if (mTopPagerContainer.getVisibility() == View.VISIBLE) {
+ viewAllIntent.putExtra(ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mTopPagerSelectedButtonIndex);
+ }
+ //viewAllIntent.putExtra(StatsAbstractFragment.ARG_REST_RESPONSE, mDatamodels[mTopPagerSelectedButtonIndex]);
+ getActivity().startActivity(viewAllIntent);
+ }
+ });
+ }
+
+ int getMaxNumberOfItemsToShowInList() {
+ return isSingleView() ? MAX_NUM_OF_ITEMS_DISPLAYED_IN_SINGLE_VIEW_LIST : MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ void setupTopModulePager(LayoutInflater inflater, ViewGroup container, View view, String[] buttonTitles) {
+ int dp4 = DisplayUtils.dpToPx(view.getContext(), 4);
+ int dp80 = DisplayUtils.dpToPx(view.getContext(), 80);
+
+ for (int i = 0; i < buttonTitles.length; i++) {
+ CheckedTextView rb = (CheckedTextView) inflater.inflate(R.layout.stats_top_module_pager_button, container, false);
+ RadioGroup.LayoutParams params = new RadioGroup.LayoutParams(RadioGroup.LayoutParams.MATCH_PARENT,
+ RadioGroup.LayoutParams.WRAP_CONTENT);
+ params.weight = 1;
+ rb.setTypeface((TypefaceCache.getTypeface(view.getContext())));
+ if (i == 0) {
+ params.setMargins(0, 0, dp4, 0);
+ } else {
+ params.setMargins(dp4, 0, 0, 0);
+ }
+ rb.setMinimumWidth(dp80);
+ rb.setGravity(Gravity.CENTER);
+ rb.setLayoutParams(params);
+ rb.setText(buttonTitles[i]);
+ rb.setChecked(i == mTopPagerSelectedButtonIndex);
+ rb.setOnClickListener(TopModulePagerOnClickListener);
+ mTopPagerContainer.addView(rb);
+ }
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ }
+
+ private final View.OnClickListener TopModulePagerOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ CheckedTextView ctv = (CheckedTextView) v;
+ if (ctv.isChecked()) {
+ // already checked. Do nothing
+ return;
+ }
+
+ int numberOfButtons = mTopPagerContainer.getChildCount();
+ int checkedId = -1;
+ for (int i = 0; i < numberOfButtons; i++) {
+ CheckedTextView currentCheckedTextView = (CheckedTextView)mTopPagerContainer.getChildAt(i);
+ if (ctv == currentCheckedTextView) {
+ checkedId = i;
+ currentCheckedTextView.setChecked(true);
+ } else {
+ currentCheckedTextView.setChecked(false);
+ }
+ }
+
+ if (checkedId == -1)
+ return;
+
+ mTopPagerSelectedButtonIndex = checkedId;
+
+ TextView entryLabel = (TextView) getView().findViewById(R.id.stats_list_entry_label);
+ if (entryLabel != null) {
+ entryLabel.setText(getEntryLabelResId());
+ }
+ updateUI();
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java
new file mode 100644
index 000000000..c3c390e5d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java
@@ -0,0 +1,1034 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.apache.commons.lang.StringUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.ui.posts.PromoDialog;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.RateLimitedTask;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * The native stats activity
+ * <p>
+ * By pressing a spinner on the action bar, the user can select which timeframe they wish to see.
+ * </p>
+ */
+public class StatsActivity extends AppCompatActivity
+ implements NestedScrollViewExt.ScrollViewListener,
+ StatsVisitorsAndViewsFragment.OnDateChangeListener,
+ StatsVisitorsAndViewsFragment.OnOverviewItemChangeListener,
+ StatsInsightsTodayFragment.OnInsightsTodayClickListener {
+
+ private static final String SAVED_WP_LOGIN_STATE = "SAVED_WP_LOGIN_STATE";
+ private static final String SAVED_STATS_TIMEFRAME = "SAVED_STATS_TIMEFRAME";
+ private static final String SAVED_STATS_REQUESTED_DATE = "SAVED_STATS_REQUESTED_DATE";
+ private static final String SAVED_STATS_SCROLL_POSITION = "SAVED_STATS_SCROLL_POSITION";
+ private static final String SAVED_THERE_WAS_AN_ERROR_LOADING_STATS = "SAVED_THERE_WAS_AN_ERROR_LOADING_STATS";
+
+ private Spinner mSpinner;
+ private NestedScrollViewExt mOuterScrollView;
+
+ private static final int REQUEST_JETPACK = 7000;
+ public static final String ARG_LOCAL_TABLE_BLOG_ID = "ARG_LOCAL_TABLE_BLOG_ID";
+ public static final String ARG_LAUNCHED_FROM = "ARG_LAUNCHED_FROM";
+ public static final String ARG_DESIRED_TIMEFRAME = "ARG_DESIRED_TIMEFRAME";
+
+ public enum StatsLaunchedFrom {
+ STATS_WIDGET,
+ NOTIFICATIONS
+ }
+
+ private int mResultCode = -1;
+ private boolean mIsInFront;
+ private int mLocalBlogID = -1;
+ private StatsTimeframe mCurrentTimeframe = StatsTimeframe.INSIGHTS;
+ private String mRequestedDate;
+ private boolean mIsUpdatingStats;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private TimeframeSpinnerAdapter mTimeframeSpinnerAdapter;
+ private final StatsTimeframe[] timeframes = {StatsTimeframe.INSIGHTS, StatsTimeframe.DAY, StatsTimeframe.WEEK,
+ StatsTimeframe.MONTH, StatsTimeframe.YEAR};
+ private StatsVisitorsAndViewsFragment.OverviewLabel mTabToSelectOnGraph = StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS;
+
+ private boolean mThereWasAnErrorLoadingStats = false;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (WordPress.wpDB == null) {
+ Toast.makeText(this, R.string.fatal_db_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.stats_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setElevation(0);
+ actionBar.setTitle(R.string.stats);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+
+ if (mIsUpdatingStats) {
+ AppLog.w(T.STATS, "stats are already updating, refresh cancelled");
+ return;
+ }
+
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ if (checkCredentials()) {
+ updateTimeframeAndDateAndStartRefreshOfFragments(true);
+ }
+ }
+ });
+
+ setTitle(R.string.stats);
+
+ mOuterScrollView = (NestedScrollViewExt) findViewById(R.id.scroll_view_stats);
+ mOuterScrollView.setScrollViewListener(this);
+
+ if (savedInstanceState != null) {
+ mResultCode = savedInstanceState.getInt(SAVED_WP_LOGIN_STATE);
+ mLocalBlogID = savedInstanceState.getInt(ARG_LOCAL_TABLE_BLOG_ID);
+ mCurrentTimeframe = (StatsTimeframe) savedInstanceState.getSerializable(SAVED_STATS_TIMEFRAME);
+ mRequestedDate = savedInstanceState.getString(SAVED_STATS_REQUESTED_DATE);
+ mThereWasAnErrorLoadingStats = savedInstanceState.getBoolean(SAVED_THERE_WAS_AN_ERROR_LOADING_STATS);
+ final int yScrollPosition = savedInstanceState.getInt(SAVED_STATS_SCROLL_POSITION);
+ if(yScrollPosition != 0) {
+ mOuterScrollView.postDelayed(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ mOuterScrollView.scrollTo(0, yScrollPosition);
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ } else if (getIntent() != null) {
+ mLocalBlogID = getIntent().getIntExtra(ARG_LOCAL_TABLE_BLOG_ID, -1);
+ if (getIntent().hasExtra(SAVED_STATS_TIMEFRAME)) {
+ mCurrentTimeframe = (StatsTimeframe) getIntent().getSerializableExtra(SAVED_STATS_TIMEFRAME);
+ } else if (getIntent().hasExtra(ARG_DESIRED_TIMEFRAME)) {
+ mCurrentTimeframe = (StatsTimeframe) getIntent().getSerializableExtra(ARG_DESIRED_TIMEFRAME);
+ } else {
+ // Read the value from app preferences here. Default to 0 - Insights
+ mCurrentTimeframe = AppPrefs.getStatsTimeframe();
+ }
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+
+ if (getIntent().hasExtra(ARG_LAUNCHED_FROM)) {
+ StatsLaunchedFrom from = (StatsLaunchedFrom) getIntent().getSerializableExtra(ARG_LAUNCHED_FROM);
+ if (from == StatsLaunchedFrom.STATS_WIDGET) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_WIDGET_TAPPED, WordPress.getBlog(mLocalBlogID));
+ }
+ }
+
+ }
+
+ //Make sure the blog_id passed to this activity is valid and the blog is available within the app
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ // create the fragments without forcing the re-creation. If the activity is restarted fragments can already
+ // be there, and ready to be displayed without making any network connections. A fragment calls the stats service
+ // if its internal datamodel is empty.
+ createFragments(false);
+
+ if (mSpinner == null) {
+ mSpinner = (Spinner) findViewById(R.id.filter_spinner);
+
+ mTimeframeSpinnerAdapter = new TimeframeSpinnerAdapter(this, timeframes);
+
+ mSpinner.setAdapter(mTimeframeSpinnerAdapter);
+ mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (isFinishing()) {
+ return;
+ }
+ final StatsTimeframe selectedTimeframe = (StatsTimeframe) mTimeframeSpinnerAdapter.getItem(position);
+
+ if (mCurrentTimeframe == selectedTimeframe) {
+ AppLog.d(T.STATS, "The selected TIME FRAME is already active: " + selectedTimeframe.getLabel());
+ return;
+ }
+
+ AppLog.d(T.STATS, "NEW TIME FRAME : " + selectedTimeframe.getLabel());
+ mCurrentTimeframe = selectedTimeframe;
+ AppPrefs.setStatsTimeframe(mCurrentTimeframe);
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ createFragments(true); // Need to recreate fragment here, since a new timeline was selected.
+ mSpinner.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ scrollToTop();
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+
+ trackStatsAnalytics();
+ }
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // nop
+ }
+ });
+
+ Toolbar spinnerToolbar = (Toolbar) findViewById(R.id.toolbar_filter);
+ spinnerToolbar.setBackgroundColor(getResources().getColor(R.color.blue_medium));
+
+ }
+
+ selectCurrentTimeframeInActionBar();
+
+ TextView otherRecentStatsMovedLabel = (TextView) findViewById(R.id.stats_other_recent_stats_moved);
+ otherRecentStatsMovedLabel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ for (int i = 0; i < timeframes.length; i++) {
+ if (timeframes[i] == StatsTimeframe.INSIGHTS) {
+ mSpinner.setSelection(i);
+ break;
+ }
+ }
+
+ mSpinner.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ scrollToTop();
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ });
+
+ // Track usage here
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_ACCESSED, currentBlog);
+ trackStatsAnalytics();
+ }
+ }
+
+ private void trackStatsAnalytics() {
+ // Track usage here
+ Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ switch (mCurrentTimeframe) {
+ case INSIGHTS:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_INSIGHTS_ACCESSED, currentBlog);
+ break;
+ case DAY:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_DAYS_ACCESSED, currentBlog);
+ break;
+ case WEEK:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_WEEKS_ACCESSED, currentBlog);
+ break;
+ case MONTH:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_MONTHS_ACCESSED, currentBlog);
+ break;
+ case YEAR:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_YEARS_ACCESSED, currentBlog);
+ break;
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsInFront = true;
+ if (NetworkUtils.checkConnection(this)) {
+ checkCredentials();
+ } else {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+ ActivityId.trackLastActivity(ActivityId.STATS);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsInFront = false;
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(SAVED_WP_LOGIN_STATE, mResultCode);
+ outState.putInt(ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ outState.putSerializable(SAVED_STATS_TIMEFRAME, mCurrentTimeframe);
+ outState.putString(SAVED_STATS_REQUESTED_DATE, mRequestedDate);
+ outState.putBoolean(SAVED_THERE_WAS_AN_ERROR_LOADING_STATS, mThereWasAnErrorLoadingStats);
+ if (mOuterScrollView.getScrollY() != 0) {
+ outState.putInt(SAVED_STATS_SCROLL_POSITION, mOuterScrollView.getScrollY());
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ private void createFragments(boolean forceRecreationOfFragments) {
+ if (isFinishing()) {
+ return;
+ }
+
+ // Make the labels invisible see: https://github.com/wordpress-mobile/WordPress-Android/issues/3279
+ findViewById(R.id.stats_other_recent_stats_label_insights).setVisibility(View.INVISIBLE);
+ findViewById(R.id.stats_other_recent_stats_label_timeline).setVisibility(View.INVISIBLE);
+ findViewById(R.id.stats_other_recent_stats_moved).setVisibility(View.INVISIBLE);
+
+ FragmentManager fm = getFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+
+ StatsAbstractFragment fragment;
+
+ if (mCurrentTimeframe != StatsTimeframe.INSIGHTS) {
+ findViewById(R.id.stats_timeline_fragments_container).setVisibility(View.VISIBLE);
+ findViewById(R.id.stats_insights_fragments_container).setVisibility(View.GONE);
+
+ if (fm.findFragmentByTag(StatsVisitorsAndViewsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newVisitorsAndViewsInstance(StatsViewType.GRAPH_AND_SUMMARY, mLocalBlogID, mCurrentTimeframe, mRequestedDate,
+ mTabToSelectOnGraph);
+ ft.replace(R.id.stats_visitors_and_views_container, fragment, StatsVisitorsAndViewsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsTopPostsAndPagesFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.TOP_POSTS_AND_PAGES, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_top_posts_container, fragment, StatsTopPostsAndPagesFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsReferrersFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.REFERRERS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_referrers_container, fragment, StatsReferrersFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsClicksFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.CLICKS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_clicks_container, fragment, StatsClicksFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsGeoviewsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.GEOVIEWS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_geoviews_container, fragment, StatsGeoviewsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsAuthorsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.AUTHORS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_top_authors_container, fragment, StatsAuthorsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsVideoplaysFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.VIDEO_PLAYS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_video_container, fragment, StatsVideoplaysFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsSearchTermsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.SEARCH_TERMS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_search_terms_container, fragment, StatsSearchTermsFragment.TAG);
+ }
+
+ } else {
+ findViewById(R.id.stats_timeline_fragments_container).setVisibility(View.GONE);
+ findViewById(R.id.stats_insights_fragments_container).setVisibility(View.VISIBLE);
+
+ if (fm.findFragmentByTag(StatsInsightsMostPopularFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_MOST_POPULAR, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_insights_most_popular_container, fragment, StatsInsightsMostPopularFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsInsightsAllTimeFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_ALL_TIME, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_insights_all_time_container, fragment, StatsInsightsAllTimeFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsInsightsTodayFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_TODAY, mLocalBlogID, StatsTimeframe.DAY, mRequestedDate);
+ ft.replace(R.id.stats_insights_today_container, fragment, StatsInsightsTodayFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsInsightsLatestPostSummaryFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_LATEST_POST_SUMMARY, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_insights_latest_post_summary_container, fragment, StatsInsightsLatestPostSummaryFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsCommentsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.COMMENTS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_comments_container, fragment, StatsCommentsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsTagsAndCategoriesFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.TAGS_AND_CATEGORIES, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_tags_and_categories_container, fragment, StatsTagsAndCategoriesFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsPublicizeFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.PUBLICIZE, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_publicize_container, fragment, StatsPublicizeFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsFollowersFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.FOLLOWERS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_followers_container, fragment, StatsFollowersFragment.TAG);
+ }
+ }
+
+ ft.commitAllowingStateLoss();
+
+ // Slightly delayed labels setup: see https://github.com/wordpress-mobile/WordPress-Android/issues/3279
+ mOuterScrollView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isFinishing()) {
+ return;
+ }
+ boolean isInsights = mCurrentTimeframe == StatsTimeframe.INSIGHTS;
+ findViewById(R.id.stats_other_recent_stats_label_insights).setVisibility(isInsights ? View.VISIBLE : View.GONE);
+ findViewById(R.id.stats_other_recent_stats_label_timeline).setVisibility(isInsights ? View.GONE : View.VISIBLE);
+ findViewById(R.id.stats_other_recent_stats_moved).setVisibility(isInsights ? View.GONE : View.VISIBLE);
+ }
+ }, StatsConstants.STATS_LABELS_SETUP_DELAY);
+ }
+
+ private void updateTimeframeAndDateAndStartRefreshOfFragments(boolean includeGraph) {
+ if (isFinishing()) {
+ return;
+ }
+ FragmentManager fm = getFragmentManager();
+
+ if (mCurrentTimeframe != StatsTimeframe.INSIGHTS) {
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsTopPostsAndPagesFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsReferrersFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsClicksFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsGeoviewsFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsAuthorsFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsVideoplaysFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsSearchTermsFragment.TAG);
+ if (includeGraph) {
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsVisitorsAndViewsFragment.TAG);
+ }
+ } else {
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsTodayFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsAllTimeFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsMostPopularFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsLatestPostSummaryFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsCommentsFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsTagsAndCategoriesFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsPublicizeFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsFollowersFragment.TAG);
+ }
+ }
+
+ private boolean updateTimeframeAndDateAndStartRefreshInFragment(FragmentManager fm , String fragmentTAG) {
+ StatsAbstractFragment fragment = (StatsAbstractFragment) fm.findFragmentByTag(fragmentTAG);
+ if (fragment != null) {
+ fragment.setDate(mRequestedDate);
+ fragment.setTimeframe(mCurrentTimeframe);
+ fragment.refreshStats();
+ return true;
+ }
+ return false;
+ }
+
+ private void startWPComLoginActivity() {
+ mResultCode = RESULT_CANCELED;
+ Intent signInIntent = new Intent(this, SignInActivity.class);
+ signInIntent.putExtra(SignInActivity.EXTRA_JETPACK_SITE_AUTH, mLocalBlogID);
+ signInIntent.putExtra(
+ SignInActivity.EXTRA_JETPACK_MESSAGE_AUTH,
+ getString(R.string.stats_sign_in_jetpack_different_com_account)
+ );
+ startActivityForResult(signInIntent, SignInActivity.REQUEST_CODE);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == SignInActivity.REQUEST_CODE) {
+ if (resultCode == RESULT_CANCELED) {
+ finish();
+ }
+ mResultCode = resultCode;
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (resultCode == RESULT_OK && currentBlog != null && !currentBlog.isDotcomFlag()) {
+ if (currentBlog.getDotComBlogId() == null) {
+ final Handler handler = new Handler();
+ // Attempt to get the Jetpack blog ID
+ XMLRPCClientInterface xmlrpcClient = XMLRPCFactory.instantiate(currentBlog.getUri(), "", "");
+ Map<String, String> args = ApiHelper.blogOptionsXMLRPCParameters;
+ Object[] params = {
+ currentBlog.getRemoteBlogId(), currentBlog.getUsername(), currentBlog.getPassword(), args
+ };
+ xmlrpcClient.callAsync(new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, Object result) {
+ if (result != null && (result instanceof HashMap)) {
+ Map<?, ?> blogOptions = (HashMap<?, ?>) result;
+ ApiHelper.updateBlogOptions(currentBlog, blogOptions);
+ AnalyticsUtils.refreshMetadata();
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.SIGNED_INTO_JETPACK, currentBlog);
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN, currentBlog);
+ if (isFinishing()) {
+ return;
+ }
+ // We have the blogID now, but we need to re-check if the network connection is available
+ if (NetworkUtils.checkConnection(StatsActivity.this)) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSwipeToRefreshHelper.setRefreshing(true);
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ createFragments(true); // Recreate the fragment and start a refresh of Stats
+ }
+ });
+ }
+ }
+ }
+ @Override
+ public void onFailure(long id, Exception error) {
+ AppLog.e(T.STATS,
+ "Cannot load blog options (wp.getOptions failed) "
+ + "and no jetpack_client_id is then available",
+ error);
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ ToastUtils.showToast(StatsActivity.this,
+ StatsActivity.this.getString(R.string.error_refresh_stats),
+ Duration.LONG);
+ }
+ });
+ }
+ }, Method.GET_OPTIONS, params);
+ } else {
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ createFragments(true); // Recreate the fragment and start a refresh of Stats
+ }
+ mSwipeToRefreshHelper.setRefreshing(true);
+ }
+ }
+ }
+
+ private class VerifyJetpackSettingsCallback implements ApiHelper.GenericCallback {
+ // AsyncTasks are bound to the Activity that launched it. If the user rotate the device StatsActivity is restarted.
+ // Use the event bus to fix this issue.
+
+ @Override
+ public void onSuccess() {
+ EventBus.getDefault().post(new StatsEvents.JetpackSettingsCompleted(false));
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ EventBus.getDefault().post(new StatsEvents.JetpackSettingsCompleted(true));
+ }
+ }
+
+ private void showJetpackNonConnectedAlert() {
+ if (isFinishing()) {
+ return;
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (currentBlog.isAdmin()) {
+ builder.setMessage(getString(R.string.jetpack_not_connected_message))
+ .setTitle(getString(R.string.jetpack_not_connected));
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String stringToLoad = currentBlog.getAdminUrl();
+ String jetpackConnectPageAdminPath = "admin.php?page=jetpack";
+ stringToLoad = stringToLoad.endsWith("/") ? stringToLoad + jetpackConnectPageAdminPath :
+ stringToLoad + "/" + jetpackConnectPageAdminPath;
+ String authURL = WPWebViewActivity.getBlogLoginUrl(currentBlog);
+ Intent jetpackIntent = new Intent(StatsActivity.this, WPWebViewActivity.class);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, currentBlog.getUsername());
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_PASSWD, currentBlog.getPassword());
+ jetpackIntent.putExtra(WPWebViewActivity.URL_TO_LOAD, stringToLoad);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, authURL);
+ startActivityForResult(jetpackIntent, REQUEST_JETPACK);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_SELECTED_CONNECT_JETPACK);
+ }
+ });
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User cancelled the dialog. Hide Stats.
+ finish();
+ }
+ });
+ } else {
+ builder.setMessage(getString(R.string.jetpack_message_not_admin))
+ .setTitle(getString(R.string.jetpack_not_found));
+ builder.setPositiveButton(R.string.yes, null);
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // User pressed the back key Hide Stats.
+ finish();
+ }
+ });
+ dialog.show();
+ }
+
+ private void showJetpackMissingAlert() {
+ if (isFinishing()) {
+ return;
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (currentBlog.isAdmin()) {
+ builder.setMessage(getString(R.string.jetpack_message))
+ .setTitle(getString(R.string.jetpack_not_found));
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String stringToLoad = currentBlog.getAdminUrl()
+ + "plugin-install.php?tab=search&s=jetpack+by+wordpress.com"
+ + "&plugin-search-input=Search+Plugins";
+ String authURL = WPWebViewActivity.getBlogLoginUrl(currentBlog);
+ Intent jetpackIntent = new Intent(StatsActivity.this, WPWebViewActivity.class);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, currentBlog.getUsername());
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_PASSWD, currentBlog.getPassword());
+ jetpackIntent.putExtra(WPWebViewActivity.URL_TO_LOAD, stringToLoad);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, authURL);
+ startActivityForResult(jetpackIntent, REQUEST_JETPACK);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_SELECTED_INSTALL_JETPACK);
+ }
+ });
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User cancelled the dialog. Hide Stats.
+ finish();
+ }
+ });
+ } else {
+ builder.setMessage(getString(R.string.jetpack_message_not_admin))
+ .setTitle(getString(R.string.jetpack_not_found));
+ builder.setPositiveButton(R.string.yes, null);
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // User pressed the back key Hide Stats.
+ finish();
+ }
+ });
+ dialog.show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int i = item.getItemId();
+ if (i == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void scrollToTop() {
+ mOuterScrollView.fullScroll(ScrollView.FOCUS_UP);
+ }
+
+ // StatsInsightsTodayFragment calls this when the user taps on a item in Today's Stats
+ @Override
+ public void onInsightsTodayClicked(final StatsVisitorsAndViewsFragment.OverviewLabel item) {
+ mTabToSelectOnGraph = item;
+ for (int i = 0; i < timeframes.length; i++) {
+ if (timeframes[i] == StatsTimeframe.DAY) {
+ mSpinner.setSelection(i);
+ break;
+ }
+ }
+ }
+
+ // StatsVisitorsAndViewsFragment calls this when the user taps on a bar in the graph
+ @Override
+ public void onDateChanged(String blogID, StatsTimeframe timeframe, String date) {
+ if (isFinishing()) {
+ return;
+ }
+ mRequestedDate = date;
+ updateTimeframeAndDateAndStartRefreshOfFragments(false);
+ if (NetworkUtils.checkConnection(StatsActivity.this)) {
+ mSwipeToRefreshHelper.setRefreshing(true);
+ } else {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+ }
+
+ // StatsVisitorsAndViewsFragment calls this when the user taps on the tab bar to change the type of the graph
+ @Override
+ public void onOverviewItemChanged(StatsVisitorsAndViewsFragment.OverviewLabel newItem) {
+ mTabToSelectOnGraph = newItem;
+ }
+
+ private boolean checkCredentials() {
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ AppLog.w(AppLog.T.STATS, "StatsActivity > cannot check credentials since no internet connection available");
+ return false;
+ }
+
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ return false;
+ }
+
+ final String blogId = currentBlog.getDotComBlogId();
+
+ // blogId is always available for dotcom blogs. It could be null on Jetpack blogs...
+ if (blogId != null) {
+ // for self-hosted sites; launch the user into an activity where they can provide their credentials
+ if (!currentBlog.isDotcomFlag()
+ && !currentBlog.hasValidJetpackCredentials() && mResultCode != RESULT_CANCELED) {
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ // Let's try the global wpcom credentials them first
+ String username = AccountHelper.getDefaultAccount().getUserName();
+ currentBlog.setDotcom_username(username);
+ WordPress.wpDB.saveBlog(currentBlog);
+ createFragments(true);
+ } else {
+ startWPComLoginActivity();
+ return false;
+ }
+ }
+ } else {
+ // blogId is null at this point.
+ if (!currentBlog.isDotcomFlag()) {
+ // Refresh blog settings/options that includes 'jetpack_client_id' needed here
+ mSwipeToRefreshHelper.setRefreshing(true);
+ new ApiHelper.RefreshBlogContentTask(currentBlog,
+ new VerifyJetpackSettingsCallback()).execute(false);
+ return false;
+ } else {
+ // blodID cannot be null on dotcom blogs.
+ Toast.makeText(this, R.string.error_refresh_stats, Toast.LENGTH_LONG).show();
+ AppLog.e(T.STATS, "blogID is null for a wpcom blog!! " + currentBlog.getHomeURL());
+ finish();
+ }
+ }
+
+ return true;
+ }
+
+ private void bumpPromoAnaylticsAndShowPromoDialogIfNecessary() {
+ if (mIsUpdatingStats || mThereWasAnErrorLoadingStats) {
+ // Do nothing in case of errors or when it's still loading
+ return;
+ }
+
+ if (!StringUtils.isEmpty(AppPrefs.getStatsWidgetsKeys())) {
+ // Stats widgets already used!!
+ return;
+ }
+
+ // Bump analytics that drives the Promo widget when the loading is completed without errors.
+ AppPrefs.bumpAnalyticsForStatsWidgetPromo();
+
+ // Should we display the widget promo?
+ int counter = AppPrefs.getAnalyticsForStatsWidgetPromo();
+ if (counter == 3 || counter == 1000 || counter == 10000) {
+ DialogFragment newFragment = PromoDialog.newInstance(R.drawable.stats_widget_promo_header,
+ R.string.stats_widget_promo_title, R.string.stats_widget_promo_desc,
+ R.string.stats_widget_promo_ok_btn_label);
+ newFragment.show(getFragmentManager(), "promote_widget_dialog");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.UpdateStatusChanged event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+ mSwipeToRefreshHelper.setRefreshing(event.mUpdating);
+ mIsUpdatingStats = event.mUpdating;
+
+ if (!mIsUpdatingStats && !mThereWasAnErrorLoadingStats) {
+ // Do not bump promo analytics or show the dialog in case of errors or when it's still loading
+ bumpPromoAnaylticsAndShowPromoDialogIfNecessary();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.JetpackSettingsCompleted event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+ mSwipeToRefreshHelper.setRefreshing(false);
+
+ if (!event.isError) {
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (currentBlog.getDotComBlogId() == null) {
+ if (TextUtils.isEmpty(currentBlog.getJetpackVersion())) {
+ // jetpack_version option is available, but not the jetpack_client_id ----> Jetpack available but not connected.
+ showJetpackNonConnectedAlert();
+ } else {
+ // Blog has not returned jetpack_version/jetpack_client_id.
+ showJetpackMissingAlert();
+ }
+ } else {
+ checkCredentials();
+ }
+ } else {
+ Toast.makeText(StatsActivity.this, R.string.error_refresh_stats, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ // There was an error loading Stats. Don't bump stats for promo widget.
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+
+ // There was an error loading Stats. Don't bump stats for promo widget.
+ mThereWasAnErrorLoadingStats = true;
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.JetpackAuthError event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+
+ // There was an error loading Stats. Don't bump stats for promo widget.
+ mThereWasAnErrorLoadingStats = true;
+
+ if (event.mLocalBlogId != mLocalBlogID) {
+ // The user has changed blog
+ return;
+ }
+
+ mSwipeToRefreshHelper.setRefreshing(false);
+ startWPComLoginActivity();
+ }
+
+ /*
+ * make sure the passed timeframe is the one selected in the actionbar
+ */
+ private void selectCurrentTimeframeInActionBar() {
+ if (isFinishing()) {
+ return;
+ }
+
+ if (mTimeframeSpinnerAdapter == null || mSpinner == null) {
+ return;
+ }
+
+ int position = mTimeframeSpinnerAdapter.getIndexOfTimeframe(mCurrentTimeframe);
+
+ if (position > -1 && position != mSpinner.getSelectedItemPosition()) {
+ mSpinner.setSelection(position);
+ }
+ }
+
+ /*
+ * adapter used by the timeframe spinner
+ */
+ private class TimeframeSpinnerAdapter extends BaseAdapter {
+ private final StatsTimeframe[] mTimeframes;
+ private final LayoutInflater mInflater;
+
+ TimeframeSpinnerAdapter(Context context, StatsTimeframe[] timeframeNames) {
+ super();
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mTimeframes = timeframeNames;
+ }
+
+ @Override
+ public int getCount() {
+ return (mTimeframes != null ? mTimeframes.length : 0);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ if (position < 0 || position >= getCount())
+ return "";
+ return mTimeframes[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view;
+ if (convertView == null) {
+ view = mInflater.inflate(R.layout.filter_spinner_item, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ final TextView text = (TextView) view.findViewById(R.id.text);
+ StatsTimeframe selectedTimeframe = (StatsTimeframe)getItem(position);
+ text.setText(selectedTimeframe.getLabel());
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ StatsTimeframe selectedTimeframe = (StatsTimeframe)getItem(position);
+ final TagViewHolder holder;
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.toolbar_spinner_dropdown_item, parent, false);
+ holder = new TagViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (TagViewHolder) convertView.getTag();
+ }
+
+ holder.textView.setText(selectedTimeframe.getLabel());
+ return convertView;
+ }
+
+ private class TagViewHolder {
+ private final TextView textView;
+ TagViewHolder(View view) {
+ textView = (TextView) view.findViewById(R.id.text);
+ }
+ }
+
+ public int getIndexOfTimeframe(StatsTimeframe tm) {
+ int pos = 0;
+ for (int i = 0; i < mTimeframes.length; i++) {
+ if (mTimeframes[i] == tm) {
+ pos = i;
+ return pos;
+ }
+ }
+ return pos;
+ }
+ }
+
+ @Override
+ public void onScrollChanged(NestedScrollViewExt scrollView, int x, int y, int oldx, int oldy) {
+ // We take the last son in the scrollview
+ View view = scrollView.getChildAt(scrollView.getChildCount() - 1);
+ if (view == null) {
+ return;
+ }
+ int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY() + view.getTop()));
+
+ // if diff is zero, then the bottom has been reached
+ if (diff == 0) {
+ sTrackBottomReachedStats.runIfNotLimited();
+ }
+ }
+
+ private static final RateLimitedTask sTrackBottomReachedStats = new RateLimitedTask(2) {
+ protected boolean run() {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_SCROLLED_TO_BOTTOM);
+ return true;
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java
new file mode 100644
index 000000000..65b55cc5c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java
@@ -0,0 +1,282 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.apache.commons.lang.StringUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.AuthorModel;
+import org.wordpress.android.ui.stats.models.AuthorsModel;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+public class StatsAuthorsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsAuthorsFragment.class.getSimpleName();
+
+ private AuthorsModel mAuthors;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mAuthors != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mAuthors);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mAuthors = (AuthorsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.AuthorsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mAuthors = event.mAuthors;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mAuthors = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!hasAuthors()) {
+ showHideNoResultsUI(true);
+ return;
+ }
+
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), mAuthors.getAuthors());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ }
+
+ private boolean hasAuthors() {
+ return mAuthors != null
+ && mAuthors.getAuthors() != null
+ && mAuthors.getAuthors().size() > 0;
+ }
+
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return (hasAuthors()
+ && mAuthors.getAuthors().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.AUTHORS
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_authors;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_top_posts_title;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_top_authors_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ public final Activity activity;
+ private final List<AuthorModel> authors;
+
+ public MyExpandableListAdapter(Activity act, List<AuthorModel> authors) {
+ this.activity = act;
+ this.authors = authors;
+ this.inflater = act.getLayoutInflater();
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ AuthorModel currentGroup = authors.get(groupPosition);
+ List<PostModel> posts = currentGroup.getPosts();
+ return posts.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final PostModel children = (PostModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ // The link icon
+ holder.showLinkIcon();
+
+ // name, url
+ holder.setEntryTextOpenDetailsPage(children);
+
+ // Setup the more button
+ holder.setMoreButtonOpenInReader(children);
+
+ // totals
+ int total = children.getTotals();
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ AuthorModel currentGroup = authors.get(groupPosition);
+ List<PostModel> posts = currentGroup.getPosts();
+ if (posts == null) {
+ return 0;
+ } else {
+ return posts.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return authors.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return authors.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ convertView.setTag(new StatsViewHolder(convertView));
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ AuthorModel group = (AuthorModel) getGroup(groupPosition);
+
+ String name = group.getName();
+ if (StringUtils.isBlank(name)) {
+ // Jetpack case: articles published before the activation of Jetpack.
+ name = getString(R.string.stats_unknown_author);
+ }
+ int total = group.getViews();
+ String icon = group.getAvatar();
+ int children = getChildrenCount(groupPosition);
+
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // icon
+ //holder.showNetworkImage(icon);
+ holder.networkImageView.setImageUrl(GravatarUtils.fixGravatarUrl(icon, mResourceVars.headerAvatarSizePx), WPNetworkImageView.ImageType.AVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ final FollowDataModel followData = group.getFollowData();
+ if (followData == null) {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ } else {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FollowHelper fh = new FollowHelper(activity);
+ fh.showPopup(holder.imgMore, followData);
+ }
+ });
+ }
+
+ if (children == 0) {
+ holder.showLinkIcon();
+ } else {
+ holder.showChevronIcon();
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_authors);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java
new file mode 100644
index 000000000..342c45821
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java
@@ -0,0 +1,335 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Shader;
+import android.support.v4.view.GestureDetectorCompat;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import com.jjoe64.graphview.CustomLabelFormatter;
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.GraphViewDataInterface;
+import com.jjoe64.graphview.GraphViewSeries.GraphViewSeriesStyle;
+import com.jjoe64.graphview.GraphViewStyle;
+import com.jjoe64.graphview.IndexDependentColor;
+
+import org.wordpress.android.R;
+import org.wordpress.android.widgets.TypefaceCache;
+
+import java.text.NumberFormat;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A Bar graph depicting the view and visitors.
+ * Based on BarGraph from the GraphView library.
+ */
+class StatsBarGraph extends GraphView {
+
+ private static final int DEFAULT_MAX_Y = 10;
+
+ // Keep tracks of every bar drawn on the graph.
+ private final List<List<BarChartRect>> mSeriesRectsDrawedOnScreen = (List<List<BarChartRect>>) new LinkedList();
+ private int mBarPositionToHighlight = -1;
+ private boolean[] mWeekendDays;
+
+ private final GestureDetectorCompat mDetector;
+ private OnGestureListener mGestureListener;
+
+ public StatsBarGraph(Context context) {
+ super(context, "");
+
+ int width = LayoutParams.MATCH_PARENT;
+ int height = getResources().getDimensionPixelSize(R.dimen.stats_barchart_height);
+ setLayoutParams(new LayoutParams(width, height));
+
+ setProperties();
+
+ paint.setTypeface(TypefaceCache.getTypeface(getContext()));
+
+ mDetector = new GestureDetectorCompat(getContext(), new MyGestureListener());
+ mDetector.setIsLongpressEnabled(false);
+ }
+
+ public void setGestureListener(OnGestureListener listener) {
+ this.mGestureListener = listener;
+ }
+
+ class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onDown(MotionEvent event) {
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent event) {
+ highlightBarAndBroadcastDate();
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ highlightBarAndBroadcastDate();
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ private void highlightBarAndBroadcastDate() {
+ int tappedBar = getTappedBar();
+ //AppLog.d(AppLog.T.STATS, this.getClass().getName() + " Tapped bar " + tappedBar);
+ if (tappedBar >= 0) {
+ highlightBar(tappedBar);
+ if (mGestureListener != null) {
+ mGestureListener.onBarTapped(tappedBar);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent (MotionEvent event) {
+ boolean handled = super.onTouchEvent(event);
+ if (mDetector != null && handled) {
+ this.mDetector.onTouchEvent(event);
+ }
+ return handled;
+ }
+
+ private class HorizontalLabelsColor implements IndexDependentColor {
+ public int get(int index) {
+ if (mBarPositionToHighlight == index) {
+ return getResources().getColor(R.color.orange_jazzy);
+ } else {
+ return getResources().getColor(R.color.grey_darken_30);
+ }
+ }
+ }
+
+ private void setProperties() {
+ GraphViewStyle gStyle = getGraphViewStyle();
+ gStyle.setHorizontalLabelsIndexDependentColor(new HorizontalLabelsColor());
+ gStyle.setHorizontalLabelsColor(getResources().getColor(R.color.grey_darken_30));
+ gStyle.setVerticalLabelsColor(getResources().getColor(R.color.grey_darken_10));
+ gStyle.setTextSize(getResources().getDimensionPixelSize(R.dimen.text_sz_extra_small));
+ gStyle.setGridXColor(Color.TRANSPARENT);
+ gStyle.setGridYColor(getResources().getColor(R.color.grey_lighten_30));
+ gStyle.setNumVerticalLabels(3);
+
+ setCustomLabelFormatter(new CustomLabelFormatter() {
+ private NumberFormat numberFormatter;
+
+ @Override
+ public String formatLabel(double value, boolean isValueX) {
+ if (isValueX) {
+ return null;
+ }
+ if (numberFormatter == null) {
+ numberFormatter = NumberFormat.getNumberInstance();
+ numberFormatter.setMaximumFractionDigits(0);
+ }
+ return numberFormatter.format(value);
+ }
+ });
+ }
+
+ @Override
+ protected void onBeforeDrawSeries() {
+ mSeriesRectsDrawedOnScreen.clear();
+ }
+
+ @Override
+ public void drawSeries(Canvas canvas, GraphViewDataInterface[] values,
+ float graphwidth, float graphheight, float border, double minX,
+ double minY, double diffX, double diffY, float horstart,
+ GraphViewSeriesStyle style) {
+ float colwidth = graphwidth / values.length;
+ int maxColumnSize = getGraphViewStyle().getMaxColumnWidth();
+ if (maxColumnSize > 0 && colwidth > maxColumnSize) {
+ colwidth = maxColumnSize;
+ }
+
+ paint.setStrokeWidth(style.thickness);
+ paint.setColor(style.color);
+
+ // Bar chart position of this series on the canvas
+ List<BarChartRect> barChartRects = new LinkedList<>();
+
+ // draw data
+ for (int i = 0; i < values.length; i++) {
+ float valY = (float) (values[i].getY() - minY);
+ float ratY = (float) (valY / diffY);
+ float y = graphheight * ratY;
+
+ // hook for value dependent color
+ if (style.getValueDependentColor() != null) {
+ paint.setColor(style.getValueDependentColor().get(values[i]));
+ }
+
+ float pad = style.padding;
+
+ float left = (i * colwidth) + horstart;
+ float top = (border - y) + graphheight;
+ float right = left + colwidth;
+ float bottom = graphheight + border - 1;
+
+ // Draw the orange selection behind the selected bar
+ if (style.outerhighlightColor != 0x00ffffff && mBarPositionToHighlight == i) {
+ paint.setColor(style.outerhighlightColor);
+ canvas.drawRect(left, 10f, right, bottom, paint);
+ }
+
+ // Draw the grey background color on weekend days
+ if (style.outerColor != 0x00ffffff
+ && mBarPositionToHighlight != i
+ && mWeekendDays != null && mWeekendDays[i]) {
+ paint.setColor(style.outerColor);
+ canvas.drawRect(left, 10f, right, bottom, paint);
+ }
+
+ if ((top - bottom) == 1) {
+ // draw a placeholder
+ if (mBarPositionToHighlight != i) {
+ paint.setColor(style.color);
+ paint.setAlpha(25);
+ Shader shader = new LinearGradient(left + pad, bottom - 50, left + pad, bottom, Color.WHITE, Color.BLACK, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ canvas.drawRect(left + pad, bottom - 50, right - pad, bottom, paint);
+ paint.setShader(null);
+ }
+ } else {
+ // draw a real bar
+ paint.setAlpha(255);
+ if (mBarPositionToHighlight == i) {
+ paint.setColor(style.highlightColor);
+ } else {
+ paint.setColor(style.color);
+ }
+ canvas.drawRect(left + pad, top, right - pad, bottom, paint);
+ }
+
+ barChartRects.add(new BarChartRect(left + pad, top, right - pad, bottom));
+ }
+ mSeriesRectsDrawedOnScreen.add(barChartRects);
+ }
+
+ private int getTappedBar() {
+ float[] lastBarChartTouchedPoint = this.getLastTouchedPointOnCanvasAndReset();
+ if (lastBarChartTouchedPoint[0] == 0f && lastBarChartTouchedPoint[1] == 0f) {
+ return -1;
+ }
+ for (List<BarChartRect> currentSerieChartRects : mSeriesRectsDrawedOnScreen) {
+ int i = 0;
+ for (BarChartRect barChartRect : currentSerieChartRects) {
+ if (barChartRect.isPointInside(lastBarChartTouchedPoint[0], lastBarChartTouchedPoint[1])) {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+ }
+/*
+ public float getMiddlePointOfTappedBar(int tappedBar) {
+ if (tappedBar == -1 || mSeriesRectsDrawedOnScreen == null || mSeriesRectsDrawedOnScreen.size() == 0) {
+ return -1;
+ }
+ BarChartRect rect = mSeriesRectsDrawedOnScreen.get(0).get(tappedBar);
+
+ return ((rect.mLeft + rect.mRight) / 2) + getCanvasLeft();
+ }
+
+ public void highlightAndDismissBar(int barPosition) {
+ mBarPositionToHighlight = barPosition;
+ if (mBarPositionToHighlight == -1) {
+ return;
+ }
+ this.redrawAll();
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mBarPositionToHighlight = -1;
+ redrawAll();
+ }
+ }, 500);
+ }
+*/
+
+ public void setWeekendDays(boolean[] days) {
+ mWeekendDays = days;
+ }
+
+ public void highlightBar(int barPosition) {
+ mBarPositionToHighlight = barPosition;
+ this.redrawAll();
+ }
+
+ public int getHighlightBar() {
+ return mBarPositionToHighlight;
+ }
+
+ public void resetHighlightBar() {
+ mBarPositionToHighlight = -1;
+ }
+
+ @Override
+ protected double getMinY() {
+ return 0;
+ }
+
+ // Make sure the highest number is always even, so the halfway mark is correctly balanced in the middle of the graph
+ // Also make sure to display a default value when there is no activity in the period.
+ @Override
+ protected double getMaxY() {
+ double maxY = super.getMaxY();
+ if (maxY == 0) {
+ return DEFAULT_MAX_Y;
+ }
+
+ return maxY + (maxY % 2);
+ }
+
+ /**
+ * Private class that is used to hold the local (to the canvas) coordinate on the screen
+ * of every single bar in the graph
+ */
+ private class BarChartRect {
+ final float mLeft;
+ final float mTop;
+ final float mRight;
+ final float mBottom;
+
+ BarChartRect(float left, float top, float right, float bottom) {
+ this.mLeft = left;
+ this.mTop = top;
+ this.mRight = right;
+ this.mBottom = bottom;
+ }
+
+ /**
+ * Check if the tap happens on a bar in the graph.
+ *
+ * @return true if the tap point falls within the bar for the X coordinate, and within the full canvas
+ * height for the Y coordinate. This is a fix to make very small bars tappable.
+ */
+ public boolean isPointInside(float x, float y) {
+ return x >= this.mLeft
+ && x <= this.mRight;
+ }
+ }
+
+ interface OnGestureListener {
+ void onBarTapped(int tappedBar);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java
new file mode 100644
index 000000000..edb4ef953
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java
@@ -0,0 +1,266 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.ClickGroupModel;
+import org.wordpress.android.ui.stats.models.ClicksModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+public class StatsClicksFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsClicksFragment.class.getSimpleName();
+
+ private ClicksModel mClicks;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mClicks != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mClicks);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mClicks = (ClicksModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.ClicksUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mClicks = event.mClicks;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mClicks = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasClicks()) {
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), mClicks.getClickGroups());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasClicks() {
+ return mClicks != null
+ && mClicks.getClickGroups() != null
+ && mClicks.getClickGroups().size() > 0;
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return (hasClicks() && mClicks.getClickGroups().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.CLICKS
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_clicks_link;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_clicks;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_clicks_title;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_clicks_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ private final List<ClickGroupModel> clickGroups;
+
+ public MyExpandableListAdapter(Context context, List<ClickGroupModel> clickGroups) {
+ this.clickGroups = clickGroups;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ ClickGroupModel currentGroup = clickGroups.get(groupPosition);
+ List<SingleItemModel> results = currentGroup.getClicks();
+ return results.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final SingleItemModel children = (SingleItemModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ // The link icon
+ holder.showLinkIcon();
+
+ // name, url
+ holder.setEntryTextOrLink(children.getUrl(), children.getTitle());
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(
+ children.getTotals()
+ ));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ ClickGroupModel currentGroup = clickGroups.get(groupPosition);
+ List<SingleItemModel> clicks = currentGroup.getClicks();
+ if (clicks == null) {
+ return 0;
+ } else {
+ return clicks.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return clickGroups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return clickGroups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ ClickGroupModel group = (ClickGroupModel) getGroup(groupPosition);
+
+ String name = group.getName();
+ int total = group.getViews();
+ String url = group.getUrl();
+ String icon = group.getIcon();
+ int children = getChildrenCount(groupPosition);
+
+ if (children > 0) {
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ holder.setEntryTextOrLink(url, name);
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // Site icon
+ holder.networkImageView.setVisibility(View.GONE);
+ if (!TextUtils.isEmpty(icon)) {
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(icon, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.GONE_UNTIL_AVAILABLE
+ );
+ }
+
+ if (children == 0) {
+ holder.showLinkIcon();
+ } else {
+ holder.showChevronIcon();
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_clicks);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java
new file mode 100644
index 000000000..bb13d37f3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java
@@ -0,0 +1,280 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.adapters.PostsAndPagesAdapter;
+import org.wordpress.android.ui.stats.models.AuthorModel;
+import org.wordpress.android.ui.stats.models.CommentFollowersModel;
+import org.wordpress.android.ui.stats.models.CommentsModel;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsCommentsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsCommentsFragment.class.getSimpleName();
+ static final String ARG_REST_RESPONSE_FOLLOWERS = "ARG_REST_RESPONSE_FOLLOWERS";
+
+ private CommentsModel mCommentsModel;
+ private CommentFollowersModel mCommentFollowersModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mCommentsModel != null && mCommentFollowersModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mCommentsModel != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mCommentsModel);
+ }
+ if (mCommentFollowersModel != null) {
+ outState.putSerializable(ARG_REST_RESPONSE_FOLLOWERS, mCommentFollowersModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mCommentsModel = (CommentsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE_FOLLOWERS)) {
+ mCommentFollowersModel = (CommentFollowersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE_FOLLOWERS);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.CommentsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mCommentsModel = event.mComments;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.CommentFollowersUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mCommentFollowersModel = event.mCommentFollowers;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mCommentsModel = null;
+ mCommentFollowersModel = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ Resources res = container.getContext().getResources();
+ String[] titles = {
+ res.getString(R.string.stats_comments_by_authors),
+ res.getString(R.string.stats_comments_by_posts_and_pages),
+ };
+
+ setupTopModulePager(inflater, container, view, titles);
+
+ return view;
+ }
+
+ @Override
+ protected void updateUI() {
+ // This module is a kind of exception to the normal way we build page interface.
+ // In this module only the first rest endpoint StatsService.StatsEndpointsEnum.COMMENTS
+ // is used to populate 99% of the UI even if there is a tab on the top.
+ // Switching to a different tab on the UI doesn't switch the underlying datamodel index as in all other modules.
+
+ if (!isAdded()) {
+ return;
+ }
+
+ if (mCommentsModel == null && mCommentFollowersModel == null) {
+ showHideNoResultsUI(true);
+ mTotalsLabel.setVisibility(View.GONE);
+ mTopPagerContainer.setVisibility(View.GONE);
+ return;
+ }
+
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+
+ if (mCommentFollowersModel != null) { // check if comment-followers is already here
+ mTotalsLabel.setVisibility(View.VISIBLE);
+ int totalNumberOfFollowers = mCommentFollowersModel.getTotal();
+ String totalCommentsFollowers = getString(R.string.stats_comments_total_comments_followers);
+ mTotalsLabel.setText(
+ String.format(totalCommentsFollowers, FormatUtils.formatDecimal(totalNumberOfFollowers))
+ );
+ }
+
+ ArrayAdapter adapter = null;
+
+ if (mTopPagerSelectedButtonIndex == 0 && hasAuthors()) {
+ adapter = new AuthorsAdapter(getActivity(), getAuthors());
+ } else if (mTopPagerSelectedButtonIndex == 1 && hasPosts()) {
+ adapter = new PostsAndPagesAdapter(getActivity(), getPosts());
+ }
+
+ if (adapter != null) {
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasAuthors() {
+ return mCommentsModel != null
+ && mCommentsModel.getAuthors() != null
+ && mCommentsModel.getAuthors().size() > 0;
+ }
+
+ private List<AuthorModel> getAuthors() {
+ if (!hasAuthors()) {
+ return new ArrayList<AuthorModel>(0);
+ }
+ return mCommentsModel.getAuthors();
+ }
+
+ private boolean hasPosts() {
+ return mCommentsModel != null
+ && mCommentsModel.getPosts() != null
+ && mCommentsModel.getPosts().size() > 0;
+ }
+
+ private List<PostModel> getPosts() {
+ if (!hasPosts()) {
+ return new ArrayList<PostModel>(0);
+ }
+ return mCommentsModel.getPosts();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ if (mTopPagerSelectedButtonIndex == 0 && hasAuthors() && getAuthors().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST) {
+ return true;
+ } else if (mTopPagerSelectedButtonIndex == 1 && hasPosts() && getPosts().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class AuthorsAdapter extends ArrayAdapter<AuthorModel> {
+
+ private final List<AuthorModel> list;
+ private final Activity context;
+ private final LayoutInflater inflater;
+
+ public AuthorsAdapter(Activity context, List<AuthorModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.context = context;
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final AuthorModel currentRowData = list.get(position);
+ final StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ // entries
+ holder.setEntryText(currentRowData.getName(), getResources().getColor(R.color.stats_text_color));
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getViews()));
+
+ // avatar
+ holder.networkImageView.setImageUrl(GravatarUtils.fixGravatarUrl(currentRowData.getAvatar(), mResourceVars.headerAvatarSizePx), WPNetworkImageView.ImageType.AVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ final FollowDataModel followData = currentRowData.getFollowData();
+ if (followData == null) {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ } else {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FollowHelper fh = new FollowHelper(context);
+ fh.showPopup(holder.imgMore, followData);
+ }
+ });
+ }
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ if (mTopPagerSelectedButtonIndex == 0) {
+ return R.string.stats_entry_top_commenter;
+ } else {
+ return R.string.stats_entry_posts_and_pages;
+ }
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_comments;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_comments;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_comments_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.COMMENTS, StatsService.StatsEndpointsEnum.COMMENT_FOLLOWERS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_comments);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java
new file mode 100644
index 000000000..fd1a456d8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.ui.stats;
+
+public class StatsConstants {
+
+ // Date formatting constants
+ public static final String STATS_INPUT_DATE_FORMAT = "yyyy-MM-dd";
+ public static final String STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT = "MMM d";
+ public static final String STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT = "MMMM d";
+ public static final String STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT = "MMMM dd";
+ public static final String STATS_OUTPUT_DATE_MONTH_LONG_FORMAT = "MMMM";
+ public static final String STATS_OUTPUT_DATE_YEAR_FORMAT = "yyyy";
+
+ public static final int STATS_GRAPH_BAR_MAX_COLUMN_WIDTH_DP = 100;
+
+ public static final long STATS_SCROLL_TO_DELAY = 75L;
+ public static final long STATS_LABELS_SETUP_DELAY = 75L;
+
+ public static final String ITEM_TYPE_POST = "post";
+ public static final String ITEM_TYPE_PAGE = "page";
+ public static final String ITEM_TYPE_HOME_PAGE = "homepage";
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java
new file mode 100644
index 000000000..b5b604303
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java
@@ -0,0 +1,276 @@
+package org.wordpress.android.ui.stats;
+
+import com.android.volley.VolleyError;
+
+import org.wordpress.android.ui.stats.models.AuthorsModel;
+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.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.StatsEndpointsEnum;
+
+public class StatsEvents {
+ public static class UpdateStatusChanged {
+ public final boolean mUpdating;
+ public UpdateStatusChanged(boolean updating) {
+ mUpdating = updating;
+ }
+ }
+
+ public abstract static class SectionUpdatedAbstract {
+ public final String mRequestBlogId; // This is the remote blog ID
+ public final StatsTimeframe mTimeframe;
+ public final String mDate;
+ public final int mMaxResultsRequested, mPageRequested;
+
+ public SectionUpdatedAbstract(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested) {
+ mRequestBlogId = blogId;
+ mDate = date;
+ mTimeframe = timeframe;
+ mMaxResultsRequested = maxResultsRequested;
+ mPageRequested = pageRequested;
+ }
+ }
+
+ public static class SectionUpdateError extends SectionUpdatedAbstract {
+
+ public final VolleyError mError;
+ public final StatsEndpointsEnum mEndPointName;
+
+ public SectionUpdateError(StatsEndpointsEnum endPointName, String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, VolleyError error) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mEndPointName = endPointName;
+ mError = error;
+ }
+ }
+
+ public static class VisitorsAndViewsUpdated extends SectionUpdatedAbstract {
+
+ public final VisitsModel mVisitsAndViews;
+
+ public VisitorsAndViewsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, VisitsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mVisitsAndViews = responseObjectModel;
+ }
+ }
+
+ public static class TopPostsUpdated extends SectionUpdatedAbstract {
+
+ public final TopPostsAndPagesModel mTopPostsAndPagesModel;
+
+ public TopPostsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, TopPostsAndPagesModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mTopPostsAndPagesModel = responseObjectModel;
+ }
+ }
+
+ public static class ReferrersUpdated extends SectionUpdatedAbstract {
+
+ public final ReferrersModel mReferrers;
+
+ public ReferrersUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, ReferrersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mReferrers = responseObjectModel;
+ }
+ }
+
+ public static class ClicksUpdated extends SectionUpdatedAbstract {
+
+ public final ClicksModel mClicks;
+
+ public ClicksUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, ClicksModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mClicks = responseObjectModel;
+ }
+ }
+
+
+ public static class AuthorsUpdated extends SectionUpdatedAbstract {
+
+ public final AuthorsModel mAuthors;
+
+ public AuthorsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, AuthorsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mAuthors = responseObjectModel;
+ }
+ }
+
+ public static class CountriesUpdated extends SectionUpdatedAbstract {
+
+ public final GeoviewsModel mCountries;
+
+ public CountriesUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, GeoviewsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mCountries = responseObjectModel;
+ }
+ }
+
+ public static class VideoPlaysUpdated extends SectionUpdatedAbstract {
+
+ public final VideoPlaysModel mVideos;
+
+ public VideoPlaysUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, VideoPlaysModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mVideos = responseObjectModel;
+ }
+ }
+
+ public static class SearchTermsUpdated extends SectionUpdatedAbstract {
+
+ public final SearchTermsModel mSearchTerms;
+
+ public SearchTermsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, SearchTermsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mSearchTerms = responseObjectModel;
+ }
+ }
+
+ public static class CommentsUpdated extends SectionUpdatedAbstract {
+
+ public final CommentsModel mComments;
+
+ public CommentsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, CommentsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mComments = responseObjectModel;
+ }
+ }
+
+ public static class CommentFollowersUpdated extends SectionUpdatedAbstract {
+
+ public final CommentFollowersModel mCommentFollowers;
+
+ public CommentFollowersUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, CommentFollowersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mCommentFollowers = responseObjectModel;
+ }
+ }
+
+ public static class TagsUpdated extends SectionUpdatedAbstract {
+
+ public final TagsContainerModel mTagsContainer;
+
+ public TagsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, TagsContainerModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mTagsContainer = responseObjectModel;
+ }
+ }
+
+ public static class PublicizeUpdated extends SectionUpdatedAbstract {
+
+ public final PublicizeModel mPublicizeModel;
+
+ public PublicizeUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, PublicizeModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mPublicizeModel = responseObjectModel;
+ }
+ }
+
+ public static class FollowersWPCOMUdated extends SectionUpdatedAbstract {
+
+ public final FollowersModel mFollowers;
+
+ public FollowersWPCOMUdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, FollowersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mFollowers = responseObjectModel;
+ }
+ }
+
+ public static class FollowersEmailUdated extends SectionUpdatedAbstract {
+
+ public final FollowersModel mFollowers;
+
+ public FollowersEmailUdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, FollowersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mFollowers = responseObjectModel;
+ }
+ }
+
+ public static class InsightsAllTimeUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsAllTimeModel mInsightsAllTimeModel;
+
+ public InsightsAllTimeUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, InsightsAllTimeModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsAllTimeModel = responseObjectModel;
+ }
+ }
+
+ public static class InsightsPopularUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsPopularModel mInsightsPopularModel;
+
+ public InsightsPopularUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, InsightsPopularModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsPopularModel = responseObjectModel;
+ }
+ }
+
+ public static class InsightsLatestPostSummaryUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsLatestPostModel mInsightsLatestPostModel;
+
+ public InsightsLatestPostSummaryUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested,
+ InsightsLatestPostModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsLatestPostModel = responseObjectModel;
+ }
+ }
+
+ public static class InsightsLatestPostDetailsUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsLatestPostDetailsModel mInsightsLatestPostDetailsModel;
+
+ public InsightsLatestPostDetailsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested,
+ InsightsLatestPostDetailsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsLatestPostDetailsModel = responseObjectModel;
+ }
+ }
+
+ public static class JetpackSettingsCompleted {
+ public final boolean isError;
+ public JetpackSettingsCompleted(boolean isError) {
+ this.isError = isError;
+ }
+ }
+
+ public static class JetpackAuthError {
+ public final int mLocalBlogId; // This is the local blogID
+
+ public JetpackAuthError(int blogId) {
+ mLocalBlogId = blogId;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java
new file mode 100644
index 000000000..dc15eab17
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java
@@ -0,0 +1,449 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.FollowerModel;
+import org.wordpress.android.ui.stats.models.FollowersModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+
+public class StatsFollowersFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsFollowersFragment.class.getSimpleName();
+
+ private static final String ARG_REST_RESPONSE_FOLLOWERS_EMAIL = "ARG_REST_RESPONSE_FOLLOWERS_EMAIL";
+ private final Map<String, Integer> userBlogs = new HashMap<>();
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ Resources res = container.getContext().getResources();
+
+ String[] titles = {
+ res.getString(R.string.stats_followers_wpcom_selector),
+ res.getString(R.string.stats_followers_email_selector),
+ };
+
+
+ setupTopModulePager(inflater, container, view, titles);
+
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ mTotalsLabel.setVisibility(View.VISIBLE);
+ mTotalsLabel.setText("");
+
+ return view;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Single background thread used to create the blogs list in BG
+ ThreadPoolExecutor blogsListCreatorExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
+ blogsListCreatorExecutor.submit(new Thread() {
+ @Override
+ public void run() {
+ // Read all the dotcomBlog blogs and get the list of home URLs.
+ // This will be used later to check if the user is a member of followers blog marked as private.
+ List<Map<String, Object>> dotComUserBlogs = WordPress.wpDB.getBlogsBy("dotcomFlag=1",
+ new String[]{"homeURL"});
+ for (Map<String, Object> blog : dotComUserBlogs) {
+ if (blog != null && blog.get("homeURL") != null && blog.get("blogId") != null) {
+ String normURL = normalizeAndRemoveScheme(blog.get("homeURL").toString());
+ Integer blogID = (Integer) blog.get("blogId");
+ userBlogs.put(normURL, blogID);
+ }
+ }
+ }
+ });
+ }
+
+ private FollowersModel mFollowersWPCOM;
+ private FollowersModel mFollowersEmail;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mFollowersWPCOM != null || mFollowersEmail != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mFollowersWPCOM != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mFollowersWPCOM);
+ }
+ if (mFollowersEmail != null) {
+ outState.putSerializable(ARG_REST_RESPONSE_FOLLOWERS_EMAIL, mFollowersEmail);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mFollowersWPCOM = (FollowersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE_FOLLOWERS_EMAIL)) {
+ mFollowersEmail = (FollowersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE_FOLLOWERS_EMAIL);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.FollowersWPCOMUdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mFollowersWPCOM = event.mFollowers;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.FollowersEmailUdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mFollowersEmail = event.mFollowers;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mFollowersWPCOM = null;
+ mFollowersEmail = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!hasDataAvailable()) {
+ showHideNoResultsUI(true);
+ mTotalsLabel.setText(getTotalFollowersLabel(0));
+ return;
+ }
+
+ mTotalsLabel.setVisibility(View.VISIBLE);
+
+ final FollowersModel followersModel = getCurrentDataModel();
+
+ if (followersModel != null && followersModel.getFollowers() != null &&
+ followersModel.getFollowers().size() > 0) {
+ ArrayAdapter adapter = new DotComFollowerAdapter(getActivity(), followersModel.getFollowers());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+
+ if (mTopPagerSelectedButtonIndex == 0) {
+ mTotalsLabel.setText(getTotalFollowersLabel(followersModel.getTotalWPCom()));
+ } else {
+ mTotalsLabel.setText(getTotalFollowersLabel(followersModel.getTotalEmail()));
+ }
+
+ if (isSingleView()) {
+ if (followersModel.getPages() > 1) {
+ mBottomPaginationContainer.setVisibility(View.VISIBLE);
+ mTopPaginationContainer.setVisibility(View.VISIBLE);
+ String paginationLabel = String.format(
+ getString(R.string.stats_pagination_label),
+ FormatUtils.formatDecimal(followersModel.getPage()),
+ FormatUtils.formatDecimal(followersModel.getPages())
+ );
+ mBottomPaginationText.setText(paginationLabel);
+ mTopPaginationText.setText(paginationLabel);
+ setNavigationButtonsEnabled(true);
+
+ // Setting up back buttons
+ if (followersModel.getPage() == 1) {
+ // first page. No go back buttons
+ setNavigationBackButtonsVisibility(false);
+ } else {
+ setNavigationBackButtonsVisibility(true);
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setNavigationButtonsEnabled(false);
+ refreshStats(
+ followersModel.getPage() - 1,
+ new StatsService.StatsEndpointsEnum[]{sectionsToUpdate()[mTopPagerSelectedButtonIndex]}
+ );
+ }
+ };
+ mBottomPaginationGoBackButton.setOnClickListener(clickListener);
+ mTopPaginationGoBackButton.setOnClickListener(clickListener);
+ }
+
+ // Setting up forward buttons
+ if (followersModel.getPage() == followersModel.getPages()) {
+ // last page. No go forward buttons
+ setNavigationForwardButtonsVisibility(false);
+ } else {
+ setNavigationForwardButtonsVisibility(true);
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setNavigationButtonsEnabled(false);
+ refreshStats(
+ followersModel.getPage() + 1,
+ new StatsService.StatsEndpointsEnum[]{sectionsToUpdate()[mTopPagerSelectedButtonIndex]}
+ );
+ }
+ };
+ mBottomPaginationGoForwardButton.setOnClickListener(clickListener);
+ mTopPaginationGoForwardButton.setOnClickListener(clickListener);
+ }
+
+ // Change the total number of followers label by adding the current paging info
+ int startIndex = followersModel.getPage() * StatsService.MAX_RESULTS_REQUESTED_PER_PAGE - StatsService.MAX_RESULTS_REQUESTED_PER_PAGE + 1;
+ int endIndex = startIndex + followersModel.getFollowers().size() - 1;
+ String pagedLabel = getString(
+ mTopPagerSelectedButtonIndex == 0 ? R.string.stats_followers_total_wpcom_paged : R.string.stats_followers_total_email_paged,
+ startIndex,
+ endIndex,
+ FormatUtils.formatDecimal(mTopPagerSelectedButtonIndex == 0 ? followersModel.getTotalWPCom() : followersModel.getTotalEmail())
+ );
+ mTotalsLabel.setText(pagedLabel);
+ } else {
+ // No paging required. Hide the controls.
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTopPaginationContainer.setVisibility(View.GONE);
+ }
+ }
+ } else {
+ showHideNoResultsUI(true);
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTotalsLabel.setText(getTotalFollowersLabel(0));
+ }
+
+ // Always visible. Even if the current tab is empty, otherwise the user can't switch tab
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ }
+
+ private FollowersModel getCurrentDataModel() {
+ return mTopPagerSelectedButtonIndex == 0 ? mFollowersWPCOM : mFollowersEmail;
+ }
+
+ private void setNavigationBackButtonsVisibility(boolean visible) {
+ mBottomPaginationGoBackButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ mTopPaginationGoBackButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void setNavigationForwardButtonsVisibility(boolean visible) {
+ mBottomPaginationGoForwardButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ mTopPaginationGoForwardButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void setNavigationButtonsEnabled(boolean enable) {
+ mBottomPaginationGoBackButton.setEnabled(enable);
+ mBottomPaginationGoForwardButton.setEnabled(enable);
+ mTopPaginationGoBackButton.setEnabled(enable);
+ mTopPaginationGoForwardButton.setEnabled(enable);
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ if (!hasDataAvailable()) {
+ return false;
+ }
+ FollowersModel followersModel = getCurrentDataModel();
+ return !(followersModel == null || followersModel.getFollowers() == null
+ || followersModel.getFollowers().size() < MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+
+ }
+
+ private String getTotalFollowersLabel(int total) {
+ final String totalFollowersLabel;
+
+ if (mTopPagerSelectedButtonIndex == 0) {
+ totalFollowersLabel = getString(R.string.stats_followers_total_wpcom);
+ } else {
+ totalFollowersLabel = getString(R.string.stats_followers_total_email);
+ }
+
+ return String.format(totalFollowersLabel, FormatUtils.formatDecimal(total));
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class DotComFollowerAdapter extends ArrayAdapter<FollowerModel> {
+
+ private final List<FollowerModel> list;
+ private final Activity context;
+ private final LayoutInflater inflater;
+
+ public DotComFollowerAdapter(Activity context, List<FollowerModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.context = context;
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // set a min-width value that is large enough to contains the "since" string
+ LinearLayout totalContainer = (LinearLayout) rowView.findViewById(R.id.stats_list_cell_total_container);
+ int dp64 = DisplayUtils.dpToPx(rowView.getContext(), 64);
+ totalContainer.setMinimumWidth(dp64);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final FollowerModel currentRowData = list.get(position);
+ final StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ holder.entryTextView.setTextColor(context.getResources().getColor(R.color.stats_text_color));
+ holder.rowContent.setClickable(false);
+
+ final FollowDataModel followData = currentRowData.getFollowData();
+
+ // entries
+ if (mTopPagerSelectedButtonIndex == 0 && !(TextUtils.isEmpty(currentRowData.getURL()) && followData == null)) {
+ // WPCOM followers with no empty URL or empty follow data
+
+ final int blogID;
+ if (followData == null) {
+ // If follow data is empty, we cannot follow the blog, or access it in the reader.
+ // We need to check if the user is a member of this blog.
+ // If so, we can launch open the reader, otherwise open the blog in the in-app browser.
+ String normURL = normalizeAndRemoveScheme(currentRowData.getURL());
+ blogID = userBlogs.containsKey(normURL) ? userBlogs.get(normURL) : Integer.MIN_VALUE;
+ } else {
+ blogID = followData.getSiteID();
+ }
+
+ if (blogID > Integer.MIN_VALUE) {
+ // Open the Reader
+ holder.entryTextView.setText(currentRowData.getLabel());
+ holder.rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ context,
+ blogID
+ );
+ }
+ });
+ } else {
+ // Open the in-app web browser
+ holder.setEntryTextOrLink(currentRowData.getURL(), currentRowData.getLabel());
+ }
+ holder.entryTextView.setTextColor(context.getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ // Email followers, or wpcom followers with empty URL and no blogID
+ holder.setEntryText(currentRowData.getLabel());
+ }
+
+ // since date
+ holder.totalsTextView.setText(
+ StatsUtils.getSinceLabel(
+ context,
+ currentRowData.getDateSubscribed()
+ )
+ );
+
+ // Avatar
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(currentRowData.getAvatar(), mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.AVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ if (followData == null) {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ } else {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FollowHelper fh = new FollowHelper(context);
+ fh.showPopup(holder.imgMore, followData);
+ }
+ });
+ }
+
+ return rowView;
+ }
+
+
+ }
+
+ private static String normalizeAndRemoveScheme(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return "";
+ }
+ String normURL = UrlUtils.normalizeUrl(url.toLowerCase());
+ int pos = normURL.indexOf("://");
+ if (pos > -1) {
+ return normURL.substring(pos + 3);
+ } else {
+ return normURL;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_followers;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_followers;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_followers;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_followers_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.FOLLOWERS_WPCOM, StatsService.StatsEndpointsEnum.FOLLOWERS_EMAIL
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_followers);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java
new file mode 100644
index 000000000..3622c817a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java
@@ -0,0 +1,299 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.GeoviewModel;
+import org.wordpress.android.ui.stats.models.GeoviewsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+
+public class StatsGeoviewsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsGeoviewsFragment.class.getSimpleName();
+
+ private GeoviewsModel mCountries;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mCountries != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mCountries);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mCountries = (GeoviewsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.CountriesUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mCountries = event.mCountries;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mCountries = null;
+ showErrorUI(event.mError);
+ }
+
+ private void hideMap() {
+ if (!isAdded()) {
+ return;
+ }
+
+ mTopPagerContainer.setVisibility(View.GONE);
+ }
+
+ private void showMap(final List<GeoviewModel> countries) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // setting up different margins for the map. We're basically remove left margins since the
+ // chart service produce a map that's slightly shifted on the right. See the Web version.
+ int dp4 = DisplayUtils.dpToPx(mTopPagerContainer.getContext(), 4);
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ layoutParams.setMargins(0, 0, dp4, 0);
+ mTopPagerContainer.setLayoutParams(layoutParams);
+
+ mTopPagerContainer.removeAllViews();
+
+ // must wait for mTopPagerContainer to be fully laid out (ie: measured). Then we can read the width and
+ // calculate the right height for the map div
+ mTopPagerContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mTopPagerContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ if (!isAdded()) {
+ return;
+ }
+
+ StringBuilder dataToLoad = new StringBuilder();
+
+ for (int i = 0; i < countries.size(); i++) {
+ final GeoviewModel currentCountry = countries.get(i);
+ dataToLoad.append("['").append(currentCountry.getCountryFullName()).append("',")
+ .append(currentCountry.getViews()).append("],");
+ }
+
+ // This is the label that is shown when the user taps on a region
+ String label = getResources().getString(getTotalsLabelResId());
+
+ // See: https://developers.google.com/chart/interactive/docs/gallery/geochart
+ // Loading the v42 of the Google Charts API, since the latest stable version has a problem with the legend. https://github.com/wordpress-mobile/WordPress-Android/issues/4131
+ // https://developers.google.com/chart/interactive/docs/release_notes#release-candidate-details
+ String htmlPage = "<html>" +
+ "<head>" +
+ "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>" +
+ "<script type=\"text/javascript\" src=\"https://www.google.com/jsapi\"></script>" +
+ "<script type=\"text/javascript\">" +
+ "google.charts.load('42', {'packages':['geochart']});" +
+ "google.charts.setOnLoadCallback(drawRegionsMap);" +
+ "function drawRegionsMap() {" +
+ "var data = google.visualization.arrayToDataTable(" +
+ "[" +
+ "['Country', '" + label + "']," +
+ dataToLoad +
+ "]);" +
+ "var options = {keepAspectRatio: true, region: 'world', colorAxis: { colors: [ '#FFF088', '#F34605' ] }, enableRegionInteractivity: true};" +
+ "var chart = new google.visualization.GeoChart(document.getElementById('regions_div'));" +
+ "chart.draw(data, options);" +
+ "}" +
+ "</script>" +
+ "</head>" +
+ "<body>" +
+ "<div id=\"regions_div\" style=\"width: 100%; height: 100%;\"></div>" +
+ "</body>" +
+ "</html>";
+
+ WebView webView = new WebView(getActivity());
+ mTopPagerContainer.addView(webView);
+
+ int width = mTopPagerContainer.getWidth();
+ int height = width * 3 / 4;
+
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) webView.getLayoutParams();
+ params.width = WebView.LayoutParams.MATCH_PARENT;
+ params.height = height;
+
+ webView.setLayoutParams(params);
+
+ webView.setWebViewClient(new MyWebViewClient()); // Hide map in case of unrecoverable errors
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
+ webView.loadData(htmlPage, "text/html", "UTF-8");
+
+ }
+ });
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ }
+
+ // Hide the Map in case of errors
+ private class MyWebViewClient extends WebViewClient {
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ super.onReceivedError(view, errorCode, description, failingUrl);
+ mTopPagerContainer.setVisibility(View.GONE);
+ AppLog.e(AppLog.T.STATS, "Cannot load geochart."
+ + " ErrorCode: " + errorCode
+ + " Description: " + description
+ + " Failing URL: " + failingUrl);
+ }
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ super.onReceivedSslError(view, handler, error);
+ mTopPagerContainer.setVisibility(View.GONE);
+ AppLog.e(AppLog.T.STATS, "Cannot load geochart. SSL ERROR. " + error.toString());
+ }
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasCountries()) {
+ List<GeoviewModel> countries = getCountries();
+ ArrayAdapter adapter = new GeoviewsAdapter(getActivity(), countries);
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ showMap(countries);
+ } else {
+ showHideNoResultsUI(true);
+ hideMap();
+ }
+ }
+
+ private boolean hasCountries() {
+ return mCountries != null && mCountries.getCountries() != null;
+ }
+
+ private List<GeoviewModel> getCountries() {
+ if (!hasCountries()) {
+ return null;
+ }
+ return mCountries.getCountries();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return (hasCountries()
+ && mCountries.getCountries().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class GeoviewsAdapter extends ArrayAdapter<GeoviewModel> {
+
+ private final List<GeoviewModel> list;
+ private final Activity context;
+ private final LayoutInflater inflater;
+
+ public GeoviewsAdapter(Activity context, List<GeoviewModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.context = context;
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final GeoviewModel currentRowData = list.get(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+ // fill data
+ String entry = currentRowData.getCountryFullName();
+ String imageUrl = currentRowData.getFlatFlagIconURL();
+ int total = currentRowData.getViews();
+
+ holder.setEntryText(entry);
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // image (country flag)
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(imageUrl, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.BLAVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_country;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_geoviews;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_geoviews_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.GEO_VIEWS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_countries);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java
new file mode 100644
index 000000000..8fa6a17f5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java
@@ -0,0 +1,99 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.InsightsAllTimeModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+
+public class StatsInsightsAllTimeFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsAllTimeFragment.class.getSimpleName();
+
+ private InsightsAllTimeModel mInsightsAllTimeModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mInsightsAllTimeModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mInsightsAllTimeModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mInsightsAllTimeModel = (InsightsAllTimeModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsAllTimeUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mInsightsAllTimeModel = event.mInsightsAllTimeModel;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mInsightsAllTimeModel = null;
+ showErrorUI(event.mError);
+ }
+
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_all_time_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ TextView postsTextView = (TextView) ll.findViewById(R.id.stats_all_time_posts);
+ TextView viewsTextView = (TextView) ll.findViewById(R.id.stats_all_time_views);
+ TextView visitorsTextView = (TextView) ll.findViewById(R.id.stats_all_time_visitors);
+ TextView besteverTextView = (TextView) ll.findViewById(R.id.stats_all_time_bestever);
+ TextView besteverDateTextView = (TextView) ll.findViewById(R.id.stats_all_time_bestever_date);
+
+
+ postsTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getPosts()));
+ viewsTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getViews()));
+ visitorsTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getVisitors()));
+
+ besteverTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getViewsBestDayTotal()));
+ besteverDateTextView.setText(
+ StatsUtils.parseDate(mInsightsAllTimeModel.getViewsBestDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, "MMMM dd, yyyy")
+ );
+
+ mResultContainer.addView(ll);
+ }
+
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.INSIGHTS_ALL_TIME
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_all_time);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java
new file mode 100644
index 000000000..c65b2053c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java
@@ -0,0 +1,280 @@
+package org.wordpress.android.ui.stats;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostDetailsModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+public class StatsInsightsLatestPostSummaryFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsLatestPostSummaryFragment.class.getSimpleName();
+
+ private static final String ARG_REST_RESPONSE_DETAILS = "ARG_REST_RESPONSE_DETAILS";
+
+ private InsightsLatestPostModel mInsightsLatestPostModel;
+ private InsightsLatestPostDetailsModel mInsightsLatestPostDetailsModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mInsightsLatestPostModel != null && mInsightsLatestPostDetailsModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mInsightsLatestPostModel);
+ outState.putSerializable(ARG_REST_RESPONSE_DETAILS, mInsightsLatestPostDetailsModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mInsightsLatestPostModel = (InsightsLatestPostModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE_DETAILS)) {
+ mInsightsLatestPostDetailsModel = (InsightsLatestPostDetailsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE_DETAILS);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsLatestPostSummaryUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ if (event.mInsightsLatestPostModel == null) {
+ showErrorUI();
+ return;
+ }
+
+ mInsightsLatestPostModel = event.mInsightsLatestPostModel;
+
+ // check if there is a post "published" on the blog
+ View mainView = getView();
+ if (mainView != null) {
+ mainView.setVisibility(mInsightsLatestPostModel.isLatestPostAvailable() ? View.VISIBLE : View.GONE);
+ }
+ if (!mInsightsLatestPostModel.isLatestPostAvailable()) {
+ // No need to go further into UI updating. There are no posts on this blog and the
+ // entire fragment is hidden.
+ return;
+ }
+
+ // Check if we already have the number of "views" for the latest post
+ if (mInsightsLatestPostModel.getPostViewsCount() == Integer.MIN_VALUE) {
+ // we don't have the views count. Need to call the service again here
+ refreshStats(mInsightsLatestPostModel.getPostID(),
+ new StatsService.StatsEndpointsEnum[]{StatsService.StatsEndpointsEnum.INSIGHTS_LATEST_POST_VIEWS});
+ showPlaceholderUI();
+ } else {
+ updateUI();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsLatestPostDetailsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ if (mInsightsLatestPostModel == null || event.mInsightsLatestPostDetailsModel == null) {
+ showErrorUI();
+ return;
+ }
+
+ mInsightsLatestPostDetailsModel = event.mInsightsLatestPostDetailsModel;
+ mInsightsLatestPostModel.setPostViewsCount(mInsightsLatestPostDetailsModel.getPostViewsCount());
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+
+ if (!shouldUpdateFragmentOnErrorEvent(event)
+ && event.mEndPointName != StatsService.StatsEndpointsEnum.INSIGHTS_LATEST_POST_VIEWS ) {
+ return;
+ }
+
+ mInsightsLatestPostDetailsModel = null;
+ mInsightsLatestPostModel = null;
+ showErrorUI(event.mError);
+ }
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ // check if there are posts "published" on the blog
+ if (!mInsightsLatestPostModel.isLatestPostAvailable()) {
+ // No need to go further into UI updating. There are no posts on this blog and the
+ // entire fragment is hidden.
+ return;
+ }
+
+ TextView moduleTitle = (TextView) getView().findViewById(R.id.stats_module_title);
+ moduleTitle.setOnClickListener(ViewsTabOnClickListener);
+ moduleTitle.setTextColor(getResources().getColor(R.color.stats_link_text_color));
+
+ // update the tabs and the text now
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_latest_post_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ String trendLabel = getString(R.string.stats_insights_latest_post_trend);
+ String sinceLabel = StatsUtils.getSinceLabel(
+ getActivity(),
+ mInsightsLatestPostModel.getPostDate()
+ ).toLowerCase();
+
+ String postTitle = StringEscapeUtils.unescapeHtml(mInsightsLatestPostModel.getPostTitle());
+ if (TextUtils.isEmpty(postTitle)) {
+ postTitle = getString(R.string.stats_insights_latest_post_no_title);
+ }
+
+ final String trendLabelFormatted = String.format(
+ trendLabel, sinceLabel, postTitle);
+
+ int startIndex, endIndex;
+ startIndex = trendLabelFormatted.indexOf(postTitle);
+ endIndex = startIndex + postTitle.length() +1;
+
+ Spannable descriptionTextToSpan = new SpannableString(trendLabelFormatted);
+ descriptionTextToSpan.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.stats_link_text_color)),
+ startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ TextView trendLabelTextField = (TextView) ll.findViewById(R.id.stats_post_trend_label);
+ trendLabelTextField.setText(descriptionTextToSpan);
+ trendLabelTextField.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ StatsUtils.openPostInReaderOrInAppWebview(getActivity(),
+ mInsightsLatestPostModel.getBlogID(),
+ String.valueOf(mInsightsLatestPostModel.getPostID()),
+ StatsConstants.ITEM_TYPE_POST,
+ mInsightsLatestPostModel.getPostURL());
+ }
+ });
+
+ LinearLayout tabs = (LinearLayout) ll.findViewById(R.id.stats_latest_post_tabs);
+
+ for (int i = 0; i < tabs.getChildCount(); i++) {
+ LinearLayout currentTab = (LinearLayout) tabs.getChildAt(i);
+ switch (i) {
+ case 0:
+ setupTab(currentTab, FormatUtils.formatDecimal(mInsightsLatestPostModel.getPostViewsCount()), StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS);
+ break;
+ case 1:
+ setupTab(currentTab, FormatUtils.formatDecimal(mInsightsLatestPostModel.getPostLikeCount()), StatsVisitorsAndViewsFragment.OverviewLabel.LIKES);
+ break;
+ case 2:
+ setupTab(currentTab, FormatUtils.formatDecimal(mInsightsLatestPostModel.getPostCommentCount()), StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS);
+ break;
+ }
+ }
+
+ mResultContainer.addView(ll);
+ }
+
+ private void setupTab(LinearLayout currentTab, String total, final StatsVisitorsAndViewsFragment.OverviewLabel itemType) {
+ final TextView label;
+ final TextView value;
+ final ImageView icon;
+
+ currentTab.setTag(itemType);
+ // Only Views is clickable here
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS) {
+ currentTab.setOnClickListener(ViewsTabOnClickListener);
+ } else {
+ currentTab.setClickable(false);
+ }
+
+ label = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_label);
+ label.setText(itemType.getLabel());
+ value = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_value);
+ value.setText(total);
+ if (total.equals("0")) {
+ value.setTextColor(getResources().getColor(R.color.grey));
+ } else {
+ // Only Views is clickable here.
+ // Likes and Comments shouldn't link anywhere because they don't have summaries
+ // so their color should be Gray Darken 30 or #3d596d
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS) {
+ value.setTextColor(getResources().getColor(R.color.blue_wordpress));
+ } else {
+ value.setTextColor(getResources().getColor(R.color.grey_darken_30));
+ }
+ }
+ icon = (ImageView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_icon);
+ icon.setImageDrawable(getTabIcon(itemType));
+
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS) {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_white);
+ } else {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_white);
+ }
+ }
+
+ private final View.OnClickListener ViewsTabOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Another check that the data is available
+ if (mInsightsLatestPostModel == null) {
+ showErrorUI();
+ return;
+ }
+
+ PostModel postModel = new PostModel(
+ mInsightsLatestPostModel.getBlogID(),
+ String.valueOf(mInsightsLatestPostModel.getPostID()),
+ mInsightsLatestPostModel.getPostTitle(),
+ mInsightsLatestPostModel.getPostURL(),
+ StatsConstants.ITEM_TYPE_POST);
+ ActivityLauncher.viewStatsSinglePostDetails(getActivity(), postModel);
+ }
+ };
+
+ private Drawable getTabIcon(final StatsVisitorsAndViewsFragment.OverviewLabel labelItem) {
+ switch (labelItem) {
+ case VISITORS:
+ return getResources().getDrawable(R.drawable.stats_icon_visitors);
+ case COMMENTS:
+ return getResources().getDrawable(R.drawable.stats_icon_comments);
+ case LIKES:
+ return getResources().getDrawable(R.drawable.stats_icon_likes);
+ default:
+ // Views and when no prev match
+ return getResources().getDrawable(R.drawable.stats_icon_views);
+ }
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.INSIGHTS_LATEST_POST_SUMMARY,
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_latest_post_summary);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java
new file mode 100644
index 000000000..247ca80b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java
@@ -0,0 +1,149 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.InsightsPopularModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+
+public class StatsInsightsMostPopularFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsMostPopularFragment.class.getSimpleName();
+
+ private InsightsPopularModel mInsightsPopularModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mInsightsPopularModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mInsightsPopularModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mInsightsPopularModel = (InsightsPopularModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsPopularUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mInsightsPopularModel = event.mInsightsPopularModel;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mInsightsPopularModel = null;
+ showErrorUI(event.mError);
+ }
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_most_popular_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ int dayOfTheWeek = mInsightsPopularModel.getHighestDayOfWeek();
+
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ c.setTimeInMillis(System.currentTimeMillis());
+ switch (dayOfTheWeek) {
+ case 0:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ break;
+ case 1:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);
+ break;
+ case 2:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);
+ break;
+ case 3:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY);
+ break;
+ case 4:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+ break;
+ case 5:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
+ break;
+ case 6:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+ break;
+ }
+
+ DateFormat formatter = new SimpleDateFormat("EEEE");
+ final TextView mostPopularDayTextView = (TextView) ll.findViewById(R.id.stats_most_popular_day);
+ mostPopularDayTextView.setText(formatter.format(c.getTime()));
+ final TextView mostPopularDayPercentTextView = (TextView) ll.findViewById(R.id.stats_most_popular_day_percent);
+ mostPopularDayPercentTextView.setText(
+ String.format(
+ getString(R.string.stats_insights_most_popular_percent_views),
+ roundToInteger(mInsightsPopularModel.getHighestDayPercent())
+ )
+ );
+
+ TextView mostPopularHourTextView = (TextView) ll.findViewById(R.id.stats_most_popular_hour);
+ DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(getActivity());
+ c.set(Calendar.HOUR_OF_DAY, mInsightsPopularModel.getHighestHour());
+ c.set(Calendar.MINUTE, 0);
+ mostPopularHourTextView.setText(timeFormat.format(c.getTime()));
+ final TextView mostPopularHourPercentTextView = (TextView) ll.findViewById(R.id.stats_most_popular_hour_percent);
+ mostPopularHourPercentTextView.setText(
+ String.format(
+ getString(R.string.stats_insights_most_popular_percent_views),
+ roundToInteger(mInsightsPopularModel.getHighestHourPercent())
+ )
+ );
+
+ mResultContainer.addView(ll);
+ }
+
+ /*
+ * Round a double to the closest integer
+ *
+ * If the decimal part is less than 0.5, the integer part stays the same,
+ * and truncation gives the right result.
+ * If the decimal part is more that 0.5, the integer part increments,
+ * and again truncation gives what we want.
+ *
+ */
+ private int roundToInteger(double inputValue) {
+ return (int) Math.floor(inputValue + 0.5);
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.INSIGHTS_POPULAR
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_popular);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java
new file mode 100644
index 000000000..0f1ae466b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java
@@ -0,0 +1,200 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.List;
+
+
+public class StatsInsightsTodayFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsTodayFragment.class.getSimpleName();
+
+ // Container Activity must implement this interface
+ public interface OnInsightsTodayClickListener {
+ void onInsightsTodayClicked(StatsVisitorsAndViewsFragment.OverviewLabel item);
+ }
+
+ private OnInsightsTodayClickListener mListener;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mListener = (OnInsightsTodayClickListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnInsightsTodayClickListener");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ TextView moduleTitle = (TextView) view.findViewById(R.id.stats_module_title);
+ moduleTitle.setTag(StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS);
+ moduleTitle.setOnClickListener(ButtonsOnClickListener);
+ moduleTitle.setTextColor(getResources().getColor(R.color.stats_link_text_color));
+ return view;
+ }
+
+
+ private VisitsModel mVisitsModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mVisitsModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mVisitsModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mVisitsModel = (VisitsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.VisitorsAndViewsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mVisitsModel = event.mVisitsAndViews;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mVisitsModel = null;
+ showErrorUI(event.mError);
+ }
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ if (mVisitsModel.getVisits() == null || mVisitsModel.getVisits().size() == 0) {
+ showErrorUI();
+ return;
+ }
+
+ List<VisitModel> visits = mVisitsModel.getVisits();
+ VisitModel data = visits.get(visits.size() - 1);
+
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_today_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ LinearLayout tabs = (LinearLayout) ll.findViewById(R.id.stats_post_tabs);
+
+ for (int i = 0; i < tabs.getChildCount(); i++) {
+ LinearLayout currentTab = (LinearLayout) tabs.getChildAt(i);
+ switch (i) {
+ case 0:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getViews()), StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS);
+ break;
+ case 1:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getVisitors()), StatsVisitorsAndViewsFragment.OverviewLabel.VISITORS );
+ break;
+ case 2:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getLikes()), StatsVisitorsAndViewsFragment.OverviewLabel.LIKES );
+ break;
+ case 3:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getComments()), StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS );
+ break;
+ }
+ }
+
+ mResultContainer.addView(ll);
+ }
+
+ private void setupTab(LinearLayout currentTab, String total, final StatsVisitorsAndViewsFragment.OverviewLabel itemType) {
+ final TextView label;
+ final TextView value;
+ final ImageView icon;
+
+ currentTab.setTag(itemType);
+ currentTab.setOnClickListener(ButtonsOnClickListener);
+
+ label = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_label);
+ label.setText(itemType.getLabel());
+ value = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_value);
+ value.setText(total);
+ if (total.equals("0")) {
+ value.setTextColor(getResources().getColor(R.color.grey));
+ } else {
+ value.setTextColor(getResources().getColor(R.color.blue_wordpress));
+ }
+ icon = (ImageView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_icon);
+ icon.setImageDrawable(getTabIcon(itemType));
+
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS) {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_white);
+ } else {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_white);
+ }
+ }
+
+ private final View.OnClickListener ButtonsOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+ if (mListener == null) {
+ return;
+ }
+ StatsVisitorsAndViewsFragment.OverviewLabel tag = (StatsVisitorsAndViewsFragment.OverviewLabel) v.getTag();
+ mListener.onInsightsTodayClicked(tag);
+ }
+ };
+
+ private Drawable getTabIcon(final StatsVisitorsAndViewsFragment.OverviewLabel labelItem) {
+ switch (labelItem) {
+ case VISITORS:
+ return getResources().getDrawable(R.drawable.stats_icon_visitors);
+ case COMMENTS:
+ return getResources().getDrawable(R.drawable.stats_icon_comments);
+ case LIKES:
+ return getResources().getDrawable(R.drawable.stats_icon_likes);
+ default:
+ // Views and when no prev match
+ return getResources().getDrawable(R.drawable.stats_icon_views);
+ }
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.VISITS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_today);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java
new file mode 100644
index 000000000..84522cf08
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java
@@ -0,0 +1,238 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.PublicizeModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+
+public class StatsPublicizeFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsPublicizeFragment.class.getSimpleName();
+
+ private PublicizeModel mPublicizeData;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mPublicizeData != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mPublicizeData != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mPublicizeData);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mPublicizeData = (PublicizeModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.PublicizeUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mPublicizeData = event.mPublicizeModel;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mPublicizeData = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasPublicize()) {
+ ArrayAdapter adapter = new PublicizeAdapter(getActivity(), getPublicize());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasPublicize() {
+ return mPublicizeData != null
+ && mPublicizeData.getServices() != null
+ && mPublicizeData.getServices().size() > 0;
+ }
+
+ private List<SingleItemModel> getPublicize() {
+ if (!hasPublicize()) {
+ return null;
+ }
+ return mPublicizeData.getServices();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return false;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class PublicizeAdapter extends ArrayAdapter<SingleItemModel> {
+
+ private final List<SingleItemModel> list;
+ private final LayoutInflater inflater;
+
+ public PublicizeAdapter(Activity context, List<SingleItemModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ final StatsViewHolder holder;
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ holder = new StatsViewHolder(rowView);
+ holder.networkImageView.setErrorImageResId(R.drawable.stats_icon_default_site_avatar);
+ holder.networkImageView.setDefaultImageResId(R.drawable.stats_icon_default_site_avatar);
+ rowView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) rowView.getTag();
+ }
+
+ final SingleItemModel currentRowData = list.get(position);
+
+ String serviceName = currentRowData.getTitle();
+
+ // entries
+ holder.setEntryText(getServiceName(serviceName));
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // image
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(getServiceImage(serviceName), mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.BLAVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ return rowView;
+ }
+ }
+
+ private String getServiceImage(String service) {
+ String serviceIconURL;
+
+ switch (service) {
+ case "facebook":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/2343ec78a04c6ea9d80806345d31fd78?s=";
+ break;
+ case "twitter":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/7905d1c4e12c54933a44d19fcd5f9356?s=";
+ break;
+ case "tumblr":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/84314f01e87cb656ba5f382d22d85134?s=";
+ break;
+ case "google_plus":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/4a4788c1dfc396b1f86355b274cc26b3?s=";
+ break;
+ case "linkedin":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/f54db463750940e0e7f7630fe327845e?s=";
+ break;
+ case "path":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/3a03c8ce5bf1271fb3760bb6e79b02c1?s=";
+ break;
+ default:
+ return null;
+ }
+
+ return serviceIconURL + mResourceVars.headerAvatarSizePx;
+ }
+
+ private String getServiceName(String service) {
+ if (service.equals("facebook")) {
+ return "Facebook";
+ }
+
+ if (service.equals("twitter")) {
+ return "Twitter";
+ }
+
+ if (service.equals("tumblr")) {
+ return "Tumblr";
+ }
+
+ if (service.equals("google_plus")) {
+ return "Google+";
+ }
+
+ if (service.equals("linkedin")) {
+ return "LinkedIn";
+ }
+
+ if (service.equals("path")) {
+ return "Path";
+ }
+
+ return StringUtils.capitalize(service);
+ }
+
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_publicize;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_publicize;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_publicize;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_publicize_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.PUBLICIZE
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_publicize);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java
new file mode 100644
index 000000000..c94293c7b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java
@@ -0,0 +1,340 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.ReferrerGroupModel;
+import org.wordpress.android.ui.stats.models.ReferrerResultModel;
+import org.wordpress.android.ui.stats.models.ReferrersModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StatsReferrersFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsReferrersFragment.class.getSimpleName();
+
+ private ReferrersModel mReferrers;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mReferrers != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mReferrers);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mReferrers = (ReferrersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.ReferrersUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mReferrers = event.mReferrers;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mReferrers = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasReferrers()) {
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), getReferrersGroups());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasReferrers() {
+ return mReferrers != null
+ && mReferrers.getGroups() != null
+ && mReferrers.getGroups().size() > 0;
+ }
+
+ private List<ReferrerGroupModel> getReferrersGroups() {
+ if (!hasReferrers()) {
+ return new ArrayList<ReferrerGroupModel>(0);
+ }
+ return mReferrers.getGroups();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasReferrers() && getReferrersGroups().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.REFERRERS
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_referrers;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_referrers_title;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_referrers_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ public final Activity act;
+ private final List<ReferrerGroupModel> groups;
+ private final List<List<MyChildModel>> children;
+
+ public MyExpandableListAdapter(Activity act, List<ReferrerGroupModel> groups) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(act);
+ this.act = act;
+
+ // The code below flattens the 3-levels tree of children to a 2-levels structure
+ // that will be used later to populate the UI
+ this.children = new ArrayList<>(groups.size());
+ // pre-populate the structure with null values
+ for (int i = 0; i < groups.size(); i++) {
+ this.children.add(null);
+ }
+
+ for (int i = 0; i < groups.size(); i++) {
+ ReferrerGroupModel currentGroup = groups.get(i);
+ List<MyChildModel> currentGroupChildren = new ArrayList<>();
+ List<ReferrerResultModel> childrenOfLevelOne = currentGroup.getResults();
+ if (childrenOfLevelOne != null) {
+ // Children at first level could be a single item or another tree
+ // Levels 2 children are skipped in the UI.
+ for (ReferrerResultModel singleLevelOneChild : childrenOfLevelOne) {
+ // Use all the info given in the first level child.
+ MyChildModel myChild = new MyChildModel();
+ myChild.icon = singleLevelOneChild.getIcon();
+ myChild.url = singleLevelOneChild.getUrl();
+ myChild.name = singleLevelOneChild.getName();
+ myChild.views = singleLevelOneChild.getViews();
+
+ // read the URL from the first second-level child if available.
+ List<SingleItemModel> secondLevelChildren = singleLevelOneChild.getChildren();
+ if (secondLevelChildren != null && secondLevelChildren.size() > 0) {
+ SingleItemModel firstThirdLevelChild = secondLevelChildren.get(0);
+ myChild.url = firstThirdLevelChild.getUrl();
+ }
+ currentGroupChildren.add(myChild);
+ }
+ }
+ this.children.set(i, currentGroupChildren);
+ }
+ }
+
+ private final class MyChildModel {
+ String name;
+ int views;
+ String url;
+ String icon;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ List<MyChildModel> currentGroupChildren = children.get(groupPosition);
+ return currentGroupChildren.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final MyChildModel currentChild = (MyChildModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ String name = currentChild.name;
+ int views = currentChild.views;
+
+ holder.chevronImageView.setVisibility(View.GONE);
+ holder.linkImageView.setVisibility(TextUtils.isEmpty(currentChild.url) ? View.GONE : View.VISIBLE);
+ holder.setEntryTextOrLink(currentChild.url, name);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(views));
+
+ // site icon
+ holder.networkImageView.setVisibility(View.GONE);
+ if (!TextUtils.isEmpty(currentChild.icon)) {
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(currentChild.icon, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.GONE_UNTIL_AVAILABLE);
+ }
+
+ // no more btm
+ holder.imgMore.setVisibility(View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ List<MyChildModel> currentGroupChildren = children.get(groupPosition);
+ if (currentGroupChildren == null) {
+ return 0;
+ } else {
+ return currentGroupChildren.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ final ReferrerGroupModel group = (ReferrerGroupModel) getGroup(groupPosition);
+
+ String name = group.getName();
+ int total = group.getTotal();
+ String url = group.getUrl();
+ String icon = group.getIcon();
+ int children = getChildrenCount(groupPosition);
+
+ if (children > 0) {
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ holder.setEntryTextOrLink(url, name);
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // Site icon
+ holder.networkImageView.setVisibility(View.GONE);
+ if (!TextUtils.isEmpty(icon)) {
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(icon, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.GONE_UNTIL_AVAILABLE);
+ }
+
+ if (children == 0) {
+ holder.showLinkIcon();
+ } else {
+ holder.showChevronIcon();
+ }
+
+ // Setup the spam button
+ if (ReferrerSpamHelper.isSpamActionAvailable(group)) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReferrerSpamHelper rp = new ReferrerSpamHelper(act);
+ rp.showPopup(holder.imgMore, group);
+ }
+ });
+
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_referrers);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java
new file mode 100644
index 000000000..5bdbda53e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java
@@ -0,0 +1,19 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.wordpress.android.R;
+
+/*
+ * class which holds all resource-based variables used in Stats
+ */
+class StatsResourceVars {
+ final int headerAvatarSizePx;
+
+ StatsResourceVars(Context context) {
+ Resources resources = context.getResources();
+
+ headerAvatarSizePx = resources.getDimensionPixelSize(R.dimen.avatar_sz_small);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java
new file mode 100644
index 000000000..5289b2e0d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java
@@ -0,0 +1,238 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.SearchTermModel;
+import org.wordpress.android.ui.stats.models.SearchTermsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsSearchTermsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsSearchTermsFragment.class.getSimpleName();
+
+ private final static String UNKNOWN_SEARCH_TERMS_HELP_PAGE = "http://en.support.wordpress.com/stats/#search-engine-terms";
+
+ private SearchTermsModel mSearchTerms;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mSearchTerms != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mSearchTerms);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mSearchTerms = (SearchTermsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SearchTermsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mSearchTerms = event.mSearchTerms;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mSearchTerms = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasSearchTerms()) {
+
+ /**
+ * At this point we can have:
+ * - A list of search terms
+ * - A list of search terms + Encrypted item
+ * - Encrypted item only
+ *
+ * We want to display max 10 items regardless the kind of the items, AND Encrypted
+ * must be present if available.
+ *
+ * We need to do some counts then...
+ */
+
+ List<SearchTermModel> originalSearchTermList = mSearchTerms.getSearchTerms();
+ List<SearchTermModel> mySearchTermList;
+ if (originalSearchTermList == null) {
+ // No clear-text search terms. we know we have the encrypted search terms item available
+ mySearchTermList = new ArrayList<>(0);
+ } else {
+ // Make sure the list has MAX 9 items if the "Encrypted" is available
+ // we want to show exactly 10 items per module
+ if (mSearchTerms.getEncryptedSearchTerms() > 0 && originalSearchTermList.size() > getMaxNumberOfItemsToShowInList() - 1) {
+ mySearchTermList = new ArrayList<>();
+ int minIndex = Math.min(originalSearchTermList.size(), getMaxNumberOfItemsToShowInList() - 1);
+ for (int i = 0; i < minIndex; i++) {
+ mySearchTermList.add(originalSearchTermList.get(i));
+ }
+ } else {
+ mySearchTermList = originalSearchTermList;
+ }
+ }
+ ArrayAdapter adapter = new SearchTermsAdapter(getActivity(), mySearchTermList, mSearchTerms.getEncryptedSearchTerms());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasSearchTerms() {
+ return mSearchTerms != null
+ && ((mSearchTerms.getSearchTerms() != null && mSearchTerms.getSearchTerms().size() > 0)
+ || mSearchTerms.getEncryptedSearchTerms() > 0
+ );
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ if (!hasSearchTerms()) {
+ return false;
+ }
+
+
+ int total = mSearchTerms.getSearchTerms() != null ? mSearchTerms.getSearchTerms().size() : 0;
+ // If "Encrypted" is available we only have 9 items of clear text terms in the list
+ if (mSearchTerms.getEncryptedSearchTerms() > 0) {
+ return total > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST - 1;
+ } else {
+ return total > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class SearchTermsAdapter extends ArrayAdapter<SearchTermModel> {
+
+ private final List<SearchTermModel> list;
+ private final LayoutInflater inflater;
+ private final int encryptedSearchTerms;
+
+ public SearchTermsAdapter(Activity context, List<SearchTermModel> list, int encryptedSearchTerms) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ this.encryptedSearchTerms = encryptedSearchTerms;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount() + (encryptedSearchTerms > 0 ? 1 : 0);
+ }
+
+ @Override
+ public SearchTermModel getItem(int position) {
+ // If it's an element in the list returns it, otherwise it's the position of "Encrypted"
+ if (position < super.getCount()) {
+ return super.getItem(position);
+ }
+
+ return new SearchTermModel("", null, "Unknown Search Terms", encryptedSearchTerms, true);
+ }
+
+ @Override
+ public int getPosition(SearchTermModel item) {
+ if (item.isEncriptedTerms()) {
+ return super.getCount(); // "Encrypted" is always at the end of the list
+ }
+
+ return super.getPosition(item);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final SearchTermModel currentRowData = this.getItem(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ String term = currentRowData.getTitle();
+
+ if (currentRowData.isEncriptedTerms()) {
+ holder.setEntryTextOrLink(UNKNOWN_SEARCH_TERMS_HELP_PAGE, getString(R.string.stats_search_terms_unknown_search_terms));
+ } else {
+ holder.setEntryText(term, getResources().getColor(R.color.stats_text_color));
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // image
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_search_terms;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_search_terms;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_search_terms_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.SEARCH_TERMS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_search_terms);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java
new file mode 100644
index 000000000..0ec8970cd
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java
@@ -0,0 +1,907 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.volley.NoConnectionError;
+import com.android.volley.VolleyError;
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.GraphViewSeries;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.stats.models.PostViewsModel;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+
+/**
+ * Single item details activity.
+ */
+public class StatsSingleItemDetailsActivity extends AppCompatActivity
+ implements StatsBarGraph.OnGestureListener{
+
+ public static final String ARG_REMOTE_BLOG_ID = "ARG_REMOTE_BLOG_ID";
+ public static final String ARG_REMOTE_ITEM_ID = "ARG_REMOTE_ITEM_ID";
+ public static final String ARG_REMOTE_ITEM_TYPE = "ARG_REMOTE_ITEM_TYPE";
+ public static final String ARG_ITEM_TITLE = "ARG_ITEM_TITLE";
+ public static final String ARG_ITEM_URL = "ARG_ITEM_URL";
+ private static final String ARG_REST_RESPONSE = "ARG_REST_RESPONSE";
+ private static final String ARG_SELECTED_GRAPH_BAR = "ARG_SELECTED_GRAPH_BAR";
+ private static final String ARG_PREV_NUMBER_OF_BARS = "ARG_PREV_NUMBER_OF_BARS";
+ private static final String SAVED_STATS_SCROLL_POSITION = "SAVED_STATS_SCROLL_POSITION";
+
+ private boolean mIsUpdatingStats;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private ScrollViewExt mOuterScrollView;
+
+ private final Handler mHandler = new Handler();
+
+ private LinearLayout mGraphContainer;
+ private TextView mStatsViewsLabel;
+ private TextView mStatsViewsTotals;
+
+ private LinearLayout mMonthsAndYearsModule;
+ private LinearLayout mMonthsAndYearsList;
+ private RelativeLayout mMonthsAndYearsHeader;
+ private LinearLayout mMonthsAndYearsEmptyPlaceholder;
+
+ private LinearLayout mAveragesModule;
+ private LinearLayout mAveragesList;
+ private RelativeLayout mAveragesHeader;
+ private LinearLayout mAveragesEmptyPlaceholder;
+
+ private LinearLayout mRecentWeeksModule;
+ private LinearLayout mRecentWeeksList;
+ private RelativeLayout mRecentWeeksHeader;
+ private LinearLayout mRecentWeeksEmptyPlaceholder;
+ private String mRemoteBlogID, mRemoteItemID, mRemoteItemType, mItemTitle, mItemURL;
+ private PostViewsModel mRestResponseParsed;
+ private int mSelectedBarGraphIndex = -1;
+ private int mPrevNumberOfBarsGraph = -1;
+
+ private SparseBooleanArray mYearsIdToExpandedMap;
+ private SparseBooleanArray mAveragesIdToExpandedMap;
+ private SparseBooleanArray mRecentWeeksIdToExpandedMap;
+
+ private static final String ARG_YEARS_EXPANDED_ROWS = "ARG_YEARS_EXPANDED_ROWS";
+ private static final String ARG_AVERAGES_EXPANDED_ROWS = "ARG_AVERAGES_EXPANDED_ROWS";
+ private static final String ARG_RECENT_EXPANDED_ROWS = "ARG_RECENT_EXPANDED_ROWS";
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.stats_activity_single_post_details);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ // pull to refresh setup
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+ refreshStats();
+ }
+ }
+ );
+
+ TextView mStatsForLabel = (TextView) findViewById(R.id.stats_summary_title);
+ mGraphContainer = (LinearLayout) findViewById(R.id.stats_bar_chart_fragment_container);
+ mStatsViewsLabel = (TextView) findViewById(R.id.stats_views_label);
+ mStatsViewsTotals = (TextView) findViewById(R.id.stats_views_totals);
+
+ mMonthsAndYearsModule = (LinearLayout) findViewById(R.id.stats_months_years_module);
+ mMonthsAndYearsHeader = (RelativeLayout) findViewById(R.id.stats_months_years_header);
+ mMonthsAndYearsList = (LinearLayout) findViewById(R.id.stats_months_years_list_linearlayout);
+ mMonthsAndYearsEmptyPlaceholder = (LinearLayout) findViewById(R.id.stats_months_years_empty_module_placeholder);
+
+ mAveragesModule = (LinearLayout) findViewById(R.id.stats_averages_module);
+ mAveragesHeader = (RelativeLayout) findViewById(R.id.stats_averages_list_header);
+ mAveragesList = (LinearLayout) findViewById(R.id.stats_averages_list_linearlayout);
+ mAveragesEmptyPlaceholder = (LinearLayout) findViewById(R.id.stats_averages_empty_module_placeholder);
+
+ mRecentWeeksModule = (LinearLayout) findViewById(R.id.stats_recent_weeks_module);
+ mRecentWeeksHeader = (RelativeLayout) findViewById(R.id.stats_recent_weeks_list_header);
+ mRecentWeeksList = (LinearLayout) findViewById(R.id.stats_recent_weeks_list_linearlayout);
+ mRecentWeeksEmptyPlaceholder = (LinearLayout) findViewById(R.id.stats_recent_weeks_empty_module_placeholder);
+
+ mYearsIdToExpandedMap = new SparseBooleanArray();
+ mAveragesIdToExpandedMap = new SparseBooleanArray();
+ mRecentWeeksIdToExpandedMap = new SparseBooleanArray();
+
+ setTitle(R.string.stats);
+ mOuterScrollView = (ScrollViewExt) findViewById(R.id.scroll_view_stats);
+
+ if (savedInstanceState != null) {
+ mRemoteItemID = savedInstanceState.getString(ARG_REMOTE_ITEM_ID);
+ mRemoteBlogID = savedInstanceState.getString(ARG_REMOTE_BLOG_ID);
+ mRemoteItemType = savedInstanceState.getString(ARG_REMOTE_ITEM_TYPE);
+ mItemTitle = savedInstanceState.getString(ARG_ITEM_TITLE);
+ mItemURL = savedInstanceState.getString(ARG_ITEM_URL);
+ mRestResponseParsed = (PostViewsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ mSelectedBarGraphIndex = savedInstanceState.getInt(ARG_SELECTED_GRAPH_BAR, -1);
+ mPrevNumberOfBarsGraph = savedInstanceState.getInt(ARG_PREV_NUMBER_OF_BARS, -1);
+
+ final int yScrollPosition = savedInstanceState.getInt(SAVED_STATS_SCROLL_POSITION);
+ if(yScrollPosition != 0) {
+ mOuterScrollView.postDelayed(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ mOuterScrollView.scrollTo(0, yScrollPosition);
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ if (savedInstanceState.containsKey(ARG_AVERAGES_EXPANDED_ROWS)) {
+ mAveragesIdToExpandedMap = savedInstanceState.getParcelable(ARG_AVERAGES_EXPANDED_ROWS);
+ }
+ if (savedInstanceState.containsKey(ARG_RECENT_EXPANDED_ROWS)) {
+ mRecentWeeksIdToExpandedMap = savedInstanceState.getParcelable(ARG_RECENT_EXPANDED_ROWS);
+ }
+ if (savedInstanceState.containsKey(ARG_YEARS_EXPANDED_ROWS)) {
+ mYearsIdToExpandedMap = savedInstanceState.getParcelable(ARG_YEARS_EXPANDED_ROWS);
+ }
+ } else if (getIntent() != null && getIntent().getExtras() != null) {
+ Bundle extras = getIntent().getExtras();
+ mRemoteItemID = extras.getString(ARG_REMOTE_ITEM_ID);
+ mRemoteBlogID = extras.getString(ARG_REMOTE_BLOG_ID);
+ mRemoteItemType = extras.getString(ARG_REMOTE_ITEM_TYPE);
+ mItemTitle = extras.getString(ARG_ITEM_TITLE);
+ mItemURL = extras.getString(ARG_ITEM_URL);
+ mRestResponseParsed = (PostViewsModel) extras.getSerializable(ARG_REST_RESPONSE);
+ mSelectedBarGraphIndex = extras.getInt(ARG_SELECTED_GRAPH_BAR, -1);
+ }
+
+ if (mRemoteBlogID == null || mRemoteItemID == null) {
+ Toast.makeText(this, R.string.stats_generic_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.STATS_SINGLE_POST_ACCESSED,
+ mRemoteBlogID
+ );
+ }
+
+ // Setup the main top label that opens the post in the Reader where possible
+ if (mItemTitle != null || mItemURL != null) {
+ mStatsForLabel.setVisibility(View.VISIBLE);
+ mStatsForLabel.setText(mItemTitle != null ? mItemTitle : mItemURL );
+ // make the label clickable if the URL is available
+ if (mItemURL != null) {
+ mStatsForLabel.setTextColor(getResources().getColor(R.color.stats_link_text_color));
+ mStatsForLabel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Context ctx = v.getContext();
+ StatsUtils.openPostInReaderOrInAppWebview(ctx,
+ mRemoteBlogID,
+ mRemoteItemID,
+ mRemoteItemType,
+ mItemURL);
+ }
+ });
+ } else {
+ mStatsForLabel.setTextColor(getResources().getColor(R.color.grey_darken_20));
+ }
+ } else {
+ mStatsForLabel.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(ARG_SELECTED_GRAPH_BAR, mSelectedBarGraphIndex);
+ outState.putInt(ARG_PREV_NUMBER_OF_BARS, mPrevNumberOfBarsGraph);
+ outState.putString(ARG_REMOTE_BLOG_ID, mRemoteBlogID);
+ outState.putString(ARG_REMOTE_ITEM_ID, mRemoteItemID);
+ outState.putString(ARG_REMOTE_ITEM_TYPE, mRemoteItemType);
+ outState.putString(ARG_ITEM_TITLE, mItemTitle);
+ outState.putString(ARG_ITEM_URL, mItemURL);
+
+ outState.putSerializable(ARG_REST_RESPONSE, mRestResponseParsed);
+ if (mOuterScrollView.getScrollY() != 0) {
+ outState.putInt(SAVED_STATS_SCROLL_POSITION, mOuterScrollView.getScrollY());
+ }
+
+ if (mAveragesIdToExpandedMap.size() > 0){
+ outState.putParcelable(ARG_AVERAGES_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mAveragesIdToExpandedMap));
+ }
+ if (mRecentWeeksIdToExpandedMap.size() > 0) {
+ outState.putParcelable(ARG_RECENT_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mRecentWeeksIdToExpandedMap));
+ }
+ if (mYearsIdToExpandedMap.size() > 0) {
+ outState.putParcelable(ARG_YEARS_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mYearsIdToExpandedMap));
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mRestResponseParsed == null) {
+ // check if network is available, if not shows the empty UI immediately
+ if (!NetworkUtils.checkConnection(this)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ setupEmptyUI();
+ } else {
+ setupEmptyGraph("");
+ showHideEmptyModulesIndicator(true);
+ refreshStats();
+ }
+ } else {
+ updateUI();
+ }
+ ActivityId.trackLastActivity(ActivityId.STATS_POST_DETAILS);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void refreshStats() {
+
+ if (mIsUpdatingStats) {
+ AppLog.w(AppLog.T.STATS, "stats details are already updating for the following postID "
+ + mRemoteItemID + ", refresh cancelled.");
+ return;
+ }
+
+ if (!NetworkUtils.checkConnection(this)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtilsV1_1();
+
+
+ // View and visitor counts for a site
+ final String singlePostRestPath = String.format(
+ "/sites/%s/stats/post/%s", mRemoteBlogID, mRemoteItemID);
+
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following Stats request " + singlePostRestPath);
+
+ RestBatchCallListener vListener = new RestBatchCallListener(this);
+ restClientUtils.get(singlePostRestPath, vListener, vListener);
+
+ mIsUpdatingStats = true;
+ mSwipeToRefreshHelper.setRefreshing(true);
+ }
+
+ private void showHideEmptyModulesIndicator(boolean show) {
+ if (isFinishing()) {
+ return;
+ }
+
+ mMonthsAndYearsModule.setVisibility(View.VISIBLE);
+ mRecentWeeksModule.setVisibility(View.VISIBLE);
+ mAveragesModule.setVisibility(View.VISIBLE);
+
+ mMonthsAndYearsHeader.setVisibility(show ? View.GONE : View.VISIBLE);
+ mRecentWeeksHeader.setVisibility(show ? View.GONE : View.VISIBLE);
+ mAveragesHeader.setVisibility(show ? View.GONE : View.VISIBLE);
+
+ mMonthsAndYearsList.setVisibility(show ? View.GONE : View.VISIBLE);
+ mAveragesList.setVisibility(show ? View.GONE : View.VISIBLE);
+ mRecentWeeksList.setVisibility(show ? View.GONE : View.VISIBLE);
+
+ mMonthsAndYearsEmptyPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE);
+ mRecentWeeksEmptyPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE);
+ mAveragesEmptyPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ private void setupEmptyUI() {
+ if (isFinishing()) {
+ return;
+ }
+
+ setupEmptyGraph(null);
+
+ mMonthsAndYearsModule.setVisibility(View.GONE);
+ mRecentWeeksModule.setVisibility(View.GONE);
+ mAveragesModule.setVisibility(View.GONE);
+
+ mRecentWeeksIdToExpandedMap.clear();
+ mAveragesIdToExpandedMap.clear();
+ mYearsIdToExpandedMap.clear();
+ }
+
+ private void setupEmptyGraph(String emptyLabel) {
+ if (isFinishing()) {
+ return;
+ }
+ Context context = mGraphContainer.getContext();
+ if (context != null) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View emptyBarGraphView = inflater.inflate(R.layout.stats_bar_graph_empty, mGraphContainer, false);
+ if (emptyLabel != null) {
+ final TextView emptyLabelField = (TextView) emptyBarGraphView.findViewById(R.id.stats_bar_graph_empty_label);
+ emptyLabelField.setText(emptyLabel);
+ }
+ mGraphContainer.removeAllViews();
+ mGraphContainer.addView(emptyBarGraphView);
+ }
+ mStatsViewsLabel.setText("");
+ mStatsViewsTotals.setText("");
+ }
+
+ private VisitModel[] getDataToShowOnGraph () {
+ if (mRestResponseParsed == null) {
+ return new VisitModel[0];
+ }
+
+ final VisitModel[] dayViews = mRestResponseParsed.getDayViews();
+ if (dayViews == null) {
+ return new VisitModel[0];
+ }
+
+ int numPoints = Math.min(StatsUIHelper.getNumOfBarsToShow(), dayViews.length);
+ int currentPointIndex = numPoints - 1;
+ VisitModel[] visitModels = new VisitModel[numPoints];
+
+ for (int i = dayViews.length - 1; i >= 0 && currentPointIndex >= 0; i--) {
+ visitModels[currentPointIndex] = dayViews[i];
+ currentPointIndex--;
+ }
+
+ return visitModels;
+ }
+
+ private void updateUI() {
+ if (isFinishing()) {
+ return;
+ }
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph();
+
+ if (dataToShowOnGraph == null || dataToShowOnGraph.length == 0) {
+ setupEmptyUI();
+ return;
+ }
+
+ final String[] horLabels = new String[dataToShowOnGraph.length];
+ String[] mStatsDate = new String[dataToShowOnGraph.length];
+ GraphView.GraphViewData[] views = new GraphView.GraphViewData[dataToShowOnGraph.length];
+
+ for (int i = 0; i < dataToShowOnGraph.length; i++) {
+ int currentItemValue = dataToShowOnGraph[i].getViews();
+ views[i] = new GraphView.GraphViewData(i, currentItemValue);
+
+ String currentItemStatsDate = dataToShowOnGraph[i].getPeriod();
+ horLabels[i] = StatsUtils.parseDate(
+ currentItemStatsDate,
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT
+ );
+ mStatsDate[i] = currentItemStatsDate;
+ }
+
+ GraphViewSeries mCurrentSeriesOnScreen = new GraphViewSeries(views);
+ mCurrentSeriesOnScreen.getStyle().color = getResources().getColor(R.color.stats_bar_graph_main_series);
+ mCurrentSeriesOnScreen.getStyle().highlightColor = getResources().getColor(R.color.stats_bar_graph_main_series_highlight);
+ mCurrentSeriesOnScreen.getStyle().outerhighlightColor = getResources().getColor(R.color.stats_bar_graph_outer_highlight);
+ mCurrentSeriesOnScreen.getStyle().padding = DisplayUtils.dpToPx(this, 5);
+
+ StatsBarGraph mGraphView;
+ if (mGraphContainer.getChildCount() >= 1 && mGraphContainer.getChildAt(0) instanceof GraphView) {
+ mGraphView = (StatsBarGraph) mGraphContainer.getChildAt(0);
+ } else {
+ mGraphContainer.removeAllViews();
+ mGraphView = new StatsBarGraph(this);
+ mGraphContainer.addView(mGraphView);
+ }
+
+
+ mGraphView.removeAllSeries();
+ mGraphView.addSeries(mCurrentSeriesOnScreen);
+ //mGraphView.getGraphViewStyle().setNumHorizontalLabels(getNumOfHorizontalLabels(dataToShowOnGraph.length));
+ mGraphView.getGraphViewStyle().setNumHorizontalLabels(dataToShowOnGraph.length);
+ mGraphView.getGraphViewStyle().setMaxColumnWidth(
+ DisplayUtils.dpToPx(this, StatsConstants.STATS_GRAPH_BAR_MAX_COLUMN_WIDTH_DP)
+ );
+ mGraphView.setHorizontalLabels(horLabels);
+ mGraphView.setGestureListener(this);
+
+ // Reset the bar selected upon rotation of the device when the no. of bars can change with orientation.
+ // Only happens on 720DP tablets
+ if (mPrevNumberOfBarsGraph != -1 && mPrevNumberOfBarsGraph != dataToShowOnGraph.length) {
+ mSelectedBarGraphIndex = dataToShowOnGraph.length - 1;
+ } else {
+ mSelectedBarGraphIndex = (mSelectedBarGraphIndex != -1) ? mSelectedBarGraphIndex : dataToShowOnGraph.length - 1;
+ }
+
+ mGraphView.highlightBar(mSelectedBarGraphIndex);
+ mPrevNumberOfBarsGraph = dataToShowOnGraph.length;
+
+ setMainViewsLabel(
+ StatsUtils.parseDate(
+ mStatsDate[mSelectedBarGraphIndex],
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT
+ ),
+ dataToShowOnGraph[mSelectedBarGraphIndex].getViews()
+ );
+
+ showHideEmptyModulesIndicator(false);
+
+ mMonthsAndYearsList.setVisibility(View.VISIBLE);
+ List<PostViewsModel.Year> years = mRestResponseParsed.getYears();
+ MonthsAndYearsListAdapter monthsAndYearsListAdapter = new MonthsAndYearsListAdapter(this, years, mRestResponseParsed.getHighestMonth());
+ StatsUIHelper.reloadGroupViews(this, monthsAndYearsListAdapter, mYearsIdToExpandedMap, mMonthsAndYearsList);
+
+ mAveragesList.setVisibility(View.VISIBLE);
+ List<PostViewsModel.Year> averages = mRestResponseParsed.getAverages();
+ MonthsAndYearsListAdapter averagesListAdapter = new MonthsAndYearsListAdapter(this, averages, mRestResponseParsed.getHighestDayAverage());
+ StatsUIHelper.reloadGroupViews(this, averagesListAdapter, mAveragesIdToExpandedMap, mAveragesList);
+
+ mRecentWeeksList.setVisibility(View.VISIBLE);
+ List<PostViewsModel.Week> recentWeeks = mRestResponseParsed.getWeeks();
+ RecentWeeksListAdapter recentWeeksListAdapter = new RecentWeeksListAdapter(this, recentWeeks, mRestResponseParsed.getHighestWeekAverage());
+ StatsUIHelper.reloadGroupViews(this, recentWeeksListAdapter, mRecentWeeksIdToExpandedMap, mRecentWeeksList);
+ }
+
+
+ private void setMainViewsLabel(String dateFormatted, int totals) {
+ mStatsViewsLabel.setText(getString(R.string.stats_views) + ": "
+ + dateFormatted);
+ mStatsViewsTotals.setText(FormatUtils.formatDecimal(totals));
+ }
+
+
+ private class RecentWeeksListAdapter extends BaseExpandableListAdapter {
+ public static final String GROUP_DATE_FORMAT = "MMM dd";
+ public final LayoutInflater inflater;
+ private final List<PostViewsModel.Week> groups;
+ private final int maxReachedValue;
+
+ public RecentWeeksListAdapter(Context context, List<PostViewsModel.Week> groups, int maxReachedValue) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(context);
+ this.maxReachedValue = maxReachedValue;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ PostViewsModel.Week currentWeek = groups.get(groupPosition);
+ return currentWeek.getDays().get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final PostViewsModel.Day currentDay = (PostViewsModel.Day) getChild(groupPosition, childPosition);
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ holder.setEntryText(StatsUtils.parseDate(currentDay.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, "EEE, MMM dd"));
+
+ // Intercept clicks at row level and eat the event. We don't want to show the ripple here.
+ holder.rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+
+ }
+ });
+ holder.rowContent.setBackgroundColor(Color.TRANSPARENT);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentDay.getCount()));
+
+ // show the trophy indicator if the value is the maximum reached
+ if (currentDay.getCount() == maxReachedValue && maxReachedValue > 0) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ holder.networkImageView.setVisibility(View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ PostViewsModel.Week week = groups.get(groupPosition);
+ return week.getDays().size();
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ PostViewsModel.Week week = (PostViewsModel.Week) getGroup(groupPosition);
+
+ int total = week.getTotal();
+
+ // change the color of the text if one of its childs has reached maximum value
+ int numberOfChilds = getChildrenCount(groupPosition);
+ boolean shouldShowTheTrophyIcon = false;
+ if (maxReachedValue > 0) {
+ for (int i = 0; i < numberOfChilds; i++) {
+ PostViewsModel.Day currentChild = (PostViewsModel.Day) getChild(groupPosition, i);
+ if (currentChild.getCount() == maxReachedValue) {
+ shouldShowTheTrophyIcon = true;
+ }
+ }
+ }
+
+ // Build the label to show on the group
+ String name;
+ PostViewsModel.Day firstChild = (PostViewsModel.Day) getChild(groupPosition, 0);
+ if (numberOfChilds > 1) {
+ PostViewsModel.Day lastChild = (PostViewsModel.Day) getChild(groupPosition, getChildrenCount(groupPosition) - 1);
+ name = StatsUtils.parseDate(firstChild.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, GROUP_DATE_FORMAT)
+ + " - " + StatsUtils.parseDate(lastChild.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, GROUP_DATE_FORMAT);
+ } else {
+ name = StatsUtils.parseDate(firstChild.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, GROUP_DATE_FORMAT);
+ }
+
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+
+ holder.networkImageView.setVisibility(View.GONE);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+ if (shouldShowTheTrophyIcon) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ // expand/collapse chevron
+ holder.chevronImageView.setVisibility(numberOfChilds > 0 ? View.VISIBLE : View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+
+ private class MonthsAndYearsListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ private final List<PostViewsModel.Year> groups;
+ private final int maxReachedValue;
+
+ public MonthsAndYearsListAdapter(Context context, List<PostViewsModel.Year> groups, int maxReachedValue) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(context);
+ this.maxReachedValue = maxReachedValue;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ PostViewsModel.Year currentYear = groups.get(groupPosition);
+ return currentYear.getMonths().get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final PostViewsModel.Month currentMonth = (PostViewsModel.Month) getChild(groupPosition, childPosition);
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ holder.setEntryText(StatsUtils.parseDate(currentMonth.getMonth(), "MM", StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_FORMAT));
+
+ // Intercept clicks at row level and eat the event. We don't want to show the ripple here.
+ holder.rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+
+ }
+ });
+ holder.rowContent.setBackgroundColor(Color.TRANSPARENT);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentMonth.getCount()));
+
+ // show the trophy indicator if the value is the maximum reached
+ if (currentMonth.getCount() == maxReachedValue && maxReachedValue > 0) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ holder.networkImageView.setVisibility(View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ PostViewsModel.Year currentYear = groups.get(groupPosition);
+ return currentYear.getMonths().size();
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ PostViewsModel.Year year = (PostViewsModel.Year) getGroup(groupPosition);
+
+ String name = year.getLabel();
+ int total = year.getTotal();
+
+ // change the color of the text if one of its childs has reached maximum value
+ int numberOfChilds = getChildrenCount(groupPosition);
+ boolean shouldShowTheTrophyIcon = false;
+ if (maxReachedValue > 0) {
+ for (int i = 0; i < numberOfChilds; i++) {
+ PostViewsModel.Month currentChild = (PostViewsModel.Month) getChild(groupPosition, i);
+ if (currentChild.getCount() == maxReachedValue) {
+ shouldShowTheTrophyIcon = true;
+ break;
+ }
+ }
+ }
+
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+
+ if (shouldShowTheTrophyIcon) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ holder.networkImageView.setVisibility(View.GONE);
+
+ // expand/collapse chevron
+ holder.chevronImageView.setVisibility(numberOfChilds > 0 ? View.VISIBLE : View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+
+ private class RestBatchCallListener implements RestRequest.Listener, RestRequest.ErrorListener {
+
+ private final WeakReference<Activity> mActivityRef;
+
+ public RestBatchCallListener(Activity activity) {
+ mActivityRef = new WeakReference<>(activity);
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ // single background thread used to parse the response in BG.
+ ThreadPoolExecutor parseResponseExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
+ parseResponseExecutor.submit(new Thread() {
+ @Override
+ public void run() {
+ //AppLog.d(AppLog.T.STATS, "The REST response: " + response.toString());
+ mSelectedBarGraphIndex = -1;
+ try {
+ mRestResponseParsed = new PostViewsModel(response);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the JSON response", e);
+ resetModelVariables();
+ }
+
+ // Update the UI
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateUI();
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ StatsUtils.logVolleyErrorDetails(volleyError);
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+ resetModelVariables();
+
+ String label = mActivityRef.get().getString(R.string.error_refresh_stats);
+ if (volleyError instanceof NoConnectionError) {
+ label += "\n" + mActivityRef.get().getString(R.string.no_network_message);
+ }
+
+ ToastUtils.showToast(mActivityRef.get(), label, ToastUtils.Duration.LONG);
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+
+ // Update the UI
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateUI();
+ }
+ });
+ }
+ }
+
+ private void resetModelVariables() {
+ mRestResponseParsed = null;
+ mSelectedBarGraphIndex = -1;
+ mAveragesIdToExpandedMap.clear();
+ mYearsIdToExpandedMap.clear();
+ }
+
+ @Override
+ public void onBarTapped(int tappedBar) {
+ mSelectedBarGraphIndex = tappedBar;
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph();
+ String currentItemStatsDate = dataToShowOnGraph[mSelectedBarGraphIndex].getPeriod();
+ currentItemStatsDate = StatsUtils.parseDate(
+ currentItemStatsDate,
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT
+ );
+ setMainViewsLabel(currentItemStatsDate, dataToShowOnGraph[mSelectedBarGraphIndex].getViews());
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java
new file mode 100644
index 000000000..9f838ea9d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java
@@ -0,0 +1,282 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.TagModel;
+import org.wordpress.android.ui.stats.models.TagsContainerModel;
+import org.wordpress.android.ui.stats.models.TagsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StatsTagsAndCategoriesFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsTagsAndCategoriesFragment.class.getSimpleName();
+
+ private TagsContainerModel mTagsContainer;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mTagsContainer != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mTagsContainer != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mTagsContainer);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mTagsContainer = (TagsContainerModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.TagsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mTagsContainer = event.mTagsContainer;
+ mGroupIdToExpandedMap.clear();
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mTagsContainer = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasTags()) {
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), getTags());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasTags() {
+ return mTagsContainer != null
+ && mTagsContainer.getTags() != null
+ && mTagsContainer.getTags().size() > 0;
+ }
+
+ private List<TagsModel> getTags() {
+ if (!hasTags()) {
+ return new ArrayList<TagsModel>(0);
+ }
+ return mTagsContainer.getTags();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasTags() && getTags().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.TAGS_AND_CATEGORIES
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_tags_and_categories;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_tags_and_categories;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_tags_and_categories_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ private final List<TagsModel> groups;
+
+ public MyExpandableListAdapter(Context context, List<TagsModel> groups) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ TagsModel currentGroup = groups.get(groupPosition);
+ List<TagModel> results = currentGroup.getTags();
+ return results.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final TagModel children = (TagModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+
+ //Make the picture smaller (same size of the chevron) only for tag
+ ViewGroup.LayoutParams params = viewHolder.networkImageView.getLayoutParams();
+ params.width = DisplayUtils.dpToPx(convertView.getContext(), 12);
+ params.height = params.width;
+ viewHolder.networkImageView.setLayoutParams(params);
+
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ // name, url
+ holder.setEntryTextOrLink(children.getLink(), children.getName());
+
+ // totals
+ holder.totalsTextView.setText("");
+
+ // icon.
+ holder.networkImageView.setVisibility(View.VISIBLE);
+ holder.networkImageView.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_tags));
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ TagsModel currentGroup = groups.get(groupPosition);
+ List<TagModel> tags = currentGroup.getTags();
+ if (tags == null || tags.size() == 1 ) {
+ return 0;
+ } else {
+ return tags.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+
+ //Make the picture smaller (same size of the chevron) only for tag
+ ViewGroup.LayoutParams params = viewHolder.networkImageView.getLayoutParams();
+ params.width = DisplayUtils.dpToPx(convertView.getContext(), 12);
+ params.height = params.width;
+ viewHolder.networkImageView.setLayoutParams(params);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ TagsModel group = (TagsModel) getGroup(groupPosition);
+ StringBuilder groupName = new StringBuilder();
+ List<TagModel> tags = group.getTags();
+ for (int i = 0; i < tags.size(); i++) {
+ TagModel currentTag = tags.get(i);
+ groupName.append(currentTag.getName());
+ if ( i < (tags.size() - 1)) {
+ groupName.append(" | ");
+ }
+ }
+ int total = group.getViews();
+ int children = getChildrenCount(groupPosition);
+
+ if (children > 0) {
+ holder.setEntryText(groupName.toString(), getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ holder.setEntryTextOrLink(tags.get(0).getLink(), groupName.toString());
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // expand/collapse chevron
+ holder.chevronImageView.setVisibility(children > 0 ? View.VISIBLE : View.GONE);
+
+
+ // icon
+ if ( children == 0 ) {
+ holder.networkImageView.setVisibility(View.VISIBLE);
+ int drawableResource = groupName.toString().equalsIgnoreCase("uncategorized") ? R.drawable.stats_icon_categories
+ : R.drawable.stats_icon_tags;
+ holder.networkImageView.setImageDrawable(getResources().getDrawable(drawableResource));
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_tags_and_categories);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java
new file mode 100644
index 000000000..e9cae5b63
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java
@@ -0,0 +1,39 @@
+package org.wordpress.android.ui.stats;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+/**
+ * Timeframes for the stats pages.
+ */
+public enum StatsTimeframe {
+ INSIGHTS(R.string.stats_insights),
+ DAY(R.string.stats_timeframe_days),
+ WEEK(R.string.stats_timeframe_weeks),
+ MONTH(R.string.stats_timeframe_months),
+ YEAR(R.string.stats_timeframe_years),
+ ;
+
+ private final int mLabelResId;
+
+ StatsTimeframe(int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+
+ public String getLabelForRestCall() {
+ switch (this) {
+ case WEEK:
+ return "week";
+ case MONTH:
+ return "month";
+ case YEAR:
+ return "year";
+ default:
+ return "day";
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java
new file mode 100644
index 000000000..32a3742fa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java
@@ -0,0 +1,129 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Bundle;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.adapters.PostsAndPagesAdapter;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsTopPostsAndPagesFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsTopPostsAndPagesFragment.class.getSimpleName();
+
+ private TopPostsAndPagesModel mTopPostsAndPagesModel = null;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mTopPostsAndPagesModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mTopPostsAndPagesModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mTopPostsAndPagesModel = (TopPostsAndPagesModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.TopPostsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mTopPostsAndPagesModel = event.mTopPostsAndPagesModel;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mTopPostsAndPagesModel = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasTopPostsAndPages()) {
+ List<PostModel> postViews = mTopPostsAndPagesModel.getTopPostsAndPages();
+ ArrayAdapter adapter = new PostsAndPagesAdapter(getActivity(), postViews);
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasTopPostsAndPages() {
+ return mTopPostsAndPagesModel != null && mTopPostsAndPagesModel.hasTopPostsAndPages();
+ }
+
+ private List<PostModel> getTopPostsAndPages() {
+ if (!hasTopPostsAndPages()) {
+ return new ArrayList<PostModel>(0);
+ }
+ return mTopPostsAndPagesModel.getTopPostsAndPages();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasTopPostsAndPages() && getTopPostsAndPages().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_posts_and_pages;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_top_posts_title;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_top_posts_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.TOP_POSTS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_top_posts_and_pages);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java
new file mode 100644
index 000000000..441ed3050
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java
@@ -0,0 +1,344 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.text.Spannable;
+import android.text.style.URLSpan;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.RotateAnimation;
+import android.view.animation.ScaleAnimation;
+import android.widget.ExpandableListAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.DisplayUtils;
+
+class StatsUIHelper {
+ // Max number of rows to show in a stats fragment
+ private static final int STATS_GROUP_MAX_ITEMS = 10;
+ private static final int STATS_CHILD_MAX_ITEMS = 50;
+ private static final int ANIM_DURATION = 150;
+
+ // Used for tablet UI
+ private static final int TABLET_720DP = 720;
+ private static final int TABLET_600DP = 600;
+
+ private static boolean isInLandscape(Activity act) {
+ Display display = act.getWindowManager().getDefaultDisplay();
+ Point point = new Point();
+ display.getSize(point);
+ return (point.y < point.x);
+ }
+
+ // Load more bars for 720DP tablets
+ private static boolean shouldLoadMoreBars() {
+ return (StatsUtils.getSmallestWidthDP() >= TABLET_720DP);
+ }
+
+ public static void reloadLinearLayout(Context ctx, ListAdapter adapter, LinearLayout linearLayout, int maxNumberOfItemsToshow) {
+ if (ctx == null || linearLayout == null || adapter == null) {
+ return;
+ }
+
+ // limit number of items to show otherwise it would cause performance issues on the LinearLayout
+ int count = Math.min(adapter.getCount(), maxNumberOfItemsToshow);
+
+ if (count == 0) {
+ linearLayout.removeAllViews();
+ return;
+ }
+
+ int numExistingViews = linearLayout.getChildCount();
+ // remove excess views
+ if (count < numExistingViews) {
+ int numToRemove = numExistingViews - count;
+ linearLayout.removeViews(count, numToRemove);
+ numExistingViews = count;
+ }
+
+ int bgColor = Color.TRANSPARENT;
+ for (int i = 0; i < count; i++) {
+ final View view;
+ // reuse existing view when possible
+ if (i < numExistingViews) {
+ View convertView = linearLayout.getChildAt(i);
+ view = adapter.getView(i, convertView, linearLayout);
+ view.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(view, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ } else {
+ view = adapter.getView(i, null, linearLayout);
+ view.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(view, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ linearLayout.addView(view);
+ }
+ }
+ linearLayout.invalidate();
+ }
+
+ /**
+ *
+ * Padding information are reset when changing the background Drawable on a View.
+ * The reason why setting an image resets the padding is because 9-patch images can encode padding.
+ *
+ * See http://stackoverflow.com/a/10469121 and
+ * http://www.mail-archive.com/android-developers@googlegroups.com/msg09595.html
+ *
+ * @param v The view to apply the background resource
+ * @param backgroundResId The resource ID
+ */
+ private static void setViewBackgroundWithoutResettingPadding(final View v, final int backgroundResId) {
+ final int paddingBottom = v.getPaddingBottom(), paddingLeft = v.getPaddingLeft();
+ final int paddingRight = v.getPaddingRight(), paddingTop = v.getPaddingTop();
+ v.setBackgroundResource(backgroundResId);
+ v.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+ }
+
+ public static void reloadLinearLayout(Context ctx, ListAdapter adapter, LinearLayout linearLayout) {
+ reloadLinearLayout(ctx, adapter, linearLayout, STATS_GROUP_MAX_ITEMS);
+ }
+
+ public static void reloadGroupViews(final Context ctx,
+ final ExpandableListAdapter mAdapter,
+ final SparseBooleanArray mGroupIdToExpandedMap,
+ final LinearLayout mLinearLayout) {
+ reloadGroupViews(ctx, mAdapter, mGroupIdToExpandedMap, mLinearLayout, STATS_GROUP_MAX_ITEMS);
+ }
+
+ public static void reloadGroupViews(final Context ctx,
+ final ExpandableListAdapter mAdapter,
+ final SparseBooleanArray mGroupIdToExpandedMap,
+ final LinearLayout mLinearLayout,
+ final int maxNumberOfItemsToshow) {
+ if (ctx == null || mLinearLayout == null || mAdapter == null || mGroupIdToExpandedMap == null) {
+ return;
+ }
+
+ int groupCount = Math.min(mAdapter.getGroupCount(), maxNumberOfItemsToshow);
+ if (groupCount == 0) {
+ mLinearLayout.removeAllViews();
+ return;
+ }
+
+ int numExistingGroupViews = mLinearLayout.getChildCount();
+
+ // remove excess views
+ if (groupCount < numExistingGroupViews) {
+ int numToRemove = numExistingGroupViews - groupCount;
+ mLinearLayout.removeViews(groupCount, numToRemove);
+ numExistingGroupViews = groupCount;
+ }
+
+ int bgColor = Color.TRANSPARENT;
+
+ // add each group
+ for (int i = 0; i < groupCount; i++) {
+ boolean isExpanded = mGroupIdToExpandedMap.get(i);
+
+ // reuse existing view when possible
+ final View groupView;
+ if (i < numExistingGroupViews) {
+ View convertView = mLinearLayout.getChildAt(i);
+ groupView = mAdapter.getGroupView(i, isExpanded, convertView, mLinearLayout);
+ groupView.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(groupView, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ } else {
+ groupView = mAdapter.getGroupView(i, isExpanded, null, mLinearLayout);
+ groupView.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(groupView, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ mLinearLayout.addView(groupView);
+ }
+
+ // groupView is recycled, we need to reset it to the original state.
+ ViewGroup childContainer = (ViewGroup) groupView.findViewById(R.id.layout_child_container);
+ if (childContainer != null) {
+ childContainer.setVisibility(View.GONE);
+ }
+ // Remove any other prev animations set on the chevron
+ final ImageView chevron = (ImageView) groupView.findViewById(R.id.stats_list_cell_chevron);
+ if (chevron != null) {
+ chevron.clearAnimation();
+ chevron.setImageResource(R.drawable.stats_chevron_right);
+ }
+
+ // add children if this group is expanded
+ if (isExpanded) {
+ StatsUIHelper.showChildViews(mAdapter, mLinearLayout, i, groupView, false);
+ }
+
+ // toggle expand/collapse when group view is tapped
+ final int groupPosition = i;
+ groupView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mAdapter.getChildrenCount(groupPosition) == 0) {
+ return;
+ }
+ boolean shouldExpand = !mGroupIdToExpandedMap.get(groupPosition);
+ mGroupIdToExpandedMap.put(groupPosition, shouldExpand);
+ if (shouldExpand) {
+ StatsUIHelper.showChildViews(mAdapter, mLinearLayout, groupPosition, groupView, true);
+ } else {
+ StatsUIHelper.hideChildViews(groupView, groupPosition, true);
+ }
+ }
+ });
+ }
+ }
+
+ /*
+ * interpolator for all expand/collapse animations
+ */
+ private static Interpolator getInterpolator() {
+ return new AccelerateInterpolator();
+ }
+
+ private static void hideChildViews(View groupView, int groupPosition, boolean animate) {
+ final ViewGroup childContainer = (ViewGroup) groupView.findViewById(R.id.layout_child_container);
+ if (childContainer == null) {
+ return;
+ }
+
+ if (childContainer.getVisibility() != View.GONE) {
+ if (animate) {
+ Animation expand = new ScaleAnimation(1.0f, 1.0f, 1.0f, 0.0f);
+ expand.setDuration(ANIM_DURATION);
+ expand.setInterpolator(getInterpolator());
+ expand.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ childContainer.setVisibility(View.GONE);
+ }
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ });
+ childContainer.startAnimation(expand);
+ } else {
+ childContainer.setVisibility(View.GONE);
+ }
+ }
+ StatsUIHelper.setGroupChevron(false, groupView, groupPosition, animate);
+ }
+
+ /*
+ * shows the correct up/down chevron for the passed group
+ */
+ private static void setGroupChevron(final boolean isGroupExpanded, View groupView, int groupPosition, boolean animate) {
+ final ImageView chevron = (ImageView) groupView.findViewById(R.id.stats_list_cell_chevron);
+ if (chevron == null) {
+ return;
+ }
+ if (isGroupExpanded) {
+ // change the background of the parent
+ setViewBackgroundWithoutResettingPadding(groupView, R.drawable.stats_list_item_expanded_background);
+ } else {
+ setViewBackgroundWithoutResettingPadding(groupView, groupPosition == 0 ? 0 : R.drawable.stats_list_item_background);
+ }
+
+ chevron.clearAnimation(); // Remove any other prev animations set on the chevron
+ if (animate) {
+ // make sure we start with the correct chevron for the prior state before animating it
+ chevron.setImageResource(isGroupExpanded ? R.drawable.stats_chevron_right : R.drawable.stats_chevron_down);
+ float start = (isGroupExpanded ? 0.0f : 0.0f);
+ float end = (isGroupExpanded ? 90.0f : -90.0f);
+ Animation rotate = new RotateAnimation(start, end, Animation.RELATIVE_TO_SELF, 0.5f,
+ Animation.RELATIVE_TO_SELF, 0.5f);
+ rotate.setDuration(ANIM_DURATION);
+ rotate.setInterpolator(getInterpolator());
+ rotate.setFillAfter(true);
+ chevron.startAnimation(rotate);
+ } else {
+ chevron.setImageResource(isGroupExpanded ? R.drawable.stats_chevron_down : R.drawable.stats_chevron_right);
+ }
+ }
+
+ private static void showChildViews(ExpandableListAdapter mAdapter, LinearLayout mLinearLayout,
+ int groupPosition, View groupView, boolean animate) {
+ int childCount = Math.min(mAdapter.getChildrenCount(groupPosition), STATS_CHILD_MAX_ITEMS);
+ if (childCount == 0) {
+ return;
+ }
+
+ final ViewGroup childContainer = (ViewGroup) groupView.findViewById(R.id.layout_child_container);
+ if (childContainer == null) {
+ return;
+ }
+
+ int numExistingViews = childContainer.getChildCount();
+ if (childCount < numExistingViews) {
+ int numToRemove = numExistingViews - childCount;
+ childContainer.removeViews(childCount, numToRemove);
+ numExistingViews = childCount;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ boolean isLastChild = (i == childCount - 1);
+ if (i < numExistingViews) {
+ View convertView = childContainer.getChildAt(i);
+ mAdapter.getChildView(groupPosition, i, isLastChild, convertView, mLinearLayout);
+ } else {
+ View childView = mAdapter.getChildView(groupPosition, i, isLastChild, null, mLinearLayout);
+ // remove the right/left padding so the child total aligns to left
+ childView.setPadding(0,
+ childView.getPaddingTop(),
+ 0,
+ isLastChild ? 0 : childView.getPaddingBottom()); // No padding bottom on last child
+ setViewBackgroundWithoutResettingPadding(childView, R.drawable.stats_list_item_child_background);
+ childContainer.addView(childView);
+ }
+ }
+
+ if (childContainer.getVisibility() != View.VISIBLE) {
+ if (animate) {
+ Animation expand = new ScaleAnimation(1.0f, 1.0f, 0.0f, 1.0f);
+ expand.setDuration(ANIM_DURATION);
+ expand.setInterpolator(getInterpolator());
+ childContainer.startAnimation(expand);
+ }
+ childContainer.setVisibility(View.VISIBLE);
+ }
+
+ StatsUIHelper.setGroupChevron(true, groupView, groupPosition, animate);
+ }
+
+ /**
+ * Removes URL underlines in a string by replacing URLSpan occurrences by
+ * URLSpanNoUnderline objects.
+ *
+ * @param pText A Spannable object. For example, a TextView casted as
+ * Spannable.
+ */
+ public static void removeUnderlines(Spannable pText) {
+ URLSpan[] spans = pText.getSpans(0, pText.length(), URLSpan.class);
+
+ for(URLSpan span:spans) {
+ int start = pText.getSpanStart(span);
+ int end = pText.getSpanEnd(span);
+ pText.removeSpan(span);
+ span = new URLSpanNoUnderline(span.getURL());
+ pText.setSpan(span, start, end, 0);
+ }
+ }
+
+ public static int getNumOfBarsToShow() {
+ if (StatsUtils.getSmallestWidthDP() >= TABLET_720DP && DisplayUtils.isLandscape(WordPress.getContext())) {
+ return 15;
+ } else if (StatsUtils.getSmallestWidthDP() >= TABLET_600DP) {
+ return 10;
+ } else {
+ return 7;
+ }
+ }
+}
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 "";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java
new file mode 100644
index 000000000..3ca853a8f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java
@@ -0,0 +1,170 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.models.VideoPlaysModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsVideoplaysFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsVideoplaysFragment.class.getSimpleName();
+
+ private VideoPlaysModel mVideos;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mVideos != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mVideos);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mVideos = (VideoPlaysModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.VideoPlaysUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mVideos = event.mVideos;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mVideos = null;
+ showErrorUI(event.mError);
+ }
+
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasVideoplays()) {
+ ArrayAdapter adapter = new TopPostsAndPagesAdapter(getActivity(), getVideoplays());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasVideoplays() {
+ return mVideos != null
+ && mVideos.getPlays() != null
+ && mVideos.getPlays().size() > 0;
+ }
+
+ private List<SingleItemModel> getVideoplays() {
+ if (!hasVideoplays()) {
+ return new ArrayList<SingleItemModel>(0);
+ }
+ return mVideos.getPlays();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasVideoplays() && getVideoplays().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class TopPostsAndPagesAdapter extends ArrayAdapter<SingleItemModel> {
+
+ private final List<SingleItemModel> list;
+ private final LayoutInflater inflater;
+
+ public TopPostsAndPagesAdapter(Context context, List<SingleItemModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final SingleItemModel currentRowData = list.get(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+ // fill data
+ // entries
+ holder.setEntryTextOrLink(currentRowData.getUrl(), currentRowData.getTitle());
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_video_plays;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_plays;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_video;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_video_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.VIDEO_PLAYS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_videos);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java
new file mode 100644
index 000000000..0f0c795b5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java
@@ -0,0 +1,318 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Single item details activity.
+ */
+public class StatsViewAllActivity extends AppCompatActivity {
+
+ public static final String ARG_STATS_VIEW_ALL_TITLE = "arg_stats_view_all_title";
+ private static final String SAVED_STATS_SCROLL_POSITION = "SAVED_STATS_SCROLL_POSITION";
+
+ private boolean mIsInFront;
+ private boolean mIsUpdatingStats;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private ScrollViewExt mOuterScrollView;
+
+ private StatsAbstractListFragment mFragment;
+
+ private int mLocalBlogID = -1;
+ private StatsTimeframe mTimeframe;
+ private StatsViewType mStatsViewType;
+ private String mDate;
+ private Serializable[] mRestResponse;
+ private int mOuterPagerSelectedButtonIndex = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.stats_activity_view_all);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ setTitle(R.string.stats);
+
+ mOuterScrollView = (ScrollViewExt) findViewById(R.id.scroll_view_stats);
+
+ // pull to refresh setup
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ mIsUpdatingStats = false;
+ return;
+ }
+
+ if (mIsUpdatingStats) {
+ AppLog.w(AppLog.T.STATS, "stats are already updating, refresh cancelled");
+ return;
+ }
+
+ refreshStats();
+ }
+ }
+ );
+
+ if (savedInstanceState != null) {
+ mLocalBlogID = savedInstanceState.getInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, -1);
+ Serializable oldData = savedInstanceState.getSerializable(StatsAbstractFragment.ARG_REST_RESPONSE);
+ if (oldData != null && oldData instanceof Serializable[]) {
+ mRestResponse = (Serializable[]) oldData;
+ }
+ mTimeframe = (StatsTimeframe) savedInstanceState.getSerializable(StatsAbstractFragment.ARGS_TIMEFRAME);
+ mDate = savedInstanceState.getString(StatsAbstractFragment.ARGS_SELECTED_DATE);
+ mStatsViewType = (StatsViewType) savedInstanceState.getSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE);
+ mOuterPagerSelectedButtonIndex = savedInstanceState.getInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, 0);
+ final int yScrollPosition = savedInstanceState.getInt(SAVED_STATS_SCROLL_POSITION);
+ if(yScrollPosition != 0) {
+ mOuterScrollView.postDelayed(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ mOuterScrollView.scrollTo(0, yScrollPosition);
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ } else if (getIntent() != null) {
+ Bundle extras = getIntent().getExtras();
+ mLocalBlogID = extras.getInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, -1);
+ mTimeframe = (StatsTimeframe) extras.getSerializable(StatsAbstractFragment.ARGS_TIMEFRAME);
+ mDate = extras.getString(StatsAbstractFragment.ARGS_SELECTED_DATE);
+ mStatsViewType = (StatsViewType) extras.getSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE);
+ mOuterPagerSelectedButtonIndex = extras.getInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, 0);
+
+ // Set a custom activity title if one was passed
+ if (!TextUtils.isEmpty(extras.getString(ARG_STATS_VIEW_ALL_TITLE))) {
+ setTitle(extras.getString(ARG_STATS_VIEW_ALL_TITLE));
+ }
+ }
+
+ if (mStatsViewType == null || mTimeframe == null || mDate == null) {
+ Toast.makeText(this, getResources().getText(R.string.stats_generic_error),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ // Setup the top date label. It's available on those fragments that are affected by the top date selector.
+ TextView dateTextView = (TextView) findViewById(R.id.stats_summary_date);
+ switch (mStatsViewType) {
+ case TOP_POSTS_AND_PAGES:
+ case REFERRERS:
+ case CLICKS:
+ case GEOVIEWS:
+ case AUTHORS:
+ case VIDEO_PLAYS:
+ case SEARCH_TERMS:
+ dateTextView.setText(getDateForDisplayInLabels(mDate, mTimeframe));
+ dateTextView.setVisibility(View.VISIBLE);
+ break;
+ default:
+ dateTextView.setVisibility(View.GONE);
+ break;
+ }
+
+ FragmentManager fm = getFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+ mFragment = (StatsAbstractListFragment) fm.findFragmentByTag("ViewAll-Fragment");
+ if (mFragment == null) {
+ mFragment = getInnerFragment();
+ ft.replace(R.id.stats_single_view_fragment, mFragment, "ViewAll-Fragment");
+ ft.commitAllowingStateLoss();
+ }
+
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_VIEW_ALL_ACCESSED, WordPress.getBlog(mLocalBlogID));
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.UpdateStatusChanged event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+ mSwipeToRefreshHelper.setRefreshing(event.mUpdating);
+ mIsUpdatingStats = event.mUpdating;
+ }
+
+ private String getDateForDisplayInLabels(String date, StatsTimeframe timeframe) {
+ String prefix = getString(R.string.stats_for);
+ switch (timeframe) {
+ case DAY:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT));
+ case WEEK:
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ final Date parsedDate = sdf.parse(date);
+ Calendar c = Calendar.getInstance();
+ c.setTime(parsedDate);
+ String endDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ // last day of this week
+ c.add(Calendar.DAY_OF_WEEK, - 6);
+ String startDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ return String.format(prefix, startDateLabel + " - " + endDateLabel);
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return "";
+ }
+ case MONTH:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_FORMAT));
+ case YEAR:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_YEAR_FORMAT));
+ }
+ return "";
+ }
+
+ private StatsAbstractListFragment getInnerFragment() {
+ StatsAbstractListFragment fragment = null;
+ switch (mStatsViewType) {
+ case TOP_POSTS_AND_PAGES:
+ fragment = new StatsTopPostsAndPagesFragment();
+ break;
+ case REFERRERS:
+ fragment = new StatsReferrersFragment();
+ break;
+ case CLICKS:
+ fragment = new StatsClicksFragment();
+ break;
+ case GEOVIEWS:
+ fragment = new StatsGeoviewsFragment();
+ break;
+ case AUTHORS:
+ fragment = new StatsAuthorsFragment();
+ break;
+ case VIDEO_PLAYS:
+ fragment = new StatsVideoplaysFragment();
+ break;
+ case COMMENTS:
+ fragment = new StatsCommentsFragment();
+ break;
+ case TAGS_AND_CATEGORIES:
+ fragment = new StatsTagsAndCategoriesFragment();
+ break;
+ case PUBLICIZE:
+ fragment = new StatsPublicizeFragment();
+ break;
+ case FOLLOWERS:
+ fragment = new StatsFollowersFragment();
+ break;
+ case SEARCH_TERMS:
+ fragment = new StatsSearchTermsFragment();
+ break;
+ }
+
+ fragment.setTimeframe(mTimeframe);
+ fragment.setDate(mDate);
+
+ Bundle args = new Bundle();
+ args.putInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ args.putSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE, mStatsViewType);
+ args.putBoolean(StatsAbstractListFragment.ARGS_IS_SINGLE_VIEW, true); // Always true here
+ args.putInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mOuterPagerSelectedButtonIndex);
+ args.putSerializable(StatsAbstractFragment.ARG_REST_RESPONSE, mRestResponse);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ outState.putSerializable(StatsAbstractFragment.ARG_REST_RESPONSE, mRestResponse);
+ outState.putSerializable(StatsAbstractFragment.ARGS_TIMEFRAME, mTimeframe);
+ outState.putString(StatsAbstractFragment.ARGS_SELECTED_DATE, mDate);
+ outState.putSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE, mStatsViewType);
+ outState.putInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mOuterPagerSelectedButtonIndex);
+ if (mOuterScrollView.getScrollY() != 0) {
+ outState.putInt(SAVED_STATS_SCROLL_POSITION, mOuterScrollView.getScrollY());
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsInFront = true;
+ NetworkUtils.checkConnection(this); // show the error toast if the network is offline
+ ActivityId.trackLastActivity(ActivityId.STATS_VIEW_ALL);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsInFront = false;
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void refreshStats() {
+ if (mIsUpdatingStats) {
+ return;
+ }
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ AppLog.w(AppLog.T.STATS, "ViewAll on "+ mFragment.getTag() + " > no connection, update canceled");
+ return;
+ }
+
+ if (mFragment != null) {
+ mFragment.refreshStats();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java
new file mode 100644
index 000000000..0230dec77
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java
@@ -0,0 +1,170 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+/**
+ * View holder for stats_list_cell layout
+ */
+public class StatsViewHolder {
+ public final TextView entryTextView;
+ public final TextView totalsTextView;
+ public final WPNetworkImageView networkImageView;
+ public final ImageView chevronImageView;
+ public final ImageView linkImageView;
+ public final ImageView imgMore;
+ public final LinearLayout rowContent;
+
+ public StatsViewHolder(View view) {
+ rowContent = (LinearLayout) view.findViewById(R.id.layout_content);
+ entryTextView = (TextView) view.findViewById(R.id.stats_list_cell_entry);
+ totalsTextView = (TextView) view.findViewById(R.id.stats_list_cell_total);
+ chevronImageView = (ImageView) view.findViewById(R.id.stats_list_cell_chevron);
+ linkImageView = (ImageView) view.findViewById(R.id.stats_list_cell_link);
+ networkImageView = (WPNetworkImageView) view.findViewById(R.id.stats_list_cell_image);
+
+ imgMore = (ImageView) view.findViewById(R.id.image_more);
+ }
+
+ /*
+ * used by stats fragments to set the entry text, making it a clickable link if a url is passed
+ */
+ public void setEntryTextOrLink(final String linkURL, String linkName) {
+ if (entryTextView == null) {
+ return;
+ }
+
+ entryTextView.setText(linkName);
+ if (TextUtils.isEmpty(linkURL)) {
+ entryTextView.setTextColor(entryTextView.getContext().getResources().getColor(R.color.stats_text_color));
+ rowContent.setClickable(false);
+ return;
+ }
+
+ rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String url = linkURL;
+ AppLog.d(AppLog.T.UTILS, "Tapped on the Link: " + url);
+ if (url.startsWith("https://wordpress.com/my-stats")
+ || url.startsWith("http://wordpress.com/my-stats")) {
+ // make sure to load the no-chrome version of Stats over https
+ url = UrlUtils.makeHttps(url);
+ if (url.contains("?")) {
+ // add the no chrome parameters if not available
+ if (!url.contains("?no-chrome") && !url.contains("&no-chrome")) {
+ url += "&no-chrome";
+ }
+ } else {
+ url += "?no-chrome";
+ }
+ AppLog.d(AppLog.T.UTILS, "Opening the Authenticated in-app browser : " + url);
+ // Let's try the global wpcom credentials
+ String statsAuthenticatedUser = AccountHelper.getDefaultAccount().getUserName();
+ if (org.apache.commons.lang.StringUtils.isEmpty(statsAuthenticatedUser)) {
+ // Still empty. Do not eat the event, but let's open the default Web Browser.
+
+ }
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(view.getContext(),
+ url, statsAuthenticatedUser);
+
+ } else if (url.startsWith("https") || url.startsWith("http")) {
+ AppLog.d(AppLog.T.UTILS, "Opening the in-app browser: " + url);
+ WPWebViewActivity.openURL(view.getContext(), url);
+ }
+
+ }
+ }
+ );
+
+ entryTextView.setTextColor(entryTextView.getContext().getResources().getColor(R.color.stats_link_text_color));
+ }
+
+ public void setEntryText(String text) {
+ entryTextView.setText(text);
+ rowContent.setClickable(false);
+ }
+
+ public void setEntryText(String text, int color) {
+ entryTextView.setTextColor(color);
+ setEntryText(text);
+ }
+
+
+ /*
+ * Used by stats fragments to set the entry text, opening the stats details page.
+ */
+ public void setEntryTextOpenDetailsPage(final PostModel currentItem) {
+ if (entryTextView == null) {
+ return;
+ }
+
+ String name = StringEscapeUtils.unescapeHtml(currentItem.getTitle());
+ entryTextView.setText(name);
+ rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ActivityLauncher.viewStatsSinglePostDetails(view.getContext(), currentItem);
+ }
+ });
+ entryTextView.setTextColor(entryTextView.getContext().getResources().getColor(R.color.stats_link_text_color));
+ }
+
+ /*
+ * Used by stats fragments to create the more btn context menu with the "View" option in it.
+ * Opening it with reader if possible.
+ *
+ */
+ public void setMoreButtonOpenInReader(final PostModel currentItem) {
+ if (imgMore == null) {
+ return;
+ }
+
+ imgMore.setVisibility(View.VISIBLE);
+ imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ final Context ctx = view.getContext();
+ PopupMenu popup = new PopupMenu(ctx, view);
+ MenuItem menuItem = popup.getMenu().add(ctx.getString(R.string.stats_view));
+ menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ StatsUtils.openPostInReaderOrInAppWebview(ctx, currentItem);
+ return true;
+ }
+ });
+ popup.show();
+ }
+ });
+ }
+
+
+ public void showChevronIcon() {
+ linkImageView.setVisibility(View.GONE);
+ chevronImageView.setVisibility(View.VISIBLE);
+ }
+
+ public void showLinkIcon() {
+ linkImageView.setVisibility(View.VISIBLE);
+ chevronImageView.setVisibility(View.GONE);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java
new file mode 100644
index 000000000..2510d61e0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.ui.stats;
+
+/**
+ * An enum of the different view types to appear on the stats view.
+ */
+
+public enum StatsViewType {
+ // TIMEFRAME_SELECTOR,
+ GRAPH_AND_SUMMARY,
+ TOP_POSTS_AND_PAGES,
+ REFERRERS,
+ CLICKS,
+ GEOVIEWS,
+ AUTHORS,
+ VIDEO_PLAYS,
+ COMMENTS,
+ TAGS_AND_CATEGORIES,
+ PUBLICIZE,
+ FOLLOWERS,
+ SEARCH_TERMS,
+ INSIGHTS_MOST_POPULAR,
+ INSIGHTS_ALL_TIME,
+ INSIGHTS_TODAY,
+ INSIGHTS_LATEST_POST_SUMMARY,
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java
new file mode 100644
index 000000000..230064f6f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java
@@ -0,0 +1,846 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CheckedTextView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.GraphViewSeries;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+public class StatsVisitorsAndViewsFragment extends StatsAbstractFragment
+ implements StatsBarGraph.OnGestureListener {
+
+ public static final String TAG = StatsVisitorsAndViewsFragment.class.getSimpleName();
+ private static final String ARG_SELECTED_GRAPH_BAR = "ARG_SELECTED_GRAPH_BAR";
+ private static final String ARG_PREV_NUMBER_OF_BARS = "ARG_PREV_NUMBER_OF_BARS";
+ private static final String ARG_SELECTED_OVERVIEW_ITEM = "ARG_SELECTED_OVERVIEW_ITEM";
+ private static final String ARG_CHECKBOX_SELECTED = "ARG_CHECKBOX_SELECTED";
+
+
+ private LinearLayout mGraphContainer;
+ private LinearLayout mNoActivtyThisPeriodContainer;
+ private StatsBarGraph mGraphView;
+ private LinearLayout mModuleButtonsContainer;
+ private TextView mDateTextView;
+ private String[] mStatsDate;
+
+ private LinearLayout mLegendContainer;
+ private CheckedTextView mLegendLabel;
+ private LinearLayout mVisitorsCheckboxContainer;
+ private CheckBox mVisitorsCheckbox;
+ private boolean mIsCheckboxChecked = true;
+
+ private OnDateChangeListener mListener;
+ private OnOverviewItemChangeListener mOverviewItemChangeListener;
+
+ private final OverviewLabel[] overviewItems = {OverviewLabel.VIEWS, OverviewLabel.VISITORS, OverviewLabel.LIKES,
+ OverviewLabel.COMMENTS};
+
+ // Restore the following variables on restart
+ private VisitsModel mVisitsData;
+ private int mSelectedOverviewItemIndex = 0;
+ private int mSelectedBarGraphBarIndex = -1;
+ private int mPrevNumberOfBarsGraph = -1;
+
+ // Container Activity must implement this interface
+ public interface OnDateChangeListener {
+ void onDateChanged(String blogID, StatsTimeframe timeframe, String newDate);
+ }
+
+ // Container Activity must implement this interface
+ public interface OnOverviewItemChangeListener {
+ void onOverviewItemChanged(OverviewLabel newItem);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mListener = (OnDateChangeListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnDateChangeListener");
+ }
+ try {
+ mOverviewItemChangeListener = (OnOverviewItemChangeListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnOverviewItemChangeListener");
+ }
+ }
+
+ void setSelectedOverviewItem(OverviewLabel itemToSelect) {
+ for (int i = 0; i < overviewItems.length; i++) {
+ if (overviewItems[i] == itemToSelect) {
+ mSelectedOverviewItemIndex = i;
+ return;
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.stats_visitors_and_views_fragment, container, false);
+
+ mDateTextView = (TextView) view.findViewById(R.id.stats_summary_date);
+ mGraphContainer = (LinearLayout) view.findViewById(R.id.stats_bar_chart_fragment_container);
+ mModuleButtonsContainer = (LinearLayout) view.findViewById(R.id.stats_pager_tabs);
+ mNoActivtyThisPeriodContainer = (LinearLayout) view.findViewById(R.id.stats_bar_chart_no_activity);
+
+ mLegendContainer = (LinearLayout) view.findViewById(R.id.stats_legend_container);
+ mLegendLabel = (CheckedTextView) view.findViewById(R.id.stats_legend_label);
+ mLegendLabel.setCheckMarkDrawable(null); // Make sure to set a null drawable here. Otherwise the touching area is the same of a TextView
+ mVisitorsCheckboxContainer = (LinearLayout) view.findViewById(R.id.stats_checkbox_visitors_container);
+ mVisitorsCheckbox = (CheckBox) view.findViewById(R.id.stats_checkbox_visitors);
+ mVisitorsCheckbox.setOnClickListener(onCheckboxClicked);
+
+ // Fix an issue on devices with 4.1 or lower, where the Checkbox already uses padding by default internally and overriding it with paddingLeft
+ // causes the issue report here https://github.com/wordpress-mobile/WordPress-Android/pull/2377#issuecomment-77067993
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ mVisitorsCheckbox.setPadding(getResources().getDimensionPixelSize(R.dimen.margin_medium), 0, 0, 0);
+ }
+
+ // Make sure we've all the info to build the tab correctly. This is ALWAYS true
+ if (mModuleButtonsContainer.getChildCount() == overviewItems.length) {
+ for (int i = 0; i < mModuleButtonsContainer.getChildCount(); i++) {
+ LinearLayout currentTab = (LinearLayout) mModuleButtonsContainer.getChildAt(i);
+ boolean isLastItem = i == (overviewItems.length - 1);
+ boolean isChecked = i == mSelectedOverviewItemIndex;
+ TabViewHolder currentTabViewHolder = new TabViewHolder(currentTab, overviewItems[i], isChecked, isLastItem);
+ currentTab.setOnClickListener(TopButtonsOnClickListener);
+ currentTab.setTag(currentTabViewHolder);
+ }
+ mModuleButtonsContainer.setVisibility(View.VISIBLE);
+ }
+
+ return view;
+ }
+
+ private class TabViewHolder {
+ final LinearLayout tab;
+ final LinearLayout innerContainer;
+ final TextView label;
+ final TextView value;
+ final ImageView icon;
+ final OverviewLabel labelItem;
+ boolean isChecked = false;
+ boolean isLastItem = false;
+
+ public TabViewHolder(LinearLayout currentTab, OverviewLabel labelItem, boolean checked, boolean isLastItem) {
+ tab = currentTab;
+ innerContainer = (LinearLayout) currentTab.findViewById(R.id.stats_visitors_and_views_tab_inner_container);
+ label = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_label);
+ label.setText(labelItem.getLabel());
+ value = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_value);
+ icon = (ImageView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_icon);
+ this.labelItem = labelItem;
+ this.isChecked = checked;
+ this.isLastItem = isLastItem;
+ updateBackGroundAndIcon(0);
+ }
+
+ private Drawable getTabIcon() {
+ switch (labelItem) {
+ case VISITORS:
+ return getResources().getDrawable(R.drawable.stats_icon_visitors);
+ case COMMENTS:
+ return getResources().getDrawable(R.drawable.stats_icon_comments);
+ case LIKES:
+ return getResources().getDrawable(R.drawable.stats_icon_likes);
+ default:
+ // Views and when no prev match
+ return getResources().getDrawable(R.drawable.stats_icon_views);
+ }
+ }
+
+ public void updateBackGroundAndIcon(int currentValue) {
+ if (isChecked) {
+ value.setTextColor(getResources().getColor(R.color.orange_jazzy));
+ } else {
+ if (currentValue == 0) {
+ value.setTextColor(getResources().getColor(R.color.grey));
+ } else {
+ value.setTextColor(getResources().getColor(R.color.blue_wordpress));
+ }
+ }
+
+ icon.setImageDrawable(getTabIcon());
+
+ if (isLastItem) {
+ if (isChecked) {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_white);
+ } else {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_blue_light);
+ }
+ } else {
+ if (isChecked) {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_white);
+ } else {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_blue_light);
+ }
+ }
+ }
+
+ public void setChecked(boolean checked) {
+ this.isChecked = checked;
+ }
+ }
+
+ private final View.OnClickListener TopButtonsOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ //LinearLayout tab = (LinearLayout) v;
+ TabViewHolder tabViewHolder = (TabViewHolder) v.getTag();
+
+ if (tabViewHolder.isChecked) {
+ // already checked. Do nothing
+ return;
+ }
+
+ int numberOfTabs = mModuleButtonsContainer.getChildCount();
+ int checkedId = -1;
+ for (int i = 0; i < numberOfTabs; i++) {
+ LinearLayout currentTab = (LinearLayout) mModuleButtonsContainer.getChildAt(i);
+ TabViewHolder currentTabViewHolder = (TabViewHolder) currentTab.getTag();
+ if (tabViewHolder == currentTab.getTag()) {
+ checkedId = i;
+ currentTabViewHolder.setChecked(true);
+ } else {
+ currentTabViewHolder.setChecked(false);
+ }
+ }
+
+ if (checkedId == -1)
+ return;
+
+ mSelectedOverviewItemIndex = checkedId;
+ if (mOverviewItemChangeListener != null) {
+ mOverviewItemChangeListener.onOverviewItemChanged(
+ overviewItems[mSelectedOverviewItemIndex]
+ );
+ }
+ updateUI();
+ }
+ };
+
+
+ private final View.OnClickListener onCheckboxClicked = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Is the view now checked?
+ mIsCheckboxChecked = ((CheckBox) view).isChecked();
+ updateUI();
+ }
+ };
+
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mVisitsData != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mVisitsData);
+ }
+ outState.putInt(ARG_SELECTED_GRAPH_BAR, mSelectedBarGraphBarIndex);
+ outState.putInt(ARG_PREV_NUMBER_OF_BARS, mPrevNumberOfBarsGraph);
+ outState.putInt(ARG_SELECTED_OVERVIEW_ITEM, mSelectedOverviewItemIndex);
+ outState.putBoolean(ARG_CHECKBOX_SELECTED, mVisitorsCheckbox.isChecked());
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mVisitsData = (VisitsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_SELECTED_OVERVIEW_ITEM)) {
+ mSelectedOverviewItemIndex = savedInstanceState.getInt(ARG_SELECTED_OVERVIEW_ITEM, 0);
+ }
+ if (savedInstanceState.containsKey(ARG_SELECTED_GRAPH_BAR)) {
+ mSelectedBarGraphBarIndex = savedInstanceState.getInt(ARG_SELECTED_GRAPH_BAR, -1);
+ }
+ if (savedInstanceState.containsKey(ARG_PREV_NUMBER_OF_BARS)) {
+ mPrevNumberOfBarsGraph = savedInstanceState.getInt(ARG_PREV_NUMBER_OF_BARS, -1);
+ }
+
+ mIsCheckboxChecked = savedInstanceState.getBoolean(ARG_CHECKBOX_SELECTED, true);
+ }
+ }
+
+ @Override
+ protected void showErrorUI(String label) {
+ setupNoResultsUI(false);
+ }
+
+ @Override
+ protected void showPlaceholderUI() {
+ setupNoResultsUI(true);
+ }
+
+ private VisitModel[] getDataToShowOnGraph(VisitsModel visitsData) {
+ List<VisitModel> visitModels = visitsData.getVisits();
+ int numPoints = Math.min(StatsUIHelper.getNumOfBarsToShow(), visitModels.size());
+ int currentPointIndex = numPoints - 1;
+ VisitModel[] visitModelsToShow = new VisitModel[numPoints];
+
+ for (int i = visitModels.size() -1; i >= 0 && currentPointIndex >= 0; i--) {
+ VisitModel currentVisitModel = visitModels.get(i);
+ visitModelsToShow[currentPointIndex] = currentVisitModel;
+ currentPointIndex--;
+ }
+ return visitModelsToShow;
+ }
+
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (mVisitsData == null) {
+ setupNoResultsUI(false);
+ return;
+ }
+
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph(mVisitsData);
+ if (dataToShowOnGraph == null || dataToShowOnGraph.length == 0) {
+ setupNoResultsUI(false);
+ return;
+ }
+
+ // Hide the "no-activity this period" message
+ mNoActivtyThisPeriodContainer.setVisibility(View.GONE);
+
+ // Read the selected Tab in the UI
+ OverviewLabel selectedStatsType = overviewItems[mSelectedOverviewItemIndex];
+
+ // Update the Legend and enable/disable the visitors checkboxes
+ mLegendContainer.setVisibility(View.VISIBLE);
+ mLegendLabel.setText(StringUtils.capitalize(selectedStatsType.getLabel().toLowerCase()));
+ switch(selectedStatsType) {
+ case VIEWS:
+ mVisitorsCheckboxContainer.setVisibility(View.VISIBLE);
+ mVisitorsCheckbox.setEnabled(true);
+ mVisitorsCheckbox.setChecked(mIsCheckboxChecked);
+ break;
+ default:
+ mVisitorsCheckboxContainer.setVisibility(View.GONE);
+ break;
+ }
+
+ // Setting Up labels and prepare variables that hold series
+ final String[] horLabels = new String[dataToShowOnGraph.length];
+ mStatsDate = new String[dataToShowOnGraph.length];
+ GraphView.GraphViewData[] mainSeriesItems = new GraphView.GraphViewData[dataToShowOnGraph.length];
+
+ GraphView.GraphViewData[] secondarySeriesItems = null;
+ if (mIsCheckboxChecked && selectedStatsType == OverviewLabel.VIEWS) {
+ secondarySeriesItems = new GraphView.GraphViewData[dataToShowOnGraph.length];
+ }
+
+ // index of days that should be XXX on the graph
+ final boolean[] weekendDays;
+ if (getTimeframe() == StatsTimeframe.DAY) {
+ weekendDays = new boolean[dataToShowOnGraph.length];
+ } else {
+ weekendDays = null;
+ }
+
+ // Check we have at least one result in the current section.
+ boolean atLeastOneResultIsAvailable = false;
+
+ // Fill series variables with data
+ for (int i = 0; i < dataToShowOnGraph.length; i++) {
+ int currentItemValue = 0;
+ switch(selectedStatsType) {
+ case VIEWS:
+ currentItemValue = dataToShowOnGraph[i].getViews();
+ break;
+ case VISITORS:
+ currentItemValue = dataToShowOnGraph[i].getVisitors();
+ break;
+ case LIKES:
+ currentItemValue = dataToShowOnGraph[i].getLikes();
+ break;
+ case COMMENTS:
+ currentItemValue = dataToShowOnGraph[i].getComments();
+ break;
+ }
+ mainSeriesItems[i] = new GraphView.GraphViewData(i, currentItemValue);
+
+ if (currentItemValue > 0) {
+ atLeastOneResultIsAvailable = true;
+ }
+
+ if (mIsCheckboxChecked && secondarySeriesItems != null) {
+ secondarySeriesItems[i] = new GraphView.GraphViewData(i, dataToShowOnGraph[i].getVisitors());
+ }
+
+ String currentItemStatsDate = dataToShowOnGraph[i].getPeriod();
+ horLabels[i] = getDateLabelForBarInGraph(currentItemStatsDate);
+ mStatsDate[i] = currentItemStatsDate;
+
+ if (weekendDays != null) {
+ SimpleDateFormat from = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ try {
+ Date date = from.parse(currentItemStatsDate);
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ c.setTimeInMillis(date.getTime());
+ weekendDays[i] = c.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY || c.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY;
+ } catch (ParseException e) {
+ weekendDays[i] = false;
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+ }
+
+ if (mGraphContainer.getChildCount() >= 1 && mGraphContainer.getChildAt(0) instanceof GraphView) {
+ mGraphView = (StatsBarGraph) mGraphContainer.getChildAt(0);
+ } else {
+ mGraphContainer.removeAllViews();
+ mGraphView = new StatsBarGraph(getActivity());
+ mGraphContainer.addView(mGraphView);
+ }
+
+ mGraphView.removeAllSeries();
+
+ GraphViewSeries mainSeriesOnScreen = new GraphViewSeries(mainSeriesItems);
+ mainSeriesOnScreen.getStyle().color = getResources().getColor(R.color.stats_bar_graph_main_series);
+ mainSeriesOnScreen.getStyle().outerColor = getResources().getColor(R.color.translucent_grey_lighten_30);
+ mainSeriesOnScreen.getStyle().highlightColor = getResources().getColor(R.color.stats_bar_graph_main_series_highlight);
+ mainSeriesOnScreen.getStyle().outerhighlightColor = getResources().getColor(R.color.stats_bar_graph_outer_highlight);
+ mainSeriesOnScreen.getStyle().padding = DisplayUtils.dpToPx(getActivity(), 5);
+ mGraphView.addSeries(mainSeriesOnScreen);
+
+ // Add the Visitors series if it's checked in the legend
+ if (mIsCheckboxChecked && secondarySeriesItems != null && selectedStatsType == OverviewLabel.VIEWS) {
+ GraphViewSeries secondarySeries = new GraphViewSeries(secondarySeriesItems);
+ secondarySeries.getStyle().padding = DisplayUtils.dpToPx(getActivity(), 10);
+ secondarySeries.getStyle().color = getResources().getColor(R.color.stats_bar_graph_secondary_series);
+ secondarySeries.getStyle().highlightColor = getResources().getColor(R.color.orange_fire);
+ mGraphView.addSeries(secondarySeries);
+ }
+
+ // Setup the Y-axis on Visitors and Views Tabs.
+ // Views and Visitors tabs have the exact same Y-axis as shifting from one Y-axis to another defeats
+ // the purpose of making these bars visually easily to compare.
+ switch(selectedStatsType) {
+ case VISITORS:
+ double maxYValue = getMaxYValueForVisitorsAndView(dataToShowOnGraph);
+ mGraphView.setManualYAxisBounds(maxYValue, 0d);
+ break;
+ default:
+ mGraphView.setManualYAxis(false);
+ break;
+ }
+
+ // Set the Graph Style
+ mGraphView.getGraphViewStyle().setNumHorizontalLabels(dataToShowOnGraph.length);
+ // Set the maximum size a column can get on the screen in PX
+ mGraphView.getGraphViewStyle().setMaxColumnWidth(
+ DisplayUtils.dpToPx(getActivity(), StatsConstants.STATS_GRAPH_BAR_MAX_COLUMN_WIDTH_DP)
+ );
+ mGraphView.setHorizontalLabels(horLabels);
+ mGraphView.setGestureListener(this);
+
+ // If zero results in the current section disable clicks on the graph and show the dialog.
+ mNoActivtyThisPeriodContainer.setVisibility(atLeastOneResultIsAvailable ? View.GONE : View.VISIBLE);
+ mGraphView.setClickable(atLeastOneResultIsAvailable);
+
+ // Draw the background on weekend days
+ mGraphView.setWeekendDays(weekendDays);
+
+ // Reset the bar selected upon rotation of the device when the no. of bars can change with orientation.
+ // Only happens on 720DP tablets
+ if (mPrevNumberOfBarsGraph != -1 && mPrevNumberOfBarsGraph != dataToShowOnGraph.length) {
+ mSelectedBarGraphBarIndex = -1;
+ mPrevNumberOfBarsGraph = dataToShowOnGraph.length;
+ onBarTapped(dataToShowOnGraph.length - 1);
+ mGraphView.highlightBar(dataToShowOnGraph.length - 1);
+ return;
+ }
+
+ mPrevNumberOfBarsGraph = dataToShowOnGraph.length;
+ int barSelectedOnGraph;
+ if (mSelectedBarGraphBarIndex == -1) {
+ // No previous bar was highlighted, highlight the most recent one
+ barSelectedOnGraph = dataToShowOnGraph.length - 1;
+ } else if (mSelectedBarGraphBarIndex < dataToShowOnGraph.length) {
+ barSelectedOnGraph = mSelectedBarGraphBarIndex;
+ } else {
+ // A previous bar was highlighted, but it's out of the screen now. This should never happen atm.
+ barSelectedOnGraph = dataToShowOnGraph.length - 1;
+ mSelectedBarGraphBarIndex = barSelectedOnGraph;
+ }
+
+ updateUIBelowTheGraph(barSelectedOnGraph);
+ mGraphView.highlightBar(barSelectedOnGraph);
+ }
+
+ // Find the max value in Visitors and Views data.
+ // Only checks the Views data, since Visitors is for sure less-equals than Views.
+ private double getMaxYValueForVisitorsAndView(final VisitModel[] dataToShowOnGraph) {
+ if (dataToShowOnGraph == null || dataToShowOnGraph.length == 0) {
+ return 0d;
+ }
+ double largest = Integer.MIN_VALUE;
+
+ for (VisitModel aDataToShowOnGraph : dataToShowOnGraph) {
+ int currentItemValue = aDataToShowOnGraph.getViews();
+ if (currentItemValue > largest) {
+ largest = currentItemValue;
+ }
+ }
+ return largest;
+ }
+
+ //update the area right below the graph
+ private void updateUIBelowTheGraph(int itemPosition) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (mVisitsData == null) {
+ setupNoResultsUI(false);
+ return;
+ }
+
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph(mVisitsData);
+
+ // Make sure we've data to show on the screen
+ if (dataToShowOnGraph.length == 0) {
+ return;
+ }
+
+ // This check should never be true, since we put a check on the index in the calling function updateUI()
+ if (dataToShowOnGraph.length <= itemPosition || itemPosition == -1) {
+ // Make sure we're not highlighting
+ itemPosition = dataToShowOnGraph.length -1;
+ }
+
+ String date = mStatsDate[itemPosition];
+ if (date == null) {
+ AppLog.w(AppLog.T.STATS, "Cannot update the area below the graph if a null date is passed!!");
+ return;
+ }
+
+ mDateTextView.setText(getDateForDisplayInLabels(date, getTimeframe()));
+
+ VisitModel modelTapped = dataToShowOnGraph[itemPosition];
+ for (int i=0 ; i < mModuleButtonsContainer.getChildCount(); i++) {
+ View o = mModuleButtonsContainer.getChildAt(i);
+ if (o instanceof LinearLayout && o.getTag() instanceof TabViewHolder) {
+ TabViewHolder tabViewHolder = (TabViewHolder)o.getTag();
+ int currentValue = 0;
+ switch (tabViewHolder.labelItem) {
+ case VIEWS:
+ currentValue = modelTapped.getViews();
+ break;
+ case VISITORS:
+ currentValue = modelTapped.getVisitors();
+ break;
+ case LIKES:
+ currentValue = modelTapped.getLikes();
+ break;
+ case COMMENTS:
+ currentValue = modelTapped.getComments();
+ break;
+ }
+ tabViewHolder.value.setText(FormatUtils.formatDecimal(currentValue));
+ tabViewHolder.updateBackGroundAndIcon(currentValue);
+ }
+ }
+ }
+
+ private String getDateForDisplayInLabels(String date, StatsTimeframe timeframe) {
+ String prefix = getString(R.string.stats_for);
+ switch (timeframe) {
+ case DAY:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT));
+ case WEEK:
+ try {
+ SimpleDateFormat sdf;
+ Calendar c;
+ final Date parsedDate;
+ // Used in bar graph
+ // first four digits are the year
+ // followed by Wxx where xx is the month
+ // followed by Wxx where xx is the day of the month
+ // ex: 2013W07W22 = July 22, 2013
+ sdf = new SimpleDateFormat("yyyy'W'MM'W'dd");
+ //Calculate the end of the week
+ parsedDate = sdf.parse(date);
+ c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ c.setTime(parsedDate);
+ // first day of this week
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY );
+ String startDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ // last day of this week
+ c.add(Calendar.DAY_OF_WEEK, + 6);
+ String endDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ return String.format(prefix, startDateLabel + " - " + endDateLabel);
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return "";
+ }
+ case MONTH:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_FORMAT));
+ case YEAR:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_YEAR_FORMAT));
+ }
+ return "";
+ }
+
+ /**
+ * Return the date string that is displayed under each bar in the graph
+ */
+ private String getDateLabelForBarInGraph(String dateToFormat) {
+ switch (getTimeframe()) {
+ case DAY:
+ return StatsUtils.parseDate(
+ dateToFormat,
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT
+ );
+ case WEEK:
+ // first four digits are the year
+ // followed by Wxx where xx is the month
+ // followed by Wxx where xx is the day of the month
+ // ex: 2013W07W22 = July 22, 2013
+ return StatsUtils.parseDate(dateToFormat, "yyyy'W'MM'W'dd", StatsConstants.STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT);
+ case MONTH:
+ return StatsUtils.parseDate(dateToFormat, "yyyy-MM", "MMM");
+ case YEAR:
+ return StatsUtils.parseDate(dateToFormat, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_YEAR_FORMAT);
+ default:
+ return dateToFormat;
+ }
+ }
+
+ private void setupNoResultsUI(boolean isLoading) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Hide the legend
+ mLegendContainer.setVisibility(View.GONE);
+ mVisitorsCheckboxContainer.setVisibility(View.GONE);
+
+ mSelectedBarGraphBarIndex = -1;
+ Context context = mGraphContainer.getContext();
+ if (context != null) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View emptyBarGraphView = inflater.inflate(R.layout.stats_bar_graph_empty, mGraphContainer, false);
+
+ final TextView emptyLabel = (TextView) emptyBarGraphView.findViewById(R.id.stats_bar_graph_empty_label);
+ emptyLabel.setText("");
+ if (!isLoading) {
+ mNoActivtyThisPeriodContainer.setVisibility(View.VISIBLE);
+ }
+
+ if (emptyBarGraphView != null) {
+ mGraphContainer.removeAllViews();
+ mGraphContainer.addView(emptyBarGraphView);
+ }
+ }
+ mDateTextView.setText("");
+
+ for (int i=0 ; i < mModuleButtonsContainer.getChildCount(); i++) {
+ View o = mModuleButtonsContainer.getChildAt(i);
+ if (o instanceof CheckedTextView) {
+ CheckedTextView currentBtm = (CheckedTextView)o;
+ OverviewLabel overviewItem = (OverviewLabel)currentBtm.getTag();
+ String labelPrefix = overviewItem.getLabel() + "\n 0" ;
+ currentBtm.setText(labelPrefix);
+ }
+ }
+ }
+
+ @Override
+ protected String getTitle() {
+ return getString(R.string.stats_view_visitors_and_views);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.VisitorsAndViewsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mVisitsData = event.mVisitsAndViews;
+ mSelectedBarGraphBarIndex = -1;
+
+ // Reset the bar to highlight
+ if (mGraphView != null) {
+ mGraphView.resetHighlightBar();
+ }
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mVisitsData = null;
+ mSelectedBarGraphBarIndex = -1;
+
+ // Reset the bar to highlight
+ if (mGraphView != null) {
+ mGraphView.resetHighlightBar();
+ }
+
+ updateUI();
+ }
+
+ @Override
+ public void onBarTapped(int tappedBar) {
+ if (!isAdded()) {
+ return;
+ }
+ //AppLog.d(AppLog.T.STATS, " Tapped bar date " + mStatsDate[tappedBar]);
+ mSelectedBarGraphBarIndex = tappedBar;
+ updateUIBelowTheGraph(tappedBar);
+
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ // Update Stats here
+ String date = mStatsDate[tappedBar];
+ if (date == null) {
+ AppLog.w(AppLog.T.STATS, "A bar was tapped but a null date is received!!");
+ return;
+ }
+
+ //Calculate the correct end date for the selected period
+ String calculatedDate = null;
+
+ try {
+ SimpleDateFormat sdf;
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ final Date parsedDate;
+ switch (getTimeframe()) {
+ case DAY:
+ calculatedDate = date;
+ break;
+ case WEEK:
+ // first four digits are the year
+ // followed by Wxx where xx is the month
+ // followed by Wxx where xx is the day of the month
+ // ex: 2013W07W22 = July 22, 2013
+ sdf = new SimpleDateFormat("yyyy'W'MM'W'dd");
+ //Calculate the end of the week
+ parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+ // first day of this week
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ // last day of this week
+ c.add(Calendar.DAY_OF_WEEK, +6);
+ calculatedDate = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case MONTH:
+ sdf = new SimpleDateFormat("yyyy-MM");
+ //Calculate the end of the month
+ parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+ // last day of this month
+ c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH));
+ calculatedDate = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case YEAR:
+ sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ //Calculate the end of the week
+ parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+ c.set(Calendar.MONTH, Calendar.DECEMBER);
+ c.set(Calendar.DAY_OF_MONTH, 31);
+ calculatedDate = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ }
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ if (calculatedDate == null) {
+ AppLog.w(AppLog.T.STATS, "A call to request new stats stats is made but date received cannot be parsed!! " + date);
+ return;
+ }
+
+ // Update the data below the graph
+ if (mListener!= null) {
+ // Should never be null
+ final Blog currentBlog = WordPress.getBlog(getLocalTableBlogID());
+ if (currentBlog != null && currentBlog.getDotComBlogId() != null) {
+ mListener.onDateChanged(currentBlog.getDotComBlogId(), getTimeframe(), calculatedDate);
+ }
+ }
+
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.STATS_TAPPED_BAR_CHART,
+ WordPress.getBlog(getLocalTableBlogID())
+ );
+ }
+
+ public enum OverviewLabel {
+ VIEWS(R.string.stats_views),
+ VISITORS(R.string.stats_visitors),
+ LIKES(R.string.stats_likes),
+ COMMENTS(R.string.stats_comments),
+ ;
+
+ private final int mLabelResId;
+
+ OverviewLabel(int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId).toUpperCase();
+ }
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.VISITS
+ };
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java
new file mode 100644
index 000000000..30f271981
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.ui.stats;
+
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.style.URLSpan;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPLinkMovementMethod;
+
+class StatsWPLinkMovementMethod extends WPLinkMovementMethod {
+ public static WPLinkMovementMethod getInstance() {
+ if (mMovementMethod == null) {
+ mMovementMethod = new StatsWPLinkMovementMethod();
+ }
+ return mMovementMethod;
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ if (layout == null) {
+ return super.onTouchEvent(widget, buffer, event);
+ }
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
+ if (link.length != 0) {
+ String url = link[0].getURL();
+ AppLog.d(AppLog.T.UTILS, "Tapped on the Link: " + url);
+ if (url.startsWith("https://wordpress.com/my-stats")
+ || url.startsWith("http://wordpress.com/my-stats")) {
+ // make sure to load the no-chrome version of Stats over https
+ url = UrlUtils.makeHttps(url);
+ if (url.contains("?")) {
+ // add the no chrome parameters if not available
+ if (!url.contains("?no-chrome") && !url.contains("&no-chrome")) {
+ url += "&no-chrome";
+ }
+ } else {
+ url += "?no-chrome";
+ }
+ AppLog.d(AppLog.T.UTILS, "Opening the Authenticated in-app browser : " + url);
+ // Let's try the global wpcom credentials
+ String statsAuthenticatedUser = AccountHelper.getDefaultAccount().getUserName();
+ if (org.apache.commons.lang.StringUtils.isEmpty(statsAuthenticatedUser)) {
+ // Still empty. Do not eat the event, but let's open the default Web Browser.
+ return super.onTouchEvent(widget, buffer, event);
+ }
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(widget.getContext(),
+ url, statsAuthenticatedUser);
+ return true;
+ } else if (url.startsWith("https") || url.startsWith("http")) {
+ AppLog.d(AppLog.T.UTILS, "Opening the in-app browser: " + url);
+ WPWebViewActivity.openURL(widget.getContext(), url);
+ return true;
+ }
+ }
+ }
+ return super.onTouchEvent(widget, buffer, event);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java
new file mode 100644
index 000000000..cdef4f32a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java
@@ -0,0 +1,161 @@
+package org.wordpress.android.ui.stats;
+
+/**
+ * The configuration screen for the StatsWidgetProvider widget.
+ */
+
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.List;
+import java.util.Map;
+
+public class StatsWidgetConfigureActivity extends AppCompatActivity
+ implements StatsWidgetConfigureAdapter.OnSiteClickListener {
+
+ private StatsWidgetConfigureAdapter mAdapter;
+ private RecyclerView mRecycleView;
+ private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Find the widget id from the intent.
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ mAppWidgetId = extras.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
+ }
+
+ // Set the result to CANCELED. This will cause the widget host to cancel out of the widget
+ // placement if they press the back button.
+ setResult(RESULT_CANCELED, new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
+
+ // Intent without the widget id, just bail.
+ if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish();
+ return;
+ }
+
+ // If not signed into WordPress inform the user
+ if (!AccountHelper.isSignedIn()) {
+ ToastUtils.showToast(getBaseContext(), R.string.stats_widget_error_no_account, ToastUtils.Duration.LONG);
+ finish();
+ return;
+ }
+
+ // If no visible blogs
+ List<Map<String, Object>> accounts = WordPress.wpDB.getBlogsBy("isHidden = 0", null);
+ if (accounts.size() == 0) {
+ ToastUtils.showToast(getBaseContext(), R.string.stats_widget_error_no_visible_blog, ToastUtils.Duration.LONG);
+ finish();
+ return;
+ }
+
+ // If one blog only, skip config
+ if (accounts.size() == 1) {
+ Map<String, Object> account = accounts.get(0);
+ Integer localID = (Integer) account.get("id");
+ addWidgetToScreenAndFinish(localID);
+ return;
+ }
+
+ setContentView(R.layout.stats_widget_config_activity);
+ setNewAdapter();
+ setupActionBar();
+ setupRecycleView();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ private void setupRecycleView() {
+ mRecycleView = (RecyclerView) findViewById(R.id.recycler_view);
+ mRecycleView.setLayoutManager(new LinearLayoutManager(this));
+ mRecycleView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+ mRecycleView.setItemAnimator(null);
+ mRecycleView.setAdapter(getAdapter());
+ }
+
+ private void setupActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ actionBar.setHomeButtonEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ private StatsWidgetConfigureAdapter getAdapter() {
+ if (mAdapter == null) {
+ setNewAdapter();
+ }
+ return mAdapter;
+ }
+
+ private void setNewAdapter() {
+ Blog blog = WordPress.getCurrentBlog();
+ int localBlogId = (blog != null ? blog.getLocalTableBlogId() : 0);
+ mAdapter = new StatsWidgetConfigureAdapter(this, localBlogId);
+ mAdapter.setOnSiteClickListener(this);
+ }
+
+ @Override
+ public void onSiteClick(StatsWidgetConfigureAdapter.SiteRecord site) {
+ addWidgetToScreenAndFinish(site.localId);
+ }
+
+ private void addWidgetToScreenAndFinish(int localID) {
+ final Blog currentBlog = WordPress.getBlog(localID);
+
+ if (currentBlog == null) {
+ AppLog.e(AppLog.T.STATS, "The blog with local_blog_id " + localID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ if (currentBlog.getDotComBlogId() == null) {
+ // The blog could be a self-hosted blog with NO Jetpack installed on it
+ // Or a Jetpack blog whose options are not yet synched in the app
+ // In both of these cases show a generic message that encourages the user to refresh
+ // the blog within the app. There are so many different paths here that's better to handle them in the app.
+ Toast.makeText(this, R.string.stats_widget_error_jetpack_no_blogid, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ final Context context = StatsWidgetConfigureActivity.this;
+ StatsWidgetProvider.setupNewWidget(context, mAppWidgetId, localID);
+ // Make sure we pass back the original appWidgetId
+ Intent resultValue = new Intent();
+ resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+ setResult(RESULT_OK, resultValue);
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java
new file mode 100644
index 000000000..cb18d7393
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java
@@ -0,0 +1,300 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class StatsWidgetConfigureAdapter extends RecyclerView.Adapter<StatsWidgetConfigureAdapter.SiteViewHolder> {
+
+ interface OnSiteClickListener {
+ void onSiteClick(SiteRecord site);
+ }
+
+ private final int mTextColorNormal;
+ private final int mTextColorHidden;
+
+ private static int mBlavatarSz;
+
+ private SiteList mSites = new SiteList();
+ private final int mCurrentLocalId;
+
+ private final Drawable mSelectedItemBackground;
+
+ private final LayoutInflater mInflater;
+
+ private boolean mShowHiddenSites = false;
+ private boolean mShowSelfHostedSites = true;
+
+ private OnSiteClickListener mSiteSelectedListener;
+
+ class SiteViewHolder extends RecyclerView.ViewHolder {
+ private final ViewGroup layoutContainer;
+ private final TextView txtTitle;
+ private final TextView txtDomain;
+ private final WPNetworkImageView imgBlavatar;
+ private final View divider;
+ private Boolean isSiteHidden;
+
+ public SiteViewHolder(View view) {
+ super(view);
+ layoutContainer = (ViewGroup) view.findViewById(R.id.layout_container);
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtDomain = (TextView) view.findViewById(R.id.text_domain);
+ imgBlavatar = (WPNetworkImageView) view.findViewById(R.id.image_blavatar);
+ divider = view.findViewById(R.id.divider);
+ isSiteHidden = null;
+
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mSiteSelectedListener != null) {
+ int clickedPosition = getAdapterPosition();
+ mSiteSelectedListener.onSiteClick(getItem(clickedPosition));
+ }
+ }
+ });
+ }
+ }
+
+ public StatsWidgetConfigureAdapter(Context context, int currentLocalBlogId) {
+ super();
+
+ setHasStableIds(true);
+
+ mCurrentLocalId = currentLocalBlogId;
+ mInflater = LayoutInflater.from(context);
+
+ mBlavatarSz = context.getResources().getDimensionPixelSize(R.dimen.blavatar_sz);
+ mTextColorNormal = context.getResources().getColor(R.color.grey_dark);
+ mTextColorHidden = context.getResources().getColor(R.color.grey);
+
+ mSelectedItemBackground = new ColorDrawable(context.getResources().getColor(R.color.translucent_grey_lighten_20));
+
+ loadSites();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSites.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).localId;
+ }
+
+ private SiteRecord getItem(int position) {
+ return mSites.get(position);
+ }
+
+ public void setOnSiteClickListener(OnSiteClickListener listener) {
+ mSiteSelectedListener = listener;
+ }
+
+ @Override
+ public SiteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = mInflater.inflate(R.layout.site_picker_listitem, parent, false);
+ return new SiteViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(SiteViewHolder holder, int position) {
+ SiteRecord site = getItem(position);
+
+ holder.txtTitle.setText(site.getBlogNameOrHomeURL());
+ holder.txtDomain.setText(site.homeURL);
+ holder.imgBlavatar.setImageUrl(site.blavatarUrl, WPNetworkImageView.ImageType.BLAVATAR);
+
+ if (site.localId == mCurrentLocalId) {
+ holder.layoutContainer.setBackgroundDrawable(mSelectedItemBackground);
+ } else {
+ holder.layoutContainer.setBackgroundDrawable(null);
+ }
+
+ // different styling for visible/hidden sites
+ if (holder.isSiteHidden == null || holder.isSiteHidden != site.isHidden) {
+ holder.isSiteHidden = site.isHidden;
+ holder.txtTitle.setTextColor(site.isHidden ? mTextColorHidden : mTextColorNormal);
+ holder.txtTitle.setTypeface(holder.txtTitle.getTypeface(), site.isHidden ? Typeface.NORMAL : Typeface.BOLD);
+ holder.imgBlavatar.setAlpha(site.isHidden ? 0.5f : 1f);
+ }
+
+ // hide the divider for the last item
+ boolean isLastItem = (position == getItemCount() - 1);
+ holder.divider.setVisibility(isLastItem ? View.INVISIBLE : View.VISIBLE);
+ }
+
+
+ private void loadSites() {
+ if (mIsTaskRunning) {
+ AppLog.w(AppLog.T.UTILS, "site picker > already loading sites");
+ } else {
+ new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ /*
+ * AsyncTask which loads sites from database and populates the adapter
+ */
+ private boolean mIsTaskRunning;
+ private class LoadSitesTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ List<Map<String, Object>> blogs;
+ String[] extraFields = {"isHidden", "dotcomFlag", "homeURL"};
+
+ blogs = getBlogsForCurrentView(extraFields);
+ SiteList sites = new SiteList(blogs);
+
+ // sort by blog/host
+ final long primaryBlogId = AccountHelper.getDefaultAccount().getPrimaryBlogId();
+ Collections.sort(sites, new Comparator<SiteRecord>() {
+ public int compare(SiteRecord site1, SiteRecord site2) {
+ if (primaryBlogId > 0) {
+ if (site1.blogId == primaryBlogId) {
+ return -1;
+ } else if (site2.blogId == primaryBlogId) {
+ return 1;
+ }
+ }
+ return site1.getBlogNameOrHomeURL().compareToIgnoreCase(site2.getBlogNameOrHomeURL());
+ }
+ });
+
+ if (mSites == null || !mSites.isSameList(sites)) {
+ mSites = sites;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void results) {
+ notifyDataSetChanged();
+ mIsTaskRunning = false;
+ }
+
+ private List<Map<String, Object>> getBlogsForCurrentView(String[] extraFields) {
+ if (mShowHiddenSites) {
+ if (mShowSelfHostedSites) {
+ // all self-hosted blogs and all wp.com blogs
+ return WordPress.wpDB.getBlogsBy(null, extraFields);
+ } else {
+ // only wp.com blogs
+ return WordPress.wpDB.getBlogsBy("dotcomFlag=1", extraFields);
+ }
+ } else {
+ if (mShowSelfHostedSites) {
+ // all self-hosted blogs plus visible wp.com blogs
+ return WordPress.wpDB.getBlogsBy("dotcomFlag=0 OR (isHidden=0 AND dotcomFlag=1) ", extraFields);
+ } else {
+ // only visible wp.com blogs
+ return WordPress.wpDB.getBlogsBy("isHidden=0 AND dotcomFlag=1", extraFields);
+ }
+ }
+ }
+ }
+
+ /**
+ * SiteRecord is a simplified version of the full account (blog) record
+ */
+ static class SiteRecord {
+ final int localId;
+ final int blogId;
+ final String blogName;
+ final String homeURL;
+ final String url;
+ final String blavatarUrl;
+ final boolean isDotCom;
+ final boolean isHidden;
+
+ SiteRecord(Map<String, Object> account) {
+ localId = MapUtils.getMapInt(account, "id");
+ blogId = MapUtils.getMapInt(account, "blogId");
+ blogName = BlogUtils.getBlogNameOrHomeURLFromAccountMap(account);
+ homeURL = BlogUtils.getHomeURLOrHostNameFromAccountMap(account);
+ url = MapUtils.getMapStr(account, "url");
+ blavatarUrl = GravatarUtils.blavatarFromUrl(url, mBlavatarSz);
+ isDotCom = MapUtils.getMapBool(account, "dotcomFlag");
+ isHidden = MapUtils.getMapBool(account, "isHidden");
+ }
+
+ String getBlogNameOrHomeURL() {
+ if (TextUtils.isEmpty(blogName)) {
+ return homeURL;
+ }
+ return blogName;
+ }
+ }
+
+ static class SiteList extends ArrayList<SiteRecord> {
+ SiteList() { }
+ SiteList(List<Map<String, Object>> accounts) {
+ if (accounts != null) {
+ for (Map<String, Object> account : accounts) {
+ add(new SiteRecord(account));
+ }
+ }
+ }
+
+ boolean isSameList(SiteList sites) {
+ if (sites == null || sites.size() != this.size()) {
+ return false;
+ }
+ int i;
+ for (SiteRecord site: sites) {
+ i = indexOfSite(site);
+ if (i == -1 || this.get(i).isHidden != site.isHidden) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ int indexOfSite(SiteRecord site) {
+ if (site != null && site.blogId > 0) {
+ for (int i = 0; i < size(); i++) {
+ if (site.blogId == this.get(i).blogId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java
new file mode 100644
index 000000000..41efa62fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java
@@ -0,0 +1,541 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.util.SparseArray;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.android.volley.VolleyError;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.stats.exceptions.StatsError;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+
+import java.util.ArrayList;
+
+public class StatsWidgetProvider extends AppWidgetProvider {
+
+ private static void showMessage(Context context, int[] allWidgets, String message){
+ if (allWidgets.length == 0){
+ return;
+ }
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+
+ for (int widgetId : allWidgets) {
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.stats_widget_layout);
+ int remoteBlogID = getRemoteBlogIDFromWidgetID(widgetId);
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ String name;
+ if (blog != null) {
+ name = context.getString(R.string.stats_widget_name_for_blog);
+ name = String.format(name, StringEscapeUtils.unescapeHtml(blog.getNameOrHostUrl()));
+ } else {
+ name = context.getString(R.string.stats_widget_name);
+ }
+ remoteViews.setTextViewText(R.id.blog_title, name);
+
+ remoteViews.setViewVisibility(R.id.stats_widget_error_container, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.stats_widget_values_container, View.GONE);
+ remoteViews.setTextViewText(R.id.stats_widget_error_text, message);
+
+ Intent intent = new Intent(context, WPMainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.setAction("android.intent.action.MAIN");
+ intent.addCategory("android.intent.category.LAUNCHER");
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ remoteViews.setOnClickPendingIntent(R.id.stats_widget_outer_container, pendingIntent);
+
+ appWidgetManager.updateAppWidget(widgetId, remoteViews);
+ }
+ }
+
+ private static void updateTabValue(Context context, RemoteViews remoteViews, int viewId, String text) {
+ remoteViews.setTextViewText(viewId, text);
+ if (text.equals("0")) {
+ remoteViews.setTextColor(viewId, context.getResources().getColor(R.color.grey));
+ }
+ }
+
+ private static void showStatsData(Context context, int[] allWidgets, Blog blog, JSONObject data) {
+ if (allWidgets.length == 0){
+ return;
+ }
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+
+ String name = context.getString(R.string.stats_widget_name_for_blog);
+ name = String.format(name, StringEscapeUtils.unescapeHtml(blog.getNameOrHostUrl()));
+
+ for (int widgetId : allWidgets) {
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.stats_widget_layout);
+ remoteViews.setTextViewText(R.id.blog_title, name);
+
+ remoteViews.setViewVisibility(R.id.stats_widget_error_container, View.GONE);
+ remoteViews.setViewVisibility(R.id.stats_widget_values_container, View.VISIBLE);
+
+ // Update Views
+ updateTabValue(context, remoteViews, R.id.stats_widget_views, data.optString("views", " 0"));
+
+ // Update Visitors
+ updateTabValue(context, remoteViews, R.id.stats_widget_visitors, data.optString("visitors", " 0"));
+
+ // Update Comments
+ updateTabValue(context, remoteViews, R.id.stats_widget_comments, data.optString("comments", " 0"));
+
+ // Update Likes
+ updateTabValue(context, remoteViews, R.id.stats_widget_likes, data.optString("likes", " 0"));
+
+ Intent intent = new Intent(context, StatsActivity.class);
+ intent.putExtra(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, blog.getLocalTableBlogId());
+ intent.putExtra(StatsActivity.ARG_LAUNCHED_FROM, StatsActivity.StatsLaunchedFrom.STATS_WIDGET);
+ intent.putExtra(StatsActivity.ARG_DESIRED_TIMEFRAME, StatsTimeframe.DAY);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, blog.getLocalTableBlogId(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ remoteViews.setOnClickPendingIntent(R.id.stats_widget_outer_container, pendingIntent);
+ appWidgetManager.updateAppWidget(widgetId, remoteViews);
+ }
+ }
+
+ private static void ShowCacheIfAvailableOrGenericError(Context context, int remoteBlogID) {
+ int[] widgetIDs = getWidgetIDsFromRemoteBlogID(remoteBlogID);
+ if (widgetIDs.length == 0){
+ return;
+ }
+
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (blog == null) {
+ AppLog.e(AppLog.T.STATS, "No blog found in the db!");
+ return;
+ }
+
+ String currentDate = StatsUtils.getCurrentDateTZ(localId);
+
+ // Show cached data if available
+ JSONObject cache = getCacheDataForBlog(remoteBlogID, currentDate);
+ if (cache != null) {
+ showStatsData(context, widgetIDs, blog, cache);
+ } else {
+ showMessage(context, widgetIDs, context.getString(R.string.stats_widget_error_generic));
+ }
+ }
+
+ public static void updateWidgetsOnLogout(Context context) {
+ refreshAllWidgets(context);
+ }
+
+ public static void updateWidgetsOnLogin(Context context) {
+ refreshAllWidgets(context);
+ }
+
+ // This is called by the Stats service in case of error
+ public static void updateWidgets(Context context, int remoteBlogID, VolleyError error) {
+ if (error == null) {
+ AppLog.e(AppLog.T.STATS, "Widget received a VolleyError that is null!");
+ return;
+ }
+
+ // If it's an auth error, show it in the widget UI
+ if (error instanceof com.android.volley.AuthFailureError) {
+ int[] widgetIDs = getWidgetIDsFromRemoteBlogID(remoteBlogID);
+ if (widgetIDs.length == 0){
+ return;
+ }
+
+ // Check if Jetpack or .com
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (blog == null) {
+ return;
+ }
+
+ if (blog.isDotcomFlag()) {
+ // User cannot access stats for this .com blog
+ showMessage(context, widgetIDs, context.getString(R.string.stats_widget_error_no_permissions));
+ } else {
+ // Not logged into wpcom, or the main .com account of the app is not linked with this blog
+ showMessage(context, widgetIDs, context.getString(R.string.stats_sign_in_jetpack_different_com_account));
+ }
+ return;
+ }
+
+ ShowCacheIfAvailableOrGenericError(context, remoteBlogID);
+ }
+
+ // This is called by the Stats service in case of error
+ public static void updateWidgets(Context context, int remoteBlogID, StatsError error) {
+ if (error == null) {
+ AppLog.e(AppLog.T.STATS, "Widget received a StatsError that is null!");
+ return;
+ }
+
+ ShowCacheIfAvailableOrGenericError(context, remoteBlogID);
+ }
+
+ // This is called by the Stats service to keep widgets updated
+ public static void updateWidgets(Context context, int remoteBlogID, VisitModel data) {
+ AppLog.d(AppLog.T.STATS, "updateWidgets called for the blogID " + remoteBlogID);
+
+ int[] widgetIDs = getWidgetIDsFromRemoteBlogID(remoteBlogID);
+ if (widgetIDs.length == 0){
+ return;
+ }
+
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (blog == null) {
+ AppLog.e(AppLog.T.STATS, "No blog found in the db!");
+ return;
+ }
+
+ try {
+ String currentDate = StatsUtils.getCurrentDateTZ(blog.getLocalTableBlogId());
+ JSONObject newData = new JSONObject();
+ newData.put("blog_id", data.getBlogID());
+ newData.put("date", currentDate);
+ newData.put("views", data.getViews());
+ newData.put("visitors", data.getVisitors());
+ newData.put("comments", data.getComments());
+ newData.put("likes", data.getLikes());
+
+ // Store new data in cache
+ String prevDataAsString = AppPrefs.getStatsWidgetsData();
+ JSONObject prevData = null;
+ if (!StringUtils.isEmpty(prevDataAsString)) {
+ try {
+ prevData = new JSONObject(prevDataAsString);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+ try {
+ if (prevData == null) {
+ prevData = new JSONObject();
+ }
+ prevData.put(data.getBlogID(), newData);
+ AppPrefs.setStatsWidgetsData(prevData.toString());
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+
+ // Show data on the screen now!
+ showStatsData(context, widgetIDs, blog, newData);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ // This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute in the AppWidgetProviderInfo.
+ // Also called at booting time!
+ // This method is NOT called when the user adds the App Widget.
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ AppLog.d(AppLog.T.STATS, "onUpdate called");
+ refreshWidgets(context, appWidgetIds);
+ }
+
+ /**
+ * This is called when an instance the App Widget is created for the first time.
+ * For example, if the user adds two instances of your App Widget, this is only called the first time.
+ */
+ @Override
+ public void onEnabled(Context context) {
+ AppLog.d(AppLog.T.STATS, "onEnabled called");
+ // Note: don't erase prefs here, since for some reasons this method is called after the booting of the device.
+ }
+
+ /**
+ * This is called when the last instance of your App Widget is deleted from the App Widget host.
+ * This is where you should clean up any work done in onEnabled(Context), such as delete a temporary database.
+ * @param context The Context in which this receiver is running.
+ */
+ @Override
+ public void onDisabled(Context context) {
+ AppLog.d(AppLog.T.STATS, "onDisabled called");
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_WIDGET_REMOVED);
+ AnalyticsTracker.flush();
+ AppPrefs.resetStatsWidgetsKeys();
+ AppPrefs.resetStatsWidgetsData();
+ }
+
+ /**
+ * This is called every time an App Widget is deleted from the App Widget host.
+ * @param context The Context in which this receiver is running.
+ * @param widgetIDs Widget IDs to set blank. We cannot remove widget from home screen.
+ */
+ @Override
+ public void onDeleted(Context context, int[] widgetIDs) {
+ setRemoteBlogIDForWidgetIDs(widgetIDs, null);
+ }
+
+ public static void enqueueStatsRequestForBlog(Context context, String remoteBlogID, String date) {
+ // start service to get stats
+ Intent intent = new Intent(context, StatsService.class);
+ intent.putExtra(StatsService.ARG_BLOG_ID, remoteBlogID);
+ intent.putExtra(StatsService.ARG_PERIOD, StatsTimeframe.DAY);
+ intent.putExtra(StatsService.ARG_DATE, date);
+ intent.putExtra(StatsService.ARG_SECTION, new int[]{StatsService.StatsEndpointsEnum.VISITS.ordinal()});
+ context.startService(intent);
+ }
+
+ private static synchronized JSONObject getCacheDataForBlog(int remoteBlogID, String date) {
+ String prevDataAsString = AppPrefs.getStatsWidgetsData();
+ if (StringUtils.isEmpty(prevDataAsString)) {
+ AppLog.i(AppLog.T.STATS, "No cache found for the widgets");
+ return null;
+ }
+
+ try {
+ JSONObject prevData = new JSONObject(prevDataAsString);
+ if (!prevData.has(String.valueOf(remoteBlogID))) {
+ AppLog.i(AppLog.T.STATS, "No cache found for the blog ID " + remoteBlogID);
+ return null;
+ }
+
+ JSONObject cache = prevData.getJSONObject(String.valueOf(remoteBlogID));
+ String dateStoredInCache = cache.optString("date");
+ if (date.equals(dateStoredInCache)) {
+ AppLog.i(AppLog.T.STATS, "Cache found for the blog ID " + remoteBlogID);
+ return cache;
+ } else {
+ AppLog.i(AppLog.T.STATS, "Cache found for the blog ID " + remoteBlogID + " but the date value doesn't match!!");
+ return null;
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ return null;
+ }
+ }
+
+ public static synchronized boolean isBlogDisplayedInWidget(int remoteBlogID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ if (StringUtils.isEmpty(prevWidgetKeysString)) {
+ return false;
+ }
+ try {
+ JSONObject prevKeys = new JSONObject(prevWidgetKeysString);
+ JSONArray allKeys = prevKeys.names();
+ if (allKeys == null) {
+ return false;
+ }
+ for (int i=0; i < allKeys.length(); i ++) {
+ String currentKey = allKeys.getString(i);
+ int currentBlogID = prevKeys.getInt(currentKey);
+ if (currentBlogID == remoteBlogID) {
+ return true;
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ return false;
+ }
+
+ private static synchronized int[] getWidgetIDsFromRemoteBlogID(int remoteBlogID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ if (StringUtils.isEmpty(prevWidgetKeysString)) {
+ return new int[0];
+ }
+ ArrayList<Integer> widgetIDs = new ArrayList<>();
+
+ try {
+ JSONObject prevKeys = new JSONObject(prevWidgetKeysString);
+ JSONArray allKeys = prevKeys.names();
+ if (allKeys == null) {
+ return new int[0];
+ }
+ for (int i=0; i < allKeys.length(); i ++) {
+ String currentKey = allKeys.getString(i);
+ int currentBlogID = prevKeys.getInt(currentKey);
+ if (currentBlogID == remoteBlogID) {
+ AppLog.d(AppLog.T.STATS, "The blog with remoteID " + remoteBlogID + " is displayed in the widget " + currentKey);
+ widgetIDs.add(Integer.parseInt(currentKey));
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ return ArrayUtils.toPrimitive(widgetIDs.toArray(new Integer[widgetIDs.size()]));
+ }
+
+ private static synchronized int getRemoteBlogIDFromWidgetID(int widgetID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ if (StringUtils.isEmpty(prevWidgetKeysString)) {
+ return 0;
+ }
+ try {
+ JSONObject prevKeys = new JSONObject(prevWidgetKeysString);
+ return prevKeys.optInt(String.valueOf(widgetID), 0);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ return 0;
+ }
+
+
+ // Store the association between widgetIDs and the remote blog id into prefs.
+ private static void setRemoteBlogIDForWidgetIDs(int[] widgetIDs, String remoteBlogID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ JSONObject prevKeys = null;
+ if (!StringUtils.isEmpty(prevWidgetKeysString)) {
+ try {
+ prevKeys = new JSONObject(prevWidgetKeysString);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ if (prevKeys == null) {
+ prevKeys = new JSONObject();
+ }
+
+ for (int widgetID : widgetIDs) {
+ try {
+ prevKeys.put(String.valueOf(widgetID), remoteBlogID);
+ AppPrefs.setStatsWidgetsKeys(prevKeys.toString());
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+ }
+
+ // This is called by the Widget config activity at the end if the process
+ static void setupNewWidget(Context context, int widgetID, int localBlogID) {
+ AppLog.d(AppLog.T.STATS, "setupNewWidget called");
+
+ Blog blog = WordPress.getBlog(localBlogID);
+ if (blog == null) {
+ // it's unlikely that blog is null here.
+ // This method is called from config activity which has loaded the blog fine.
+ showMessage(context, new int[]{widgetID},
+ context.getString(R.string.stats_widget_error_readd_widget));
+ AppLog.e(AppLog.T.STATS, "setupNewWidget: No blog found in the db!");
+ return;
+ }
+
+ // At this point the remote ID cannot be null.
+ String remoteBlogID = blog.getDotComBlogId();
+ // Add the following check just to be safe
+ if (remoteBlogID == null) {
+ showMessage(context, new int[]{widgetID},
+ context.getString(R.string.stats_widget_error_readd_widget));
+ return;
+ }
+
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_WIDGET_ADDED, remoteBlogID);
+ AnalyticsTracker.flush();
+
+ // Store the association between the widget ID and the remote blog id into prefs.
+ setRemoteBlogIDForWidgetIDs(new int[] {widgetID}, remoteBlogID);
+
+ String currentDate = StatsUtils.getCurrentDateTZ(localBlogID);
+
+ // Load cached data if available and show it immediately
+ JSONObject cache = getCacheDataForBlog(Integer.parseInt(remoteBlogID), currentDate);
+ if (cache != null) {
+ showStatsData(context, new int[] {widgetID}, blog, cache);
+ return;
+ }
+
+ if (!NetworkUtils.isNetworkAvailable(context)) {
+ showMessage(context, new int[] {widgetID}, context.getString(R.string.no_network_title));
+ } else {
+ showMessage(context, new int[] {widgetID}, context.getString(R.string.stats_widget_loading_data));
+ enqueueStatsRequestForBlog(context, remoteBlogID, currentDate);
+ }
+ }
+
+
+ private static void refreshWidgets(Context context, int[] appWidgetIds) {
+ // If not signed into WordPress inform the user
+ if (!AccountHelper.isSignedIn()) {
+ showMessage(context, appWidgetIds, context.getString(R.string.stats_widget_error_no_account));
+ return;
+ }
+
+ SparseArray<ArrayList<Integer>> blogsToWidgetIDs = new SparseArray<>();
+ for (int widgetId : appWidgetIds) {
+ int remoteBlogID = getRemoteBlogIDFromWidgetID(widgetId);
+ if (remoteBlogID == 0) {
+ // This could happen on logout when prefs are erased completely since we cannot remove
+ // widgets programmatically from the screen, or during the configuration of new widgets!!!
+ AppLog.e(AppLog.T.STATS, "No remote blog ID for widget ID " + widgetId);
+ showMessage(context, new int[] {widgetId}, context.getString(R.string.stats_widget_error_readd_widget));
+ continue;
+ }
+
+ ArrayList<Integer> widgetIDs = blogsToWidgetIDs.get(remoteBlogID, new ArrayList<Integer>());
+ widgetIDs.add(widgetId);
+ blogsToWidgetIDs.append(remoteBlogID, widgetIDs);
+ }
+
+ // we now have an optimized data structure for our needs. BlogId -> widgetIDs list
+ for(int i = 0; i < blogsToWidgetIDs.size(); i++) {
+ int remoteBlogID = blogsToWidgetIDs.keyAt(i);
+ // get the object by the key.
+ ArrayList<Integer> widgetsList = blogsToWidgetIDs.get(remoteBlogID);
+ int[] currentWidgets = ArrayUtils.toPrimitive(widgetsList.toArray(new Integer[widgetsList.size()]));
+
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (localId == 0 || blog == null) {
+ // No blog in the app
+ showMessage(context, currentWidgets, context.getString(R.string.stats_widget_error_readd_widget));
+ continue;
+ }
+ String currentDate = StatsUtils.getCurrentDateTZ(localId);
+
+ // Load cached data if available and show it immediately
+ JSONObject cache = getCacheDataForBlog(remoteBlogID, currentDate);
+ if (cache != null) {
+ showStatsData(context, currentWidgets, blog, cache);
+ }
+
+ // If network is not available check if NO cache, and show the generic error
+ // If network is available always start a refresh, and show prev data or the loading in progress message.
+ if (!NetworkUtils.isNetworkAvailable(context)) {
+ if (cache == null) {
+ showMessage(context, currentWidgets, context.getString(R.string.stats_widget_error_generic));
+ }
+ } else {
+ if (cache == null) {
+ showMessage(context, currentWidgets, context.getString(R.string.stats_widget_loading_data));
+ }
+ // Make sure to refresh widget data now.
+ enqueueStatsRequestForBlog(context, String.valueOf(remoteBlogID), currentDate);
+ }
+ }
+ }
+
+ private static void refreshAllWidgets(Context context) {
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ ComponentName thisWidget = new ComponentName(context, StatsWidgetProvider.class);
+ int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
+ refreshWidgets(context, allWidgetIds);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java
new file mode 100644
index 000000000..2b3e04c6f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java
@@ -0,0 +1,15 @@
+package org.wordpress.android.ui.stats;
+
+import android.text.TextPaint;
+import android.text.style.URLSpan;
+
+public class URLSpanNoUnderline extends URLSpan {
+ public URLSpanNoUnderline(String url) {
+ super(url);
+ }
+
+ public void updateDrawState(TextPaint drawState) {
+ super.updateDrawState(drawState);
+ drawState.setUnderlineText(false);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java
new file mode 100644
index 000000000..e49cbaf67
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java
@@ -0,0 +1,55 @@
+package org.wordpress.android.ui.stats.adapters;
+
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.StatsViewHolder;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.List;
+
+public class PostsAndPagesAdapter extends ArrayAdapter<PostModel> {
+
+ private final List<PostModel> list;
+ private final LayoutInflater inflater;
+
+ public PostsAndPagesAdapter(Context context, List<PostModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final PostModel currentRowData = list.get(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ // Entry
+ holder.setEntryTextOpenDetailsPage(currentRowData);
+
+ // Setup the more button
+ holder.setMoreButtonOpenInReader(currentRowData);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+ return rowView;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java
new file mode 100644
index 000000000..03670cda8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java
@@ -0,0 +1,130 @@
+package org.wordpress.android.ui.stats.datasets;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import org.wordpress.android.util.AppLog;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * database for all tracks information
+ */
+public class StatsDatabaseHelper extends SQLiteOpenHelper {
+ private static final String DB_NAME = "stats.db";
+ private static final int DB_VERSION = 1;
+
+ /*
+ * database singleton
+ */
+ private static StatsDatabaseHelper mDatabaseHelper;
+ private final static Object mDbLock = new Object();
+ private final Context mContext;
+
+ public static StatsDatabaseHelper getDatabase(Context ctx) {
+ if (mDatabaseHelper == null) {
+ synchronized(mDbLock) {
+ if (mDatabaseHelper == null) {
+ mDatabaseHelper = new StatsDatabaseHelper(ctx);
+ // this ensures that onOpen() is called with a writable database (open will fail if app calls getReadableDb() first)
+ mDatabaseHelper.getWritableDatabase();
+ }
+ }
+ }
+ return mDatabaseHelper;
+ }
+
+ private StatsDatabaseHelper(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ mContext = context;
+ }
+
+
+ public static SQLiteDatabase getReadableDb(Context ctx) {
+ return getDatabase(ctx).getReadableDatabase();
+ }
+ public static SQLiteDatabase getWritableDb(Context ctx) {
+ return getDatabase(ctx).getWritableDatabase();
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ super.onOpen(db);
+ // Used during development to copy database to external storage and read its content.
+ // copyDatabase(db);
+ }
+
+ /*
+ * drop & recreate all tables (essentially clears the db of all data)
+ */
+ public void reset() {
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ dropAllTables(db);
+ createAllTables(db);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createAllTables(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // for now just reset the db when upgrading, future versions may want to avoid this
+ // and modify table structures, etc., on upgrade while preserving data
+ AppLog.i(AppLog.T.STATS, "Upgrading database from version " + oldVersion + " to version " + newVersion);
+ reset();
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // IMPORTANT: do NOT call super() here - doing so throws a SQLiteException
+ AppLog.w(AppLog.T.STATS, "Downgrading database from version " + oldVersion + " to version " + newVersion);
+ reset();
+ }
+
+ private void createAllTables(SQLiteDatabase db) {
+ StatsTable.createTables(db);
+ }
+
+ private void dropAllTables(SQLiteDatabase db) {
+ StatsTable.dropTables(db);
+ }
+
+ /*
+ * used during development to copy database to external storage so we can access it via DDMS
+ */
+ @SuppressWarnings("unused")
+ private void copyDatabase(SQLiteDatabase db) {
+ String copyFrom = db.getPath();
+ String copyTo = mContext.getExternalFilesDir(null).getAbsolutePath() + "/" + DB_NAME;
+
+ try {
+ InputStream input = new FileInputStream(copyFrom);
+ OutputStream output = new FileOutputStream(copyTo);
+
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+
+ output.flush();
+ output.close();
+ input.close();
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.STATS, "failed to copy stats database", e);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java
new file mode 100644
index 000000000..27cad108c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java
@@ -0,0 +1,226 @@
+package org.wordpress.android.ui.stats.datasets;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.ui.stats.StatsTimeframe;
+import org.wordpress.android.ui.stats.service.StatsService.StatsEndpointsEnum;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.SqlUtils;
+
+public class StatsTable {
+
+ private static final String TABLE_NAME = "tbl_stats";
+ public static final int CACHE_TTL_MINUTES = 10;
+ private static final int MAX_RESPONSE_LEN = (int) (1024 * 1024 * 1.8); // 1.8 MB Approx
+
+ static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_NAME + " ("
+ + " id INTEGER PRIMARY KEY ASC," // Also alias for the built-in rowid: "rowid", "oid", or "_rowid_"
+ + " blogID INTEGER NOT NULL," // The local blog_id as stored in the WPDB
+ + " type INTEGER DEFAULT 0," // The type of the stats. TopPost, followers, etc..
+ + " timeframe INTEGER DEFAULT 0," // This could be days, week, years - It's an enum
+ + " date TEXT NOT NULL,"
+ + " jsonData TEXT NOT NULL,"
+ + " maxResult INTEGER DEFAULT 0,"
+ + " page INTEGER DEFAULT 0,"
+ + " timestamp INTEGER NOT NULL," // The unix timestamp of the response
+ + " UNIQUE (blogID, type, timeframe, date) ON CONFLICT REPLACE"
+ + ")");
+ }
+
+ static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+ }
+
+ protected static void reset(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+
+
+ public static String getStats(final Context ctx, final int blogId, final StatsTimeframe timeframe, final String date,
+ final StatsEndpointsEnum sectionToUpdate, final int maxResultsRequested, final int pageRequested) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot insert a null stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return null;
+ }
+
+ String sql = "SELECT * FROM " + TABLE_NAME + " WHERE blogID = ? "
+ + " AND type=?"
+ + " AND timeframe=?"
+ + " AND date=?"
+ + " AND page=?"
+ + " AND maxResult >=?"
+ + " ORDER BY timestamp DESC"
+ + " LIMIT 1";
+
+ String[] args = {
+ Integer.toString(blogId),
+ Integer.toString(sectionToUpdate.ordinal()),
+ Integer.toString(timeframe.ordinal()),
+ date,
+ Integer.toString(pageRequested),
+ Integer.toString(maxResultsRequested),
+ };
+
+ Cursor cursor = StatsDatabaseHelper.getReadableDb(ctx).rawQuery(sql, args);
+
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ long timestamp = cursor.getLong(cursor.getColumnIndex("timestamp"));
+ long currentTime = System.currentTimeMillis();
+ long deltaMS = currentTime - timestamp;
+ if (deltaMS < 0) {
+ // current date is in the past respect to stats date?? Uhhh!
+ return null;
+ }
+
+ deltaMS = deltaMS / 1000; // seconds
+ // check if the cache is fresh
+ if ((deltaMS / 60) > CACHE_TTL_MINUTES) {
+ return null; // cache is expired
+ }
+
+ return cursor.getString(cursor.getColumnIndex("jsonData"));
+ } else {
+ return null;
+ }
+ } catch (IllegalStateException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+
+ return null;
+ }
+
+ public static void insertStats(final Context ctx, final int blogId, final StatsTimeframe timeframe, final String date,
+ final StatsEndpointsEnum sectionToUpdate, final int maxResultsRequested, final int pageRequested,
+ final String jsonResponse, final long responseTimestamp) {
+
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot insert a null stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return;
+ }
+
+ /*
+ * 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 - note that this situation very rarely occurs
+ * https://github.com/android/platform_frameworks_base/blob/master/core/res/res/values/config.xml#L1268
+ * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java/android/database/CursorWindow.java#L103
+ */
+
+ //Check if the response document from the server is less than 1.8MB. getBytes uses UTF-8 on Android.
+ if (jsonResponse.getBytes().length > MAX_RESPONSE_LEN) {
+ AppLog.w(AppLog.T.STATS, "Stats JSON response length > max allowed length of 1.8MB. Current response will not be stored in cache.");
+ return;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ db.beginTransaction();
+ SQLiteStatement stmt = db.compileStatement("INSERT INTO " + TABLE_NAME + " (blogID, type, timeframe, date, " +
+ "jsonData, maxResult, page, timestamp) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)");
+ try {
+ stmt.bindLong(1, blogId);
+ stmt.bindLong(2, sectionToUpdate.ordinal());
+ stmt.bindLong(3, timeframe.ordinal());
+ stmt.bindString(4, date);
+ stmt.bindString(5, jsonResponse);
+ stmt.bindLong(6, maxResultsRequested);
+ stmt.bindLong(7, pageRequested);
+ stmt.bindLong(8, responseTimestamp);
+ stmt.execute();
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /**
+ * Delete expired Stats data from StatsDB
+ */
+ public static boolean deleteOldStats(final Context ctx, final long timestamp) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot delete stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return false;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ try {
+ db.beginTransaction();
+ int rowDeleted = db.delete(TABLE_NAME, "timestamp <= ?", new String[] { Long.toString(timestamp) });
+ db.setTransactionSuccessful();
+ AppLog.d(AppLog.T.STATS, "Number of old stats deleted : " + rowDeleted);
+ return rowDeleted > 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public static boolean deleteStatsForBlog(final Context ctx, final int blogId) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot delete stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return false;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ try {
+ db.beginTransaction();
+ int rowDeleted = db.delete(TABLE_NAME, "blogID=?", new String[] {Integer.toString(blogId)});
+ db.setTransactionSuccessful();
+ AppLog.d(AppLog.T.STATS, "Stats deleted for localBlogID " + blogId);
+ return rowDeleted > 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public static boolean deleteStatsForBlog(final Context ctx, final int blogId, final StatsEndpointsEnum sectionToUpdate ) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot delete stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return false;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ try {
+ db.beginTransaction();
+ int rowDeleted = db.delete(TABLE_NAME, "blogID=? AND type=?",
+ new String[] {Integer.toString(blogId), Integer.toString(sectionToUpdate.ordinal())}
+ );
+ db.setTransactionSuccessful();
+ AppLog.d(AppLog.T.STATS, "Stats deleted for localBlogID " + blogId + " and type " + sectionToUpdate.getRestEndpointPath());
+ return rowDeleted > 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ public static void purgeAll(Context ctx) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot purgeAll stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return;
+ }
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ db.beginTransaction();
+ try {
+ db.execSQL("DELETE FROM " + TABLE_NAME);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java
new file mode 100644
index 000000000..8784ff95f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.ui.stats.exceptions;
+
+import java.io.Serializable;
+
+public class StatsError extends Exception implements Serializable {
+ public StatsError(String errorMessage) {
+ super(errorMessage);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java
new file mode 100644
index 000000000..51ba25535
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java
@@ -0,0 +1,119 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A model to represent a Author
+ */
+public class AuthorModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+ private String mGroupId;
+ private String mName;
+ private String mAvatar;
+ private int mViews;
+ private FollowDataModel mFollowData;
+ private List<PostModel> mPosts;
+
+ public AuthorModel(String mBlogId, String date, String mGroupId, String mName, String mAvatar, int mViews, JSONObject followData) throws JSONException {
+ this.mBlogId = mBlogId;
+ setDate(StatsUtils.toMs(date));
+ this.mGroupId = mGroupId;
+ this.mName = mName;
+ this.mAvatar = mAvatar;
+ this.mViews = mViews;
+ if (followData != null) {
+ this.mFollowData = new FollowDataModel(followData);
+ }
+ }
+
+ public AuthorModel(String blogId, String date, JSONObject authorJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setGroupId(authorJSON.getString("name"));
+ setName(authorJSON.getString("name"));
+ setViews(authorJSON.getInt("views"));
+ setAvatar(JSONUtils.getString(authorJSON, "avatar"));
+
+ // Follow data could return a boolean false
+ JSONObject followData = authorJSON.optJSONObject("follow_data");
+ if (followData != null) {
+ this.mFollowData = new FollowDataModel(followData);
+ }
+
+ JSONArray postsJSON = authorJSON.getJSONArray("posts");
+ mPosts = new ArrayList<>(authorJSON.length());
+ for (int i = 0; i < postsJSON.length(); i++) {
+ JSONObject currentPostJSON = postsJSON.getJSONObject(i);
+ String postId = String.valueOf(currentPostJSON.getInt("id"));
+ String title = currentPostJSON.getString("title");
+ int views = currentPostJSON.getInt("views");
+ String url = currentPostJSON.getString("url");
+ PostModel currentPost = new PostModel(mBlogId, mDate, postId, title, views, url);
+ mPosts.add(currentPost);
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ private void setGroupId(String groupId) {
+ this.mGroupId = groupId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ private void setViews(int total) {
+ this.mViews = total;
+ }
+
+ public FollowDataModel getFollowData() {
+ return mFollowData;
+ }
+
+ public String getAvatar() {
+ return mAvatar;
+ }
+
+ private void setAvatar(String icon) {
+ this.mAvatar = icon;
+ }
+
+ public List<PostModel> getPosts() { return mPosts; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java
new file mode 100644
index 000000000..92b5f6a47
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class AuthorsModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherViews;
+ private List<AuthorModel> mAuthors;
+
+ public AuthorsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ JSONArray authorsJSONArray;
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherViews = firstDayObject.optInt("other_views");
+ authorsJSONArray = firstDayObject.optJSONArray("authors");
+
+ if (authorsJSONArray != null) {
+ mAuthors = new ArrayList<>(authorsJSONArray.length());
+ for (int i = 0; i < authorsJSONArray.length(); i++) {
+ try {
+ JSONObject currentAuthorJSON = authorsJSONArray.getJSONObject(i);
+ AuthorModel currentAuthor = new AuthorModel(blogID, mDate, currentAuthorJSON);
+ mAuthors.add(currentAuthor);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected Author object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<AuthorModel> getAuthors() {
+ return this.mAuthors;
+ }
+
+ public int getOtherViews() {
+ return mOtherViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java
new file mode 100644
index 000000000..814feefd6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+public class BaseStatsModel implements Serializable{
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java
new file mode 100644
index 000000000..660c30767
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java
@@ -0,0 +1,113 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A model to represent a click group stat
+ */
+public class ClickGroupModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+
+ private String mGroupId;
+ private String mName;
+ private String mIcon;
+ private int mViews;
+ private String mUrl;
+ private List<SingleItemModel> mClicks;
+
+ public ClickGroupModel(String blogId, String date, JSONObject clickGroupJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setGroupId(clickGroupJSON.getString("name"));
+ setName(clickGroupJSON.getString("name"));
+ setViews(clickGroupJSON.getInt("views"));
+ setIcon(JSONUtils.getString(clickGroupJSON, "icon"));
+
+ // if URL is set in the response there is one result only. No need to unfold "results"
+ if (!TextUtils.isEmpty(JSONUtils.getString(clickGroupJSON, "url"))) {
+ setUrl(JSONUtils.getString(clickGroupJSON, "url"));
+ } else {
+ JSONArray childrenJSON = clickGroupJSON.getJSONArray("children");
+ mClicks = new ArrayList<>(childrenJSON.length());
+ for (int i = 0; i < childrenJSON.length(); i++) {
+ JSONObject currentResultJSON = childrenJSON.getJSONObject(i);
+ String name = currentResultJSON.getString("name");
+ int totals = currentResultJSON.getInt("views");
+ String icon = currentResultJSON.optString("icon");
+ String url = currentResultJSON.optString("url");
+ SingleItemModel rm = new SingleItemModel(blogId, date, null, name, totals, url, icon);
+ mClicks.add(rm);
+ }
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ private void setGroupId(String groupId) {
+ this.mGroupId = groupId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ private void setViews(int total) {
+ this.mViews = total;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ private void setUrl(String url) {
+ this.mUrl = url;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ private void setIcon(String icon) {
+ this.mIcon = icon;
+ }
+
+ public List<SingleItemModel> getClicks() { return mClicks; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java
new file mode 100644
index 000000000..ef60320aa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class ClicksModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherClicks;
+ private int mTotalClicks;
+ private List<ClickGroupModel> mClickGroups;
+
+ public ClicksModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ JSONArray jClickGroupsArray;
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherClicks = firstDayObject.getInt("other_clicks");
+ this.mTotalClicks = firstDayObject.getInt("total_clicks");
+ jClickGroupsArray = firstDayObject.optJSONArray("clicks");
+
+ if (jClickGroupsArray != null) {
+ mClickGroups = new ArrayList<>(jClickGroupsArray.length());
+ for (int i = 0; i < jClickGroupsArray.length(); i++) {
+ try {
+ JSONObject currentGroupJSON = jClickGroupsArray.getJSONObject(i);
+ ClickGroupModel currentGroupModel = new ClickGroupModel(blogID, mDate, currentGroupJSON);
+ mClickGroups.add(currentGroupModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected ClickGroupModel object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<ClickGroupModel> getClickGroups() {
+ return this.mClickGroups;
+ }
+
+ public int getOtherClicks() {
+ return mOtherClicks;
+ }
+
+ public int getTotalClicks() {
+ return mTotalClicks;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java
new file mode 100644
index 000000000..a970b44bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class CommentFollowersModel extends BaseStatsModel {
+ private String mBlogID;
+ private int mPage;
+ private int mPages;
+ private int mTotal;
+ private List<SingleItemModel> mPosts;
+
+ public CommentFollowersModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPage = response.getInt("page");
+ this.mPages = response.getInt("pages");
+ this.mTotal = response.getInt("total");
+
+ JSONArray postsJSONArray = response.optJSONArray("posts");
+ if (postsJSONArray != null) {
+ mPosts = new ArrayList<>(postsJSONArray.length());
+ for (int i = 0; i < postsJSONArray.length(); i++) {
+ JSONObject currentPostJSON = postsJSONArray.getJSONObject(i);
+ String postId = String.valueOf(currentPostJSON.getInt("id"));
+ String title = currentPostJSON.getString("title");
+ int followers = currentPostJSON.getInt("followers");
+ String url = currentPostJSON.getString("url");
+ SingleItemModel currentPost = new SingleItemModel(blogID, null, postId, title, followers, url, null);
+ mPosts.add(currentPost);
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public List<SingleItemModel> getPosts() {
+ return this.mPosts;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ public int getPages() {
+ return mPages;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java
new file mode 100644
index 000000000..ef7bfe6ac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsConstants;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class CommentsModel extends BaseStatsModel {
+ private String mDate;
+ private String mBlogID;
+ private int mMonthlyComments;
+ private int mTotalComments;
+ private String mMostActiveDay;
+ private String mMostActiveTime;
+ private SingleItemModel mMostCommentedPost;
+
+ private List<PostModel> mPosts;
+ private List<AuthorModel> mAuthors;
+
+ public CommentsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mDate = response.getString("date");
+
+ this.mMonthlyComments = response.getInt("monthly_comments");
+ this.mTotalComments = response.getInt("total_comments");
+ this.mMostActiveDay = response.getString("most_active_day");
+ this.mMostActiveTime = response.getString("most_active_time");
+
+
+ JSONArray postsJSONArray = response.optJSONArray("posts");
+ if (postsJSONArray != null) {
+ mPosts = new ArrayList<>(postsJSONArray.length());
+ for (int i = 0; i < postsJSONArray.length(); i++) {
+ JSONObject currentPostJSON = postsJSONArray.getJSONObject(i);
+ String itemID = String.valueOf(currentPostJSON.getInt("id"));
+ String name = currentPostJSON.getString("name");
+ int totals = currentPostJSON.getInt("comments");
+ String link = currentPostJSON.getString("link");
+ PostModel currentPost = new PostModel(blogID, mDate, itemID, name, totals, link, StatsConstants.ITEM_TYPE_POST);
+ mPosts.add(currentPost);
+ }
+ }
+
+ JSONArray authorsJSONArray = response.optJSONArray("authors");
+ if (authorsJSONArray != null) {
+ mAuthors = new ArrayList<>(authorsJSONArray.length());
+ for (int i = 0; i < authorsJSONArray.length(); i++) {
+ JSONObject currentAuthorJSON = authorsJSONArray.getJSONObject(i);
+ String name = currentAuthorJSON.getString("name");
+ int comments = currentAuthorJSON.getInt("comments");
+ String url = currentAuthorJSON.getString("link");
+ String gravatar = currentAuthorJSON.getString("gravatar");
+ JSONObject followData = currentAuthorJSON.optJSONObject("follow_data");
+ AuthorModel currentAuthor = new AuthorModel(blogID, mDate, url, name, gravatar, comments, followData);
+ mAuthors.add(currentAuthor);
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public List<PostModel> getPosts() {
+ return this.mPosts;
+ }
+
+ public List<AuthorModel> getAuthors() {
+ return this.mAuthors;
+ }
+
+ public int getTotalComments() {
+ return mTotalComments;
+ }
+
+ public int getMonthlyComments() {
+ return mMonthlyComments;
+ }
+
+ public String getMostActiveDay() {
+ return mMostActiveDay;
+ }
+
+ public String getMostActiveTime() {
+ return mMostActiveTime;
+ }
+
+ public SingleItemModel getMostCommentedPost() {
+ return mMostCommentedPost;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java
new file mode 100644
index 000000000..2c8acb829
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+public class FollowDataModel implements Serializable {
+
+ /*
+ "following-text": "Following",
+ "is_following": false,
+ "following-hover-text": "Unfollow",
+ "blog_id": 6098762,
+ "blog_url": "http://ilpostodellefragole.wordpress.com",
+ "blog_title": "Il posto delle fragole",
+ "site_id": 6098762,
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "blog_domain": "ilpostodellefragole.wordpress.com"
+ */
+
+ private String type;
+ private String followText;
+ private String followingText;
+ private String followingHoverText;
+ private boolean isFollowing;
+ private int blogID;
+ private int siteID;
+ private String statsSource;
+ private String blogDomain;
+
+ public transient boolean isRestCallInProgress = false;
+
+ public FollowDataModel(JSONObject followDataJSON) throws JSONException {
+ this.type = followDataJSON.getString("type");
+ JSONObject paramsJSON = followDataJSON.getJSONObject("params");
+ this.followText = paramsJSON.getString("follow-text");
+ this.followingText = paramsJSON.getString("following-text");
+ this.followingHoverText = paramsJSON.getString("following-hover-text");
+ this.isFollowing = paramsJSON.getBoolean("is_following");
+ this.blogID = paramsJSON.getInt("blog_id");
+ this.siteID = paramsJSON.getInt("site_id");
+ this.statsSource = paramsJSON.getString("stat-source");
+ this.blogDomain = paramsJSON.getString("blog_domain");
+ }
+
+ public boolean isFollowing() {
+ return isFollowing;
+ }
+
+ public void setIsFollowing(boolean following) {
+ isFollowing = following;
+ }
+
+ public int getBlogID() {
+ return blogID;
+ }
+
+ public int getSiteID() {
+ return siteID;
+ }
+
+ public String getFollowText() {
+ return followText;
+ }
+
+ public String getFollowingHoverText() {
+ return followingHoverText;
+ }
+
+ public String getFollowingText() {
+ return followingText;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getStatsSource() {
+ return statsSource;
+ }
+
+ public String getBlogDomain() {
+ return blogDomain;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java
new file mode 100644
index 000000000..4efc3bb8c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.io.Serializable;
+
+public class FollowerModel implements Serializable {
+ private String mBlogId;
+ private String mLabel;
+ private String mAvatar;
+ private String mUrl;
+ private FollowDataModel mFollowData;
+ private String mDateSubscribed;
+
+ public FollowerModel(String mBlogId, JSONObject followerJSONData) throws JSONException{
+ this.mBlogId = mBlogId;
+ this.mLabel = followerJSONData.getString("label");
+
+ setAvatar(JSONUtils.getString(followerJSONData, "avatar"));
+ setURL(JSONUtils.getString(followerJSONData, "url"));
+
+ this.mDateSubscribed = followerJSONData.getString("date_subscribed");
+
+ JSONObject followData = followerJSONData.optJSONObject("follow_data");
+ if (followData != null) {
+ this.mFollowData = new FollowDataModel(followData);
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public String getURL() {
+ return mUrl;
+ }
+
+ private boolean setURL(String URL) {
+ if (!TextUtils.isEmpty(URL) && UrlUtils.isValidUrlAndHostNotNull(URL)) {
+ this.mUrl = URL;
+ return true;
+ }
+ return false;
+ }
+
+ public FollowDataModel getFollowData() {
+ return mFollowData;
+ }
+
+ public String getAvatar() {
+ return mAvatar;
+ }
+
+
+
+
+ private void setAvatar(String icon) {
+ this.mAvatar = icon;
+ }
+
+ public String getDateSubscribed() {
+ return mDateSubscribed;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java
new file mode 100644
index 000000000..ec7826cc0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class FollowersModel extends BaseStatsModel {
+ private String mBlogID;
+ private int mPage;
+ private int mPages;
+ private int mTotal;
+ private int mTotalEmail;
+ private int mTotalWPCom;
+ private List<FollowerModel> mSubscribers;
+
+ public FollowersModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPage = response.getInt("page");
+ this.mPages = response.getInt("pages");
+ this.mTotal = response.getInt("total");
+ this.mTotalEmail = response.getInt("total_email");
+ this.mTotalWPCom = response.getInt("total_wpcom");
+
+ JSONArray subscribersJSONArray = response.optJSONArray("subscribers");
+ if (subscribersJSONArray != null) {
+ mSubscribers = new ArrayList<>(subscribersJSONArray.length());
+ for (int i = 0; i < subscribersJSONArray.length(); i++) {
+ JSONObject currentAuthorJSON = subscribersJSONArray.getJSONObject(i);
+ try {
+ FollowerModel currentFollower = new FollowerModel(mBlogID, currentAuthorJSON);
+ mSubscribers.add(currentFollower);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected Follower object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public List<FollowerModel> getFollowers() {
+ return this.mSubscribers;
+ }
+
+ public int getTotalEmail() {
+ return mTotalEmail;
+ }
+
+ public int getTotalWPCom() {
+ return mTotalWPCom;
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ public int getPages() {
+ return mPages;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java
new file mode 100644
index 000000000..bab8f4541
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+/**
+ * A model to represent a geoview stat.
+ */
+public class GeoviewModel implements Serializable {
+ private final String mCountryShortName;
+ private final String mCountryFullName;
+ private int mViews;
+ private final String mFlagIconURL;
+ private final String mFlatFlagIconURL;
+
+ public GeoviewModel(String countryShortName, String countryFullName, int views, String flagIcon, String flatFlagIcon) {
+ this.mCountryShortName = countryShortName;
+ this.mCountryFullName = countryFullName;
+ this.mViews = views;
+ this.mFlagIconURL = flagIcon;
+ this.mFlatFlagIconURL = flatFlagIcon;
+ }
+
+ public String getCountryFullName() {
+ return mCountryFullName;
+ }
+
+ public String getCountryShortName() {
+ return mCountryShortName;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public void setViews(int views) {
+ this.mViews = views;
+ }
+
+ public String getFlagIconURL() {
+ return mFlagIconURL;
+ }
+
+ public String getFlatFlagIconURL() {
+ return mFlatFlagIconURL;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java
new file mode 100644
index 000000000..589147bc0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java
@@ -0,0 +1,96 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class GeoviewsModel extends BaseStatsModel {
+ private String mDate;
+ private String mBlogID;
+ private int otherViews;
+ private int totalViews;
+ private List<GeoviewModel> countries;
+
+ public GeoviewsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String firstDayKey = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(firstDayKey);
+ this.otherViews = firstDayObject.getInt("other_views");
+ this.totalViews = firstDayObject.getInt("total_views");
+
+ JSONObject countryInfoJSON = response.optJSONObject("country-info");
+ JSONArray viewsJSON = firstDayObject.optJSONArray("views");
+
+ if (viewsJSON != null && countryInfoJSON != null) {
+ countries = new ArrayList<>(viewsJSON.length());
+ for (int i = 0; i < viewsJSON.length(); i++) {
+ JSONObject currentCountryJSON = viewsJSON.getJSONObject(i);
+ String currentCountryCode = currentCountryJSON.getString("country_code");
+ int currentCountryViews = currentCountryJSON.getInt("views");
+ String flagIcon = null;
+ String flatFlagIcon = null;
+ String countryFullName = null;
+ JSONObject currentCountryDetails = countryInfoJSON.optJSONObject(currentCountryCode);
+ if (currentCountryDetails != null) {
+ flagIcon = currentCountryDetails.optString("flag_icon");
+ flatFlagIcon = currentCountryDetails.optString("flat_flag_icon");
+ countryFullName = currentCountryDetails.optString("country_full");
+ }
+ GeoviewModel m = new GeoviewModel(currentCountryCode, countryFullName, currentCountryViews, flagIcon, flatFlagIcon);
+ countries.add(m);
+
+ }
+
+ // Sort the countries by views.
+ Collections.sort(countries, new java.util.Comparator<GeoviewModel>() {
+ public int compare(GeoviewModel o1, GeoviewModel o2) {
+ // descending order
+ return o2.getViews() - o1.getViews();
+ }
+ });
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public List<GeoviewModel> getCountries() {
+ return this.countries;
+ }
+
+ public int getOtherViews() {
+ return otherViews;
+ }
+
+ public int getTotalViews() {
+ return totalViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java
new file mode 100644
index 000000000..fd0b02e7b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class InsightsAllTimeModel extends BaseStatsModel {
+
+ private String mBlogID;
+ private String mDate;
+ private int mVisitors;
+ private int mViews;
+ private int mPosts;
+ private String mViewsBestDay;
+ private int mViewsBestDayTotal;
+
+
+ public InsightsAllTimeModel(String blogID, JSONObject response) throws JSONException {
+ this.setBlogID(blogID);
+ this.mDate = response.getString("date");
+ JSONObject stats = response.getJSONObject("stats");
+ this.mPosts = stats.optInt("posts");
+ this.mVisitors = stats.optInt("visitors");
+ this.mViews = stats.optInt("views");
+ this.mViewsBestDay = stats.getString("views_best_day");
+ this.mViewsBestDayTotal = stats.optInt("views_best_day_total");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public int getVisitors() {
+ return mVisitors;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public int getPosts() {
+ return mPosts;
+ }
+
+ public String getViewsBestDay() {
+ return mViewsBestDay;
+ }
+
+ public int getViewsBestDayTotal() {
+ return mViewsBestDayTotal;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java
new file mode 100644
index 000000000..7a70e9e89
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java
@@ -0,0 +1,23 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+public class InsightsLatestPostDetailsModel extends BaseStatsModel {
+ private String mBlogID;
+ private int mViews;
+
+ public InsightsLatestPostDetailsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mViews = response.getInt("views");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public int getPostViewsCount() {
+ return mViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java
new file mode 100644
index 000000000..534348c37
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+public class InsightsLatestPostModel extends BaseStatsModel {
+ private String mBlogID;
+ private String mPostTitle;
+ private String mPostURL;
+ private String mPostDate;
+ private int mPostID;
+ private int mPostViewsCount = Integer.MIN_VALUE;
+ private int mPostCommentCount;
+ private int mPostLikeCount;
+ private int mPostsFound; // if 0 there are no posts on the blog.
+
+ public InsightsLatestPostModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+
+ mPostsFound = response.optInt("found", 0);
+ if (mPostsFound == 0) {
+ // No latest post found!
+ return;
+ }
+
+ JSONArray postsObject = response.getJSONArray("posts");
+ if (postsObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ // Read the first post
+ JSONObject firstPostObject = postsObject.getJSONObject(0);
+
+ this.mPostID = firstPostObject.getInt("ID");
+ this.mPostTitle = firstPostObject.getString("title");
+ this.mPostDate = firstPostObject.getString("date");
+ this.mPostURL = firstPostObject.getString("URL");
+ this.mPostLikeCount = firstPostObject.getInt("like_count");
+
+ JSONObject discussionObject = firstPostObject.optJSONObject("discussion");
+ if (discussionObject != null) {
+ this.mPostCommentCount = discussionObject.optInt("comment_count", 0);
+ }
+ }
+
+ public boolean isLatestPostAvailable() {
+ return mPostsFound > 0;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public String getPostDate() {
+ return mPostDate;
+ }
+
+ public String getPostTitle() {
+ return mPostTitle;
+ }
+
+ public String getPostURL() {
+ return mPostURL;
+ }
+
+ public int getPostID() {
+ return mPostID;
+ }
+
+ public int getPostViewsCount() {
+ return mPostViewsCount;
+ }
+
+ public void setPostViewsCount(int postViewsCount) {
+ this.mPostViewsCount = postViewsCount;
+ }
+
+ public int getPostCommentCount() {
+ return mPostCommentCount;
+ }
+
+ public int getPostLikeCount() {
+ return mPostLikeCount;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java
new file mode 100644
index 000000000..640c96ffa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONObject;
+
+public class InsightsPopularModel extends BaseStatsModel {
+ private final int mHighestHour;
+ private final int mHighestDayOfWeek;
+ private final Double mHighestDayPercent;
+ private final Double mHighestHourPercent;
+ private String mBlogID;
+
+ public InsightsPopularModel(String blogID, JSONObject response) {
+ this.setBlogID(blogID);
+ this.mHighestDayOfWeek = response.optInt("highest_day_of_week");
+ this.mHighestHour = response.optInt("highest_hour");
+ this.mHighestDayPercent = response.optDouble("highest_day_percent");
+ this.mHighestHourPercent = response.optDouble("highest_hour_percent");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public int getHighestHour() {
+ return mHighestHour;
+ }
+
+ public int getHighestDayOfWeek() {
+ return mHighestDayOfWeek;
+ }
+
+ public Double getHighestDayPercent() {
+ return mHighestDayPercent;
+ }
+
+ public Double getHighestHourPercent() {
+ return mHighestHourPercent;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java
new file mode 100644
index 000000000..c633bc2e6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class InsightsTodayModel extends BaseStatsModel {
+
+ private String mBlogID;
+ private String mDate;
+ private String mPeriod;
+ private int mVisitors;
+ private int mViews;
+ private int mLikes;
+ private int mReblogs;
+ private int mComments;
+ private int mFollowers;
+
+ public InsightsTodayModel(String blogID, JSONObject response) throws JSONException {
+ this.setBlogID(blogID);
+ this.mDate = response.getString("date");
+ this.mPeriod = response.getString("period");
+ this.mViews = response.optInt("views");
+ this.mVisitors = response.optInt("visitors");
+ this.mLikes = response.optInt("likes");
+ this.mReblogs = response.optInt("reblogs");
+ this.mComments = response.optInt("comments");
+ this.mFollowers = response.optInt("followers");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public int getReblogs() {
+ return mReblogs;
+ }
+
+ public int getComments() {
+ return mComments;
+ }
+
+ public int getFollowers() {
+ return mFollowers;
+ }
+
+ public int getLikes() {
+ return mLikes;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public int getVisitors() {
+ return mVisitors;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java
new file mode 100644
index 000000000..40a37b8e1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java
@@ -0,0 +1,30 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.wordpress.android.ui.stats.StatsConstants;
+import org.wordpress.android.ui.stats.StatsUtils;
+
+import java.io.Serializable;
+
+public class PostModel extends SingleItemModel implements Serializable {
+
+ private final String mPostType;
+
+ public PostModel(String blogId, String date, String itemID, String title, int totals, String url, String postType) {
+ super(blogId, date, itemID, title, totals, url, null);
+ this.mPostType = postType;
+ }
+
+ public PostModel(String blogId, long date, String itemID, String title, int totals, String url) {
+ super(blogId, date, itemID, title, totals, url, null);
+ this.mPostType = StatsConstants.ITEM_TYPE_POST;
+ }
+
+ public PostModel(String blogId, String itemID, String title, String url, String postType) {
+ super(blogId, StatsUtils.getCurrentDate(), itemID, title, 0, url, null);
+ this.mPostType = postType;
+ }
+
+ public String getPostType() {
+ return mPostType;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java
new file mode 100644
index 000000000..5beb0df95
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java
@@ -0,0 +1,370 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class PostViewsModel implements Serializable {
+ private String mOriginalResponse;
+
+ private int mHighestMonth, mHighestDayAverage, mHighestWeekAverage;
+ private String mDate;
+ private VisitModel[] mDayViews; //Used to build the graph
+ private List<Year> mYears;
+ private List<Year> mAverages;
+ private List<Week> mWeeks;
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public String getOriginalResponse() {
+ return mOriginalResponse;
+ }
+
+ public VisitModel[] getDayViews() {
+ return mDayViews;
+ }
+
+ public int getHighestMonth() {
+ return mHighestMonth;
+ }
+
+ public int getHighestDayAverage() {
+ return mHighestDayAverage;
+ }
+
+ public int getHighestWeekAverage() {
+ return mHighestWeekAverage;
+ }
+
+ public List<Year> getYears() {
+ return mYears;
+ }
+
+ public List<Year> getAverages() {
+ return mAverages;
+ }
+
+ public List<Week> getWeeks() {
+ return mWeeks;
+ }
+
+
+ public PostViewsModel(String response) throws JSONException {
+ this.mOriginalResponse = response;
+ JSONObject responseObj = new JSONObject(response);
+ parseResponseObject(responseObj);
+ }
+
+ public PostViewsModel(JSONObject response) throws JSONException {
+ if (response == null) {
+ return;
+ }
+ this.mOriginalResponse = response.toString();
+ parseResponseObject(response);
+ }
+
+ private void parseResponseObject(JSONObject response) throws JSONException {
+
+ mDate = response.getString("date");
+ mHighestDayAverage = response.getInt("highest_day_average");
+ mHighestWeekAverage = response.getInt("highest_week_average");
+ mHighestMonth = response.getInt("highest_month");
+ mYears = new LinkedList<>();
+ mAverages = new LinkedList<>();
+ mWeeks = new LinkedList<>();
+
+ JSONArray dataJSON = response.getJSONArray("data");
+ if (dataJSON != null) {
+ // Read the position/index of each field in the response
+ JSONArray fieldsJSON = response.getJSONArray("fields");
+ HashMap<String, Integer> fieldColumnsMapping;
+ try {
+ fieldColumnsMapping = new HashMap<>(2);
+ for (int i = 0; i < fieldsJSON.length(); i++) {
+ final String field = fieldsJSON.getString(i);
+ fieldColumnsMapping.put(field, i);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot read the fields indexes from the JSON response", e);
+ throw e;
+ }
+
+ VisitModel[] visitModels = new VisitModel[dataJSON.length()];
+ int viewsColumnIndex = fieldColumnsMapping.get("views");
+ int periodColumnIndex = fieldColumnsMapping.get("period");
+
+ for (int i = 0; i < dataJSON.length(); i++) {
+ try {
+ JSONArray currentDayData = dataJSON.getJSONArray(i);
+ VisitModel currentVisitModel = new VisitModel();
+ currentVisitModel.setPeriod(currentDayData.getString(periodColumnIndex));
+ currentVisitModel.setViews(currentDayData.getInt(viewsColumnIndex));
+ visitModels[i] = currentVisitModel;
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot create the Visit at index " + i, e);
+ }
+ }
+ mDayViews = visitModels;
+ } else {
+ mDayViews = null;
+ }
+
+ parseYears(response);
+ parseAverages(response);
+ parseWeeks(response);
+ }
+
+ private String[] orderKeys(Iterator keys, int numberOfKeys) {
+ // Keys could not be ordered fine. Reordering them.
+ String[] orderedKeys = new String[numberOfKeys];
+ int i = 0;
+ while (keys.hasNext()) {
+ orderedKeys[i] = (String)keys.next();
+ i++;
+ }
+ Arrays.sort(orderedKeys);
+ return orderedKeys;
+ }
+
+ private void parseYears(JSONObject response) {
+ // Parse the Years section
+ try {
+ JSONObject yearsJSON = response.getJSONObject("years");
+ // Keys could not be ordered fine. Reordering them.
+ String[] orderedKeys = orderKeys(yearsJSON.keys(), yearsJSON.length());
+
+ for (String currentYearKey : orderedKeys) {
+ Year currentYear = new Year();
+ currentYear.setLabel(currentYearKey);
+
+ JSONObject currentYearObj = yearsJSON.getJSONObject(currentYearKey);
+ int total = currentYearObj.getInt("total");
+ currentYear.setTotal(total);
+
+ JSONObject monthsJSON = currentYearObj.getJSONObject("months");
+ Iterator<String> monthsKeys = monthsJSON.keys();
+ List<Month> monthsList = new ArrayList<>(monthsJSON.length());
+ while (monthsKeys.hasNext()) {
+ String currentMonthKey = monthsKeys.next();
+ int currentMonthVisits = monthsJSON.getInt(currentMonthKey);
+ monthsList.add(new Month(currentMonthKey, currentMonthVisits));
+ }
+
+ Collections.sort(monthsList, new Comparator<Month>() {
+ public int compare(Month o1, Month o2) {
+ int v1 = Integer.parseInt(o1.getMonth());
+ int v2 = Integer.parseInt(o2.getMonth());
+ // ascending order
+ return v1 - v2;
+ }
+ });
+
+ currentYear.setMonths(monthsList);
+ mYears.add(currentYear);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the Years section", e);
+ }
+ }
+
+ private void parseAverages(JSONObject response) {
+ // Parse the Averages section
+ try {
+ JSONObject averagesJSON = response.getJSONObject("averages");
+ // Keys could not be ordered fine. Reordering them.
+ String[] orderedKeys = orderKeys(averagesJSON.keys(), averagesJSON.length());
+
+ for (String currentJSONKey : orderedKeys) {
+ Year currentAverage = new Year();
+ currentAverage.setLabel(currentJSONKey);
+
+ JSONObject currentAverageJSONObj = averagesJSON.getJSONObject(currentJSONKey);
+ currentAverage.setTotal(currentAverageJSONObj.getInt("overall"));
+
+ JSONObject monthsJSON = currentAverageJSONObj.getJSONObject("months");
+ Iterator<String> monthsKeys = monthsJSON.keys();
+ List<Month> monthsList = new ArrayList<>(monthsJSON.length());
+ while (monthsKeys.hasNext()) {
+ String currentMonthKey = monthsKeys.next();
+ int currentMonthVisits = monthsJSON.getInt(currentMonthKey);
+ monthsList.add(new Month(currentMonthKey, currentMonthVisits));
+ }
+ Collections.sort(monthsList, new java.util.Comparator<Month>() {
+ public int compare(Month o1, Month o2) {
+ int v1 = Integer.parseInt(o1.getMonth());
+ int v2 = Integer.parseInt(o2.getMonth());
+ // ascending order
+ return v1 - v2;
+ }
+ });
+
+ currentAverage.setMonths(monthsList);
+ mAverages.add(currentAverage);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the Averages section", e);
+ }
+ }
+
+ private void parseWeeks(JSONObject response) {
+ // Parse the Weeks section
+ try {
+ JSONArray weeksJSON = response.getJSONArray("weeks");
+ for (int i = 0; i < weeksJSON.length(); i++) {
+ Week currentWeek = new Week();
+ JSONObject currentWeekJSON = weeksJSON.getJSONObject(i);
+
+ currentWeek.setTotal(currentWeekJSON.getInt("total"));
+ currentWeek.setAverage(currentWeekJSON.getInt("average"));
+ try {
+ if (i == 0 ) {
+ currentWeek.setChange(0);
+ } else {
+ currentWeek.setChange(currentWeekJSON.getInt("change"));
+ }
+ } catch (JSONException e){
+ AppLog.w(AppLog.T.STATS, "Cannot parse the change value in weeks section. Trying to understand the meaning: 42!!");
+ // if i == 0 is the first week. if not it could mean infinity
+ String aProblematicValue = currentWeekJSON.get("change").toString();
+ if (aProblematicValue.contains("infinity")) {
+ currentWeek.setChange(Integer.MAX_VALUE);
+ } else {
+ currentWeek.setChange(0);
+ }
+ }
+
+ JSONArray daysJSON = currentWeekJSON.getJSONArray("days");
+ for (int j = 0; j < daysJSON.length(); j++) {
+ Day currentDay = new Day();
+ JSONObject dayJSON = daysJSON.getJSONObject(j);
+ currentDay.setCount(dayJSON.getInt("count"));
+ currentDay.setDay(dayJSON.getString("day"));
+ currentWeek.getDays().add(currentDay);
+ }
+ mWeeks.add(currentWeek);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the Weeks section", e);
+ }
+ }
+
+ public class Day implements Serializable {
+ private int mCount;
+ private String mDay;
+
+ public String getDay() {
+ return mDay;
+ }
+
+ public void setDay(String day) {
+ this.mDay = day;
+ }
+
+ public int getCount() {
+ return mCount;
+ }
+
+ public void setCount(int count) {
+ this.mCount = count;
+ }
+ }
+
+ public class Week implements Serializable {
+ int mChange;
+ int mTotal;
+ int mAverage;
+ List<Day> mDays = new LinkedList<>();
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ public void setTotal(int total) {
+ this.mTotal = total;
+ }
+
+ public int getAverage() {
+ return mAverage;
+ }
+
+ public void setAverage(int average) {
+ this.mAverage = average;
+ }
+
+ public int getChange() {
+ return mChange;
+ }
+
+ public void setChange(int change) {
+ this.mChange = change;
+ }
+
+ public List<Day> getDays() {
+ return mDays;
+ }
+
+ public void setDays(List<Day> days) {
+ this.mDays = days;
+ }
+ }
+
+ public class Year implements Serializable {
+ private String mLabel;
+ private int mTotal;
+ private List<Month> mMonths;
+
+ public List<Month> getMonths() {
+ return mMonths;
+ }
+
+ public void setMonths(List<Month> months) {
+ mMonths = months;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ this.mLabel = label;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ public void setTotal(int total) {
+ this.mTotal = total;
+ }
+ }
+
+ public class Month implements Serializable {
+ private final int mCount;
+ private final String mMonth;
+
+ Month(String label, int count) {
+ this.mMonth = label;
+ this.mCount = count;
+ }
+
+ public String getMonth() {
+ return mMonth;
+ }
+ public int getCount() {
+ return mCount;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java
new file mode 100644
index 000000000..295a28061
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java
@@ -0,0 +1,40 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PublicizeModel extends BaseStatsModel {
+ private String mBlogID;
+ private List<SingleItemModel> mServices;
+
+ public PublicizeModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ JSONArray services = response.getJSONArray("services");
+ if (services.length() > 0) {
+ mServices = new ArrayList<>(services.length());
+ for (int i = 0; i < services.length(); i++) {
+ JSONObject current = services.getJSONObject(i);
+ String serviceName = current.getString("service");
+ int followers = current.getInt("followers");
+ SingleItemModel currentItem = new SingleItemModel(blogID, null, null, serviceName, followers, null, null);
+ mServices.add(currentItem);
+ }
+ }
+ }
+
+ public List<SingleItemModel> getServices() {
+ return mServices;
+ }
+
+ public String getBlogId() {
+ return mBlogID;
+ }
+
+ public void setBlogId(String blogId) {
+ this.mBlogID = blogId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java
new file mode 100644
index 000000000..e093c0006
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java
@@ -0,0 +1,124 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model to represent a referrer group stat
+ */
+public class ReferrerGroupModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+
+ private String mGroupId;
+ private String mName;
+ private String mIcon;
+ private int mTotal;
+ private String mUrl;
+ private List<ReferrerResultModel> mResults;
+
+ public transient boolean isRestCallInProgress = false;
+ public transient boolean isMarkedAsSpam = false;
+
+ public ReferrerGroupModel(String blogId, String date, JSONObject groupJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setGroupId(groupJSON.getString("group"));
+ setName(groupJSON.getString("name"));
+ setTotal(groupJSON.getInt("total"));
+ setIcon(JSONUtils.getString(groupJSON, "icon"));
+
+ // if URL is set in the response there is one result only.
+ if (!TextUtils.isEmpty(JSONUtils.getString(groupJSON, "url"))) {
+ setUrl(JSONUtils.getString(groupJSON, "url"));
+ }
+
+ // results is an array when there are results, otherwise it's an object.
+ JSONArray resultsArray = groupJSON.optJSONArray("results");
+ if (resultsArray != null) {
+ mResults = new ArrayList<>();
+ for (int i = 0; i < resultsArray.length(); i++) {
+ JSONObject currentResultJSON = resultsArray.getJSONObject(i);
+ ReferrerResultModel currentResultModel = new ReferrerResultModel(blogId,
+ date, currentResultJSON);
+ mResults.add(currentResultModel);
+ }
+ // Sort the results by views.
+ Collections.sort(mResults, new java.util.Comparator<ReferrerResultModel>() {
+ public int compare(ReferrerResultModel o1, ReferrerResultModel o2) {
+ // descending order
+ return o2.getViews() - o1.getViews();
+ }
+ });
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ private void setGroupId(String groupId) {
+ this.mGroupId = groupId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ private void setTotal(int total) {
+ this.mTotal = total;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ private void setUrl(String url) {
+ this.mUrl = url;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ private void setIcon(String icon) {
+ this.mIcon = icon;
+ }
+
+ public List<ReferrerResultModel> getResults() { return mResults; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java
new file mode 100644
index 000000000..a2cb1fdc4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java
@@ -0,0 +1,116 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model to represent a referrer result in stat
+ */
+public class ReferrerResultModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+
+ private String mName;
+ private String mIcon;
+ private int mViews;
+ private String mUrl;
+ private List<SingleItemModel> mChildren;
+
+ public ReferrerResultModel(String blogId, String date, JSONObject resultJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setName(resultJSON.getString("name"));
+ setViews(resultJSON.getInt("views"));
+ setIcon(JSONUtils.getString(resultJSON, "icon"));
+
+ if (!TextUtils.isEmpty(JSONUtils.getString(resultJSON, "url"))) {
+ setUrl(JSONUtils.getString(resultJSON, "url"));
+ }
+
+ if (resultJSON.has("children")) {
+ JSONArray childrenJSON = resultJSON.getJSONArray("children");
+ mChildren = new ArrayList<>();
+ for (int i = 0; i < childrenJSON.length(); i++) {
+ JSONObject currentChild = childrenJSON.getJSONObject(i);
+ mChildren.add(getChildren(blogId, date, currentChild));
+ }
+
+ //Sort the children by views.
+ Collections.sort(mChildren, new java.util.Comparator<SingleItemModel>() {
+ public int compare(SingleItemModel o1, SingleItemModel o2) {
+ // descending order
+ return o2.getTotals() - o1.getTotals();
+ }
+ });
+ }
+ }
+
+ private SingleItemModel getChildren(String blogId, String date, JSONObject child) throws JSONException {
+ String name = child.getString("name");
+ int totals = child.getInt("views");
+ String icon = JSONUtils.getString(child, "icon");
+ String url = child.optString("url");
+ return new SingleItemModel(blogId, date, null, name, totals, url, icon);
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ private void setViews(int total) {
+ this.mViews = total;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ private void setIcon(String icon) {
+ this.mIcon = icon;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ private void setUrl(String url) {
+ this.mUrl = url;
+ }
+
+ public List<SingleItemModel> getChildren() { return mChildren; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java
new file mode 100644
index 000000000..62f5f32fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class ReferrersModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherViews;
+ private int mTotalViews;
+ private List<ReferrerGroupModel> mGroups;
+
+ public ReferrersModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ JSONArray jGroupsArray;
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherViews = firstDayObject.optInt("other_views");
+ this.mTotalViews = firstDayObject.optInt("total_views");
+ jGroupsArray = firstDayObject.optJSONArray("groups");
+
+ if (jGroupsArray != null) {
+ mGroups = new ArrayList<>(jGroupsArray.length());
+ for (int i = 0; i < jGroupsArray.length(); i++) {
+ try {
+ JSONObject currentGroupJSON = jGroupsArray.getJSONObject(i);
+ ReferrerGroupModel currentGroupModel = new ReferrerGroupModel(blogID, mDate, currentGroupJSON);
+ mGroups.add(currentGroupModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected ReferrerGroupModel object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<ReferrerGroupModel> getGroups() {
+ return this.mGroups;
+ }
+
+ public int getOtherViews() {
+ return mOtherViews;
+ }
+
+ public int getTotalViews() {
+ return mTotalViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java
new file mode 100644
index 000000000..b83b71b01
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+public class SearchTermModel extends SingleItemModel implements Serializable {
+
+ private final boolean mIsEncriptedTerms;
+
+ public SearchTermModel(String blogId, String date, String title, int totals, boolean isEncriptedTerms) {
+ super(blogId, date, null, title, totals, null, null);
+ this.mIsEncriptedTerms = isEncriptedTerms;
+ }
+
+ public boolean isEncriptedTerms() {
+ return mIsEncriptedTerms;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java
new file mode 100644
index 000000000..49fde8f04
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java
@@ -0,0 +1,108 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class SearchTermsModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private List<SearchTermModel> mSearchTerms;
+ private int mEncryptedSearchTerms, mOtherSearchTerms, mTotalSearchTerms;
+
+ public SearchTermsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONArray searchTermsArray = null;
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ Iterator<String> keys = jDaysObject.keys();
+ if (keys.hasNext()) {
+ String key = keys.next();
+ JSONObject jDateObject = jDaysObject.optJSONObject(key); // This could be an empty array on site with low traffic
+ searchTermsArray = null;
+ if (jDateObject != null) {
+ searchTermsArray = jDateObject.getJSONArray("search_terms");
+ this.mEncryptedSearchTerms = jDateObject.optInt("encrypted_search_terms");
+ this.mOtherSearchTerms = jDateObject.optInt("other_search_terms");
+ this.mTotalSearchTerms = jDateObject.optInt("total_search_terms");
+ }
+ }
+
+ if (searchTermsArray == null) {
+ searchTermsArray = new JSONArray();
+ }
+
+ ArrayList<SearchTermModel> list = new ArrayList<>(searchTermsArray.length());
+ for (int i=0; i < searchTermsArray.length(); i++) {
+ try {
+ JSONObject postObject = searchTermsArray.getJSONObject(i);
+ String term = postObject.getString("term");
+ int total = postObject.getInt("views");
+ SearchTermModel currentModel = new SearchTermModel(blogID, mDate, term, total, false);
+ list.add(currentModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected SearchTerm object in searchterms array" +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+
+ this.mSearchTerms = list;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<SearchTermModel> getSearchTerms() {
+ return mSearchTerms;
+ }
+
+ public boolean hasSearchTerms() {
+ return mSearchTerms != null && mSearchTerms.size() > 0;
+ }
+
+ public int getEncryptedSearchTerms() {
+ return mEncryptedSearchTerms;
+ }
+
+ public int getOtherSearchTerms() {
+ return mOtherSearchTerms;
+ }
+
+ public int getTotalSearchTerms() {
+ return mTotalSearchTerms;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java
new file mode 100644
index 000000000..0f3b2ef23
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.webkit.URLUtil;
+
+import org.wordpress.android.ui.stats.StatsUtils;
+
+import java.io.Serializable;
+
+/*
+* A model to represent a SINGLE stats item
+*/
+public class SingleItemModel implements Serializable {
+ private final String mBlogID;
+ private final String mItemID;
+ private final long mDate;
+ private final String mTitle;
+ private final int mTotals;
+ private final String mUrl;
+ private final String mIcon;
+
+ public SingleItemModel(String blogId, String date, String itemID, String title, int totals, String url, String icon) {
+ this(blogId, StatsUtils.toMs(date), itemID, title, totals, url, icon);
+ }
+
+ SingleItemModel(String blogId, long date, String itemID, String title, int totals, String url, String icon) {
+ this.mBlogID = blogId;
+ this.mItemID = itemID;
+ this.mTitle = title;
+ this.mTotals = totals;
+
+ // We could get invalid data back from the server. Check that URL is OK.
+ if (!URLUtil.isValidUrl(url)) {
+ this.mUrl = "";
+ } else {
+ this.mUrl = url;
+ }
+
+ this.mDate = date;
+ this.mIcon = icon;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public String getItemID() {
+ return mItemID;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public int getTotals() {
+ return mTotals;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java
new file mode 100644
index 000000000..087c3db58
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+public class TagModel implements Serializable {
+ private String mName;
+ private String mLink;
+ private String mType;
+
+ public TagModel(JSONObject tagJSON) throws JSONException {
+
+ this.mName = tagJSON.getString("name");
+ this.mType = tagJSON.getString("type");
+ this.mLink = tagJSON.getString("link");
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getLink() {
+ return mLink;
+ }
+
+ public String getType() {
+ return mType;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java
new file mode 100644
index 000000000..4c35ccc1f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagsContainerModel extends BaseStatsModel {
+ private String mDate;
+ private String mBlogID;
+ private List<TagsModel> mTags;
+
+ public TagsContainerModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mDate = response.getString("date");
+ JSONArray outerTags = response.getJSONArray("tags");
+ if (outerTags != null) {
+ mTags = new ArrayList<>(outerTags.length());
+ for (int i = 0; i < outerTags.length(); i++) {
+ JSONObject current = outerTags.getJSONObject(i);
+ mTags.add(new TagsModel(current));
+ }
+ }
+ }
+
+ public List<TagsModel> getTags() {
+ return mTags;
+ }
+
+ public String getBlogId() {
+ return mBlogID;
+ }
+
+ public void setBlogId(String blogId) {
+ this.mBlogID = blogId;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java
new file mode 100644
index 000000000..19ce375ef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagsModel implements Serializable {
+ private int mViews;
+ private List<TagModel> mTags;
+
+ public TagsModel(JSONObject responseJSON) throws JSONException {
+ this.mViews = responseJSON.getInt("views");
+ JSONArray innerTagsJSON = responseJSON.getJSONArray("tags");
+ mTags = new ArrayList<>(innerTagsJSON.length());
+ for (int i = 0; i < innerTagsJSON.length(); i++) {
+ JSONObject currentTagJSON = innerTagsJSON.getJSONObject(i);
+ TagModel currentTag = new TagModel(currentTagJSON);
+ mTags.add(currentTag);
+ }
+ }
+
+
+
+ public List<TagModel> getTags() {
+ return mTags;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java
new file mode 100644
index 000000000..5ccf3bef4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java
@@ -0,0 +1,93 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class TopPostsAndPagesModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private List<PostModel> mTopPostsAndPages;
+
+ public TopPostsAndPagesModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+ JSONArray postViewsArray = null;
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ Iterator<String> keys = jDaysObject.keys();
+ if (keys.hasNext()) {
+ String key = keys.next();
+ JSONObject jDateObject = jDaysObject.optJSONObject(key); // This could be an empty array on site with low traffic
+ postViewsArray = (jDateObject != null) ? jDateObject.getJSONArray("postviews") : null;
+ }
+
+ if (postViewsArray == null) {
+ postViewsArray = new JSONArray();
+ }
+
+ ArrayList<PostModel> list = new ArrayList<>(postViewsArray.length());
+
+ for (int i=0; i < postViewsArray.length(); i++) {
+ try {
+ JSONObject postObject = postViewsArray.getJSONObject(i);
+ String itemID = postObject.getString("id");
+ String itemTitle = postObject.getString("title");
+ int itemTotal = postObject.getInt("views");
+ String itemURL = postObject.getString("href");
+ String itemType = postObject.getString("type");
+ String itemDate = postObject.getString("date");
+ PostModel currentModel = new PostModel(blogID, itemDate, itemID, itemTitle,
+ itemTotal, itemURL, itemType);
+ list.add(currentModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected PostModel object in top posts and pages array " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ this.mTopPostsAndPages = list;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<PostModel> getTopPostsAndPages() {
+ return mTopPostsAndPages;
+ }
+
+ public boolean hasTopPostsAndPages() {
+ return mTopPostsAndPages != null && mTopPostsAndPages.size() > 0;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java
new file mode 100644
index 000000000..ef31b2f19
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class VideoPlaysModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherPlays;
+ private int mTotalPlays;
+ private List<SingleItemModel> mPlays;
+
+ public VideoPlaysModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherPlays = firstDayObject.getInt("other_plays");
+ this.mTotalPlays = firstDayObject.getInt("total_plays");
+ JSONArray playsJSONArray = firstDayObject.optJSONArray("plays");
+
+ if (playsJSONArray != null) {
+ mPlays = new ArrayList<>(playsJSONArray.length());
+ for (int i = 0; i < playsJSONArray.length(); i++) {
+ JSONObject currentVideoplaysJSON = playsJSONArray.getJSONObject(i);
+ String postId = String.valueOf(currentVideoplaysJSON.getInt("post_id"));
+ String title = currentVideoplaysJSON.getString("title");
+ int views = currentVideoplaysJSON.getInt("plays");
+ String url = currentVideoplaysJSON.getString("url");
+ SingleItemModel currentPost = new SingleItemModel(blogID, mDate, postId, title, views, url, null);
+ mPlays.add(currentPost);
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<SingleItemModel> getPlays() {
+ return this.mPlays;
+ }
+
+ public int getOtherPlays() {
+ return mOtherPlays;
+ }
+
+ public int getTotalPlays() {
+ return mTotalPlays;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java
new file mode 100644
index 000000000..d1f4ac613
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+public class VisitModel implements Serializable {
+
+ private int mViews;
+ private int mLikes;
+ private int mVisitors;
+ private int mComments;
+ private String mPeriod;
+ private String mBlogID;
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public void setViews(int views) {
+ this.mViews = views;
+ }
+
+ public int getLikes() {
+ return mLikes;
+ }
+
+ public void setLikes(int likes) {
+ this.mLikes = likes;
+ }
+
+ public int getVisitors() {
+ return mVisitors;
+ }
+
+ public void setVisitors(int visitors) {
+ this.mVisitors = visitors;
+ }
+
+ public int getComments() {
+ return mComments;
+ }
+
+ public void setComments(int comments) {
+ this.mComments = comments;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java
new file mode 100644
index 000000000..4b96bf3a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java
@@ -0,0 +1,136 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class VisitsModel extends BaseStatsModel {
+ private String mFields; // Holds a JSON Object
+ private String mUnit;
+ private String mDate;
+ private String mBlogID;
+ private List<VisitModel> mVisits;
+
+ public VisitsModel(String blogID, JSONObject response) throws JSONException {
+ this.setBlogID(blogID);
+ this.setDate(response.getString("date"));
+ this.setUnit(response.getString("unit"));
+ this.setFields(response.getJSONArray("fields").toString());
+
+ JSONArray dataJSON;
+ try {
+ dataJSON = response.getJSONArray("data");
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, this.getClass().getName() + " cannot convert the data field to a JSON array", e);
+ dataJSON = new JSONArray();
+ }
+
+ if (dataJSON == null || dataJSON.length() == 0) {
+ mVisits = new ArrayList<>(0);
+ } else {
+ // Read the position/index of each field in the response
+ HashMap<String, Integer> columnsMapping = new HashMap<>(6);
+ final JSONArray fieldsJSON = getFieldsJSON();
+ if (fieldsJSON == null || fieldsJSON.length() == 0) {
+ mVisits = new ArrayList<>(0);
+ } else {
+ try {
+ for (int i = 0; i < fieldsJSON.length(); i++) {
+ final String field = fieldsJSON.getString(i);
+ columnsMapping.put(field, i);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot read the parameter fields from the JSON response." +
+ "Response: " + response.toString(), e);
+ mVisits = new ArrayList<>(0);
+ }
+ }
+
+ int viewsColumnIndex = columnsMapping.get("views");
+ int visitorsColumnIndex = columnsMapping.get("visitors");
+ int likesColumnIndex = columnsMapping.get("likes");
+ int commentsColumnIndex = columnsMapping.get("comments");
+ int periodColumnIndex = columnsMapping.get("period");
+
+ int numPoints = dataJSON.length();
+ mVisits = new ArrayList<>(numPoints);
+
+ for (int i = 0; i < numPoints; i++) {
+ try {
+ JSONArray currentDayData = dataJSON.getJSONArray(i);
+ VisitModel currentVisitModel = new VisitModel();
+ currentVisitModel.setBlogID(getBlogID());
+ currentVisitModel.setPeriod(currentDayData.getString(periodColumnIndex));
+ currentVisitModel.setViews(currentDayData.getInt(viewsColumnIndex));
+ currentVisitModel.setVisitors(currentDayData.getInt(visitorsColumnIndex));
+ currentVisitModel.setComments(currentDayData.getInt(commentsColumnIndex));
+ currentVisitModel.setLikes(currentDayData.getInt(likesColumnIndex));
+ mVisits.add(currentVisitModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot read the Visit item at index " + i
+ + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public List<VisitModel> getVisits() {
+ return mVisits;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ private void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getUnit() {
+ return mUnit;
+ }
+
+ private void setUnit(String unit) {
+ this.mUnit = unit;
+ }
+
+ private JSONArray getFieldsJSON() {
+ JSONArray jArray;
+ try {
+ String categories = StringUtils.unescapeHTML(this.getFields() != null ? this.getFields() : "[]");
+ if (TextUtils.isEmpty(categories)) {
+ jArray = new JSONArray();
+ } else {
+ jArray = new JSONArray(categories);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, this.getClass().getName() + " cannot convert the string to JSON", e);
+ return null;
+ }
+ return jArray;
+ }
+
+ private void setFields(String fields) {
+ this.mFields = fields;
+ }
+
+ private String getFields() {
+ return mFields;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java
new file mode 100644
index 000000000..77e42c01a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java
@@ -0,0 +1,614 @@
+package org.wordpress.android.ui.stats.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import com.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.stats.StatsEvents;
+import org.wordpress.android.ui.stats.StatsTimeframe;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+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.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.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Background service to retrieve Stats.
+ * Parsing of response(s) and submission of new network calls are done by using a ThreadPoolExecutor with a single thread.
+ */
+
+public class StatsService extends Service {
+ public static final String ARG_BLOG_ID = "blog_id";
+ public static final String ARG_PERIOD = "stats_period";
+ public static final String ARG_DATE = "stats_date";
+ public static final String ARG_SECTION = "stats_section";
+ public static final String ARG_MAX_RESULTS = "stats_max_results";
+ public static final String ARG_PAGE_REQUESTED = "stats_page_requested";
+
+ private static final int DEFAULT_NUMBER_OF_RESULTS = 12;
+ // The number of results to return per page for Paged REST endpoints. Numbers larger than 20 will default to 20 on the server.
+ public static final int MAX_RESULTS_REQUESTED_PER_PAGE = 20;
+
+ public enum StatsEndpointsEnum {
+ VISITS,
+ TOP_POSTS,
+ REFERRERS,
+ CLICKS,
+ GEO_VIEWS,
+ AUTHORS,
+ VIDEO_PLAYS,
+ COMMENTS,
+ FOLLOWERS_WPCOM,
+ FOLLOWERS_EMAIL,
+ COMMENT_FOLLOWERS,
+ TAGS_AND_CATEGORIES,
+ PUBLICIZE,
+ SEARCH_TERMS,
+ INSIGHTS_POPULAR,
+ INSIGHTS_ALL_TIME,
+ INSIGHTS_TODAY,
+ INSIGHTS_LATEST_POST_SUMMARY,
+ INSIGHTS_LATEST_POST_VIEWS;
+
+ public String getRestEndpointPath() {
+ switch (this) {
+ case VISITS:
+ return "visits";
+ case TOP_POSTS:
+ return "top-posts";
+ case REFERRERS:
+ return "referrers";
+ case CLICKS:
+ return "clicks";
+ case GEO_VIEWS:
+ return "country-views";
+ case AUTHORS:
+ return "top-authors";
+ case VIDEO_PLAYS:
+ return "video-plays";
+ case COMMENTS:
+ return "comments";
+ case FOLLOWERS_WPCOM:
+ return "followers?type=wpcom";
+ case FOLLOWERS_EMAIL:
+ return "followers?type=email";
+ case COMMENT_FOLLOWERS:
+ return "comment-followers";
+ case TAGS_AND_CATEGORIES:
+ return "tags";
+ case PUBLICIZE:
+ return "publicize";
+ case SEARCH_TERMS:
+ return "search-terms";
+ case INSIGHTS_POPULAR:
+ return "insights";
+ case INSIGHTS_ALL_TIME:
+ return "";
+ case INSIGHTS_TODAY:
+ return "summary";
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ return "posts";
+ case INSIGHTS_LATEST_POST_VIEWS:
+ return "post";
+ default:
+ AppLog.i(T.STATS, "Called an update of Stats of unknown section!?? " + this.name());
+ return "";
+ }
+ }
+
+ public StatsEvents.SectionUpdatedAbstract getEndpointUpdateEvent(final String blogId, final StatsTimeframe timeframe, final String date,
+ final int maxResultsRequested, final int pageRequested, final BaseStatsModel data) {
+ switch (this) {
+ case VISITS:
+ return new StatsEvents.VisitorsAndViewsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (VisitsModel)data);
+ case TOP_POSTS:
+ return new StatsEvents.TopPostsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (TopPostsAndPagesModel)data);
+ case REFERRERS:
+ return new StatsEvents.ReferrersUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (ReferrersModel)data);
+ case CLICKS:
+ return new StatsEvents.ClicksUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (ClicksModel)data);
+ case AUTHORS:
+ return new StatsEvents.AuthorsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (AuthorsModel)data);
+ case GEO_VIEWS:
+ return new StatsEvents.CountriesUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (GeoviewsModel)data);
+ case VIDEO_PLAYS:
+ return new StatsEvents.VideoPlaysUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (VideoPlaysModel)data);
+ case SEARCH_TERMS:
+ return new StatsEvents.SearchTermsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (SearchTermsModel)data);
+ case COMMENTS:
+ return new StatsEvents.CommentsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (CommentsModel)data);
+ case COMMENT_FOLLOWERS:
+ return new StatsEvents.CommentFollowersUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (CommentFollowersModel)data);
+ case TAGS_AND_CATEGORIES:
+ return new StatsEvents.TagsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (TagsContainerModel)data);
+ case PUBLICIZE:
+ return new StatsEvents.PublicizeUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (PublicizeModel)data);
+ case FOLLOWERS_WPCOM:
+ return new StatsEvents.FollowersWPCOMUdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (FollowersModel)data);
+ case FOLLOWERS_EMAIL:
+ return new StatsEvents.FollowersEmailUdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (FollowersModel)data);
+ case INSIGHTS_POPULAR:
+ return new StatsEvents.InsightsPopularUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsPopularModel)data);
+ case INSIGHTS_ALL_TIME:
+ return new StatsEvents.InsightsAllTimeUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsAllTimeModel)data);
+ case INSIGHTS_TODAY:
+ return new StatsEvents.VisitorsAndViewsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (VisitsModel)data);
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ return new StatsEvents.InsightsLatestPostSummaryUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsLatestPostModel)data);
+ case INSIGHTS_LATEST_POST_VIEWS:
+ return new StatsEvents.InsightsLatestPostDetailsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsLatestPostDetailsModel)data);
+ default:
+ AppLog.e(T.STATS, "Can't find an Update Event that match the current endpoint: " + this.name());
+ }
+
+ return null;
+ }
+ }
+
+ private int mServiceStartId;
+ private final LinkedList<Request<JSONObject>> mStatsNetworkRequests = new LinkedList<>();
+ private final ThreadPoolExecutor singleThreadNetworkHandler = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(T.STATS, "service created");
+ }
+
+ @Override
+ public void onDestroy() {
+ stopRefresh();
+ AppLog.i(T.STATS, "service destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ AppLog.e(T.STATS, "StatsService was killed and restarted with a null intent.");
+ // if this service's process is killed while it is started (after returning from onStartCommand(Intent, int, int)),
+ // then leave it in the started state but don't retain this delivered intent.
+ // Later the system will try to re-create the service.
+ // Because it is in the started state, it will guarantee to call onStartCommand(Intent, int, int) after creating the new service instance;
+ // if there are not any pending start commands to be delivered to the service, it will be called with a null intent object.
+ stopRefresh();
+ return START_NOT_STICKY;
+ }
+
+ final String blogId = intent.getStringExtra(ARG_BLOG_ID);
+ if (TextUtils.isEmpty(blogId)) {
+ AppLog.e(T.STATS, "StatsService was started with a blank blog_id ");
+ return START_NOT_STICKY;
+ }
+
+ final StatsTimeframe period;
+ if (intent.hasExtra(ARG_PERIOD)) {
+ period = (StatsTimeframe) intent.getSerializableExtra(ARG_PERIOD);
+ } else {
+ period = StatsTimeframe.DAY;
+ }
+
+ final String requestedDate;
+ if (intent.getStringExtra(ARG_DATE) == null) {
+ AppLog.w(T.STATS, "StatsService is started with a NULL date on this blogID - "
+ + blogId + ". Using current date!!!");
+ int parsedBlogID = Integer.parseInt(blogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ requestedDate = StatsUtils.getCurrentDateTZ(localTableBlogId);
+ } else {
+ requestedDate = intent.getStringExtra(ARG_DATE);
+ }
+
+ final int maxResultsRequested = intent.getIntExtra(ARG_MAX_RESULTS, DEFAULT_NUMBER_OF_RESULTS);
+ final int pageRequested = intent.getIntExtra(ARG_PAGE_REQUESTED, -1);
+
+ int[] sectionFromIntent = intent.getIntArrayExtra(ARG_SECTION);
+
+ this.mServiceStartId = startId;
+ for (int i=0; i < sectionFromIntent.length; i++){
+ final StatsEndpointsEnum currentSectionsToUpdate = StatsEndpointsEnum.values()[sectionFromIntent[i]];
+ singleThreadNetworkHandler.submit(new Thread() {
+ @Override
+ public void run() {
+ startTasks(blogId, period, requestedDate, currentSectionsToUpdate, maxResultsRequested, pageRequested);
+ }
+ });
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void stopRefresh() {
+ synchronized (mStatsNetworkRequests) {
+ this.mServiceStartId = 0;
+ for (Request<JSONObject> req : mStatsNetworkRequests) {
+ if (req != null && !req.hasHadResponseDelivered() && !req.isCanceled()) {
+ req.cancel();
+ }
+ }
+ mStatsNetworkRequests.clear();
+ }
+ }
+
+ // A fast way to disable caching during develop or when we want to disable it
+ // under some circumstances. Always true for now.
+ private boolean isCacheEnabled() {
+ return true;
+ }
+
+ // Check if we already have Stats
+ private String getCachedStats(final String blogId, final StatsTimeframe timeframe, final String date, final StatsEndpointsEnum sectionToUpdate,
+ final int maxResultsRequested, final int pageRequested) {
+ if (!isCacheEnabled()) {
+ return null;
+ }
+
+ int parsedBlogID = Integer.parseInt(blogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ return StatsTable.getStats(this, localTableBlogId, timeframe, date, sectionToUpdate, maxResultsRequested, pageRequested);
+ }
+
+ private void startTasks(final String blogId, final StatsTimeframe timeframe, final String date, final StatsEndpointsEnum sectionToUpdate,
+ final int maxResultsRequested, final int pageRequested) {
+
+ EventBus.getDefault().post(new StatsEvents.UpdateStatusChanged(true));
+
+ String cachedStats = getCachedStats(blogId, timeframe, date, sectionToUpdate, maxResultsRequested, pageRequested);
+ if (cachedStats != null) {
+ BaseStatsModel mResponseObjectModel;
+ try {
+ JSONObject response = new JSONObject(cachedStats);
+ mResponseObjectModel = StatsUtils.parseResponse(sectionToUpdate, blogId, response);
+
+ EventBus.getDefault().post(
+ sectionToUpdate.getEndpointUpdateEvent(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, mResponseObjectModel)
+ );
+
+ updateWidgetsUI(blogId, sectionToUpdate, timeframe, date, pageRequested, mResponseObjectModel);
+ checkAllRequestsFinished(null);
+ return;
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtilsV1_1();
+
+ String period = timeframe.getLabelForRestCall();
+ /*AppLog.i(T.STATS, "A new Stats network request is required for blogID: " + blogId + " - period: " + period
+ + " - date: " + date + " - StatsType: " + sectionToUpdate.name());
+*/
+
+
+ RestListener vListener = new RestListener(sectionToUpdate, blogId, timeframe, date, maxResultsRequested, pageRequested);
+
+ final String periodDateMaxPlaceholder = "?period=%s&date=%s&max=%s";
+
+ String path = String.format("/sites/%s/stats/" + sectionToUpdate.getRestEndpointPath(), blogId);
+ synchronized (mStatsNetworkRequests) {
+ switch (sectionToUpdate) {
+ case VISITS:
+ path = String.format(path + "?unit=%s&quantity=15&date=%s", period, date);
+ break;
+ case TOP_POSTS:
+ case REFERRERS:
+ case CLICKS:
+ case GEO_VIEWS:
+ case AUTHORS:
+ case VIDEO_PLAYS:
+ case SEARCH_TERMS:
+ path = String.format(path + periodDateMaxPlaceholder, period, date, maxResultsRequested);
+ break;
+ case TAGS_AND_CATEGORIES:
+ case PUBLICIZE:
+ path = String.format(path + "?max=%s", maxResultsRequested);
+ break;
+ case COMMENTS:
+ // No parameters
+ break;
+ case FOLLOWERS_WPCOM:
+ if (pageRequested < 1) {
+ path = String.format(path + "&max=%s", maxResultsRequested);
+ } else {
+ path = String.format(path + "&period=%s&date=%s&max=%s&page=%s",
+ period, date, maxResultsRequested, pageRequested);
+ }
+ break;
+ case FOLLOWERS_EMAIL:
+ if (pageRequested < 1) {
+ path = String.format(path + "&max=%s", maxResultsRequested);
+ } else {
+ path = String.format(path + "&period=%s&date=%s&max=%s&page=%s",
+ period, date, maxResultsRequested, pageRequested);
+ }
+ break;
+ case COMMENT_FOLLOWERS:
+ if (pageRequested < 1) {
+ path = String.format(path + "?max=%s", maxResultsRequested);
+ } else {
+ path = String.format(path + "?period=%s&date=%s&max=%s&page=%s", period,
+ date, maxResultsRequested, pageRequested);
+ }
+ break;
+ case INSIGHTS_ALL_TIME:
+ case INSIGHTS_POPULAR:
+ break;
+ case INSIGHTS_TODAY:
+ path = String.format(path + "?period=day&date=%s", date);
+ break;
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ // This is an edge cases since we're not loading stats but posts
+ path = String.format("/sites/%s/%s", blogId, sectionToUpdate.getRestEndpointPath()
+ + "?order_by=date&number=1&type=post&fields=ID,title,URL,discussion,like_count,date");
+ break;
+ case INSIGHTS_LATEST_POST_VIEWS:
+ // This is a kind of edge case, since we used the pageRequested parameter to request a single postID
+ path = String.format(path + "/%s?fields=views", pageRequested);
+ break;
+ default:
+ AppLog.i(T.STATS, "Called an update of Stats of unknown section!?? " + sectionToUpdate.name());
+ return;
+ }
+
+ // We need to check if we already have the same request in the queue
+ if (checkIfRequestShouldBeEnqueued(restClientUtils, path)) {
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following Stats request " + path);
+ Request<JSONObject> currentRequest = restClientUtils.get(path, vListener, vListener);
+ vListener.currentRequest = currentRequest;
+ currentRequest.setTag("StatsCall");
+ mStatsNetworkRequests.add(currentRequest);
+ } else {
+ AppLog.d(AppLog.T.STATS, "Stats request is already in the queue:" + path);
+ }
+ }
+ }
+
+ /**
+ * This method checks if we already have the same request in the Queue. No need to re-enqueue a new request
+ * if one with the same parameters is there.
+ *
+ * This method is a kind of tricky, since it does the comparison by checking the origin URL of requests.
+ * To do that we had to get the fullURL of the new request by calling a method of the REST client `getAbsoluteURL`.
+ * That's good for now, but could lead to errors if the RestClient changes the way the URL is constructed internally,
+ * by calling `getAbsoluteURL`.
+ *
+ * - Another approach would involve the get of the requests ErrorListener and the check Listener's parameters.
+ * - Cleanest approach is for sure to create a new class that extends Request<JSONObject> and stores parameters for later comparison,
+ * unfortunately we have to change the REST Client and RestClientUtils a lot if we want follow this way...
+ *
+ */
+ private boolean checkIfRequestShouldBeEnqueued(final RestClientUtils restClientUtils, String path) {
+ String absoluteRequestPath = restClientUtils.getRestClient().getAbsoluteURL(path);
+ Iterator<Request<JSONObject>> it = mStatsNetworkRequests.iterator();
+ while (it.hasNext()) {
+ Request<JSONObject> req = it.next();
+ if (!req.hasHadResponseDelivered() && !req.isCanceled() &&
+ absoluteRequestPath.equals(req.getOriginUrl())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Call an updates on the installed widgets if the blog is the primary, the endpoint is Visits
+ // the timeframe is DAY or INSIGHTS, and the date = TODAY
+ private void updateWidgetsUI(String blogId, final StatsEndpointsEnum endpointName,
+ StatsTimeframe timeframe, String date, int pageRequested,
+ Serializable responseObjectModel) {
+ if (pageRequested != -1) {
+ return;
+ }
+ if (endpointName != StatsEndpointsEnum.VISITS) {
+ return;
+ }
+ if (timeframe != StatsTimeframe.DAY && timeframe != StatsTimeframe.INSIGHTS) {
+ return;
+ }
+
+ int parsedBlogID = Integer.parseInt(blogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ // make sure the data is for the current date
+ if (!date.equals(StatsUtils.getCurrentDateTZ(localTableBlogId))) {
+ return;
+ }
+
+ if (responseObjectModel == null) {
+ // TODO What we want to do here?
+ return;
+ }
+
+ if (!StatsWidgetProvider.isBlogDisplayedInWidget(parsedBlogID)) {
+ AppLog.d(AppLog.T.STATS, "The blog with remoteID " + parsedBlogID + " is NOT displayed in any widget. Stats Service doesn't call an update of the widget.");
+ return;
+ }
+
+ if (responseObjectModel instanceof VisitsModel) {
+ VisitsModel visitsModel = (VisitsModel) responseObjectModel;
+ if (visitsModel.getVisits() == null || visitsModel.getVisits().size() == 0) {
+ return;
+ }
+ List<VisitModel> visits = visitsModel.getVisits();
+ VisitModel data = visits.get(visits.size() - 1);
+ StatsWidgetProvider.updateWidgets(getApplicationContext(), parsedBlogID, data);
+ } else if (responseObjectModel instanceof VolleyError) {
+ VolleyError error = (VolleyError) responseObjectModel;
+ StatsWidgetProvider.updateWidgets(getApplicationContext(), parsedBlogID, error);
+ } else if (responseObjectModel instanceof StatsError) {
+ StatsError statsError = (StatsError) responseObjectModel;
+ StatsWidgetProvider.updateWidgets(getApplicationContext(), parsedBlogID, statsError);
+ }
+ }
+
+ private class RestListener implements RestRequest.Listener, RestRequest.ErrorListener {
+ final String mRequestBlogId;
+ private final StatsTimeframe mTimeframe;
+ final StatsEndpointsEnum mEndpointName;
+ private final String mDate;
+ private Request<JSONObject> currentRequest;
+ private final int mMaxResultsRequested, mPageRequested;
+
+ public RestListener(StatsEndpointsEnum endpointName, String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested) {
+ mRequestBlogId = blogId;
+ mTimeframe = timeframe;
+ mEndpointName = endpointName;
+ mDate = date;
+ mMaxResultsRequested = maxResultsRequested;
+ mPageRequested = pageRequested;
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ singleThreadNetworkHandler.submit(new Thread() {
+ @Override
+ public void run() {
+ // do other stuff here
+ BaseStatsModel mResponseObjectModel = null;
+ if (response != null) {
+ try {
+ //AppLog.d(T.STATS, response.toString());
+ mResponseObjectModel = StatsUtils.parseResponse(mEndpointName, mRequestBlogId, response);
+ if (isCacheEnabled()) {
+ int parsedBlogID = Integer.parseInt(mRequestBlogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ StatsTable.insertStats(StatsService.this, localTableBlogId, mTimeframe, mDate, mEndpointName,
+ mMaxResultsRequested, mPageRequested,
+ response.toString(), System.currentTimeMillis());
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ EventBus.getDefault().post(
+ mEndpointName.getEndpointUpdateEvent(mRequestBlogId, mTimeframe, mDate,
+ mMaxResultsRequested, mPageRequested, mResponseObjectModel)
+ );
+
+ updateWidgetsUI(mRequestBlogId, mEndpointName, mTimeframe, mDate, mPageRequested, mResponseObjectModel);
+ checkAllRequestsFinished(currentRequest);
+ }
+ });
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ singleThreadNetworkHandler.submit(new Thread() {
+ @Override
+ public void run() {
+ AppLog.e(T.STATS, "Error while loading Stats!");
+ StatsUtils.logVolleyErrorDetails(volleyError);
+ BaseStatsModel mResponseObjectModel = null;
+ // Check here if this is an authentication error
+ // .com authentication errors are handled automatically by the app
+ if (volleyError instanceof com.android.volley.AuthFailureError) {
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(
+ Integer.parseInt(mRequestBlogId)
+ );
+ Blog blog = WordPress.wpDB.instantiateBlogByLocalId(localId);
+ if (blog != null && blog.isJetpackPowered()) {
+ // It's a kind of edge case, but the Jetpack site could have REST Disabled
+ // In that case (only used in insights for now) shows the error in the module that use the REST API
+ if (!StatsUtils.isRESTDisabledError(volleyError)) {
+ EventBus.getDefault().post(new StatsEvents.JetpackAuthError(localId));
+ }
+ }
+ }
+
+
+ EventBus.getDefault().post(new StatsEvents.SectionUpdateError(mEndpointName, mRequestBlogId, mTimeframe, mDate,
+ mMaxResultsRequested, mPageRequested, volleyError));
+
+ updateWidgetsUI(mRequestBlogId, mEndpointName, mTimeframe, mDate, mPageRequested, mResponseObjectModel);
+ checkAllRequestsFinished(currentRequest);
+ }
+ });
+ }
+ }
+
+ private void stopService() {
+ /* Stop the service if this is the current response, or mServiceBlogId is null
+ String currentServiceBlogId = getServiceBlogId();
+ if (currentServiceBlogId == null || currentServiceBlogId.equals(mRequestBlogId)) {
+ stopService();
+ }*/
+ EventBus.getDefault().post(new StatsEvents.UpdateStatusChanged(false));
+ stopSelf(mServiceStartId);
+ }
+
+
+ private void checkAllRequestsFinished(Request<JSONObject> req) {
+ synchronized (mStatsNetworkRequests) {
+ if (req != null) {
+ mStatsNetworkRequests.remove(req);
+ }
+ boolean isStillWorking = mStatsNetworkRequests.size() > 0 || singleThreadNetworkHandler.getQueue().size() > 0;
+ EventBus.getDefault().post(new StatsEvents.UpdateStatusChanged(isStillWorking));
+ }
+ }
+} \ No newline at end of file