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.lang3.ArrayUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.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.fluxc.model.SiteModel;
import org.wordpress.android.fluxc.store.SiteStore;
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.AppLog.T;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.SiteUtils;
import java.util.ArrayList;
import javax.inject.Inject;
public class StatsWidgetProvider extends AppWidgetProvider {
@Inject SiteStore mSiteStore;
@Override
public void onReceive(Context context, Intent intent) {
((WordPress) context.getApplicationContext()).component().inject(this);
super.onReceive(context, intent);
}
private static void showMessage(Context context, int[] allWidgets, String message, SiteStore siteStore) {
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);
SiteModel site = siteStore.getSiteBySiteId(remoteBlogID);
String name;
if (site != null) {
name = context.getString(R.string.stats_widget_name_for_blog);
name = String.format(name, StringEscapeUtils.unescapeHtml4(SiteUtils.getSiteNameOrHomeURL(site)));
} 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, SiteModel site, 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.unescapeHtml4(SiteUtils.getSiteNameOrHomeURL(site)));
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(WordPress.SITE, site);
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, site.getId(), intent, PendingIntent
.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.stats_widget_outer_container, pendingIntent);
appWidgetManager.updateAppWidget(widgetId, remoteViews);
}
}
private static void ShowCacheIfAvailableOrGenericError(Context context, SiteModel site, SiteStore siteStore) {
if (site == null) {
AppLog.e(T.STATS, "Invalid site.");
return;
}
int[] widgetIDs = getWidgetIDsFromRemoteBlogID(site.getSiteId());
if (widgetIDs.length == 0) {
return;
}
String currentDate = StatsUtils.getCurrentDateTZ(site);
// Show cached data if available
JSONObject cache = getCacheDataForBlog(site.getSiteId(), currentDate);
if (cache != null) {
showStatsData(context, widgetIDs, site, cache);
} else {
showMessage(context, widgetIDs, context.getString(R.string.stats_widget_error_generic), siteStore);
}
}
// This is called by the Stats service in case of error
public static void updateWidgets(Context context, SiteModel site, SiteStore siteStore, VolleyError error) {
if (error == null) {
AppLog.e(AppLog.T.STATS, "Widget received a VolleyError that is null!");
return;
}
if (site == null) {
return;
}
// If it's an auth error, show it in the widget UI
if (error instanceof com.android.volley.AuthFailureError) {
int[] widgetIDs = getWidgetIDsFromRemoteBlogID(site.getSiteId());
if (widgetIDs.length == 0){
return;
}
// Check if Jetpack or .com
if (SiteUtils.isAccessedViaWPComRest(site)) {
// User cannot access stats for this .com blog
showMessage(context, widgetIDs, context.getString(R.string.stats_widget_error_no_permissions),
siteStore);
} 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),
siteStore);
}
return;
}
ShowCacheIfAvailableOrGenericError(context, site, siteStore);
}
// This is called by the Stats service in case of error
public static void updateWidgets(Context context, SiteModel site, SiteStore siteStore, StatsError error) {
if (error == null) {
AppLog.e(AppLog.T.STATS, "Widget received a StatsError that is null!");
return;
}
ShowCacheIfAvailableOrGenericError(context, site, siteStore);
}
// This is called by the Stats service to keep widgets updated
public static void updateWidgets(Context context, SiteModel site, VisitModel data) {
if (site == null) {
AppLog.e(AppLog.T.STATS, "No blog found in the db!");
return;
}
AppLog.d(AppLog.T.STATS, "updateWidgets called for the blogID " + site.getSiteId());
int[] widgetIDs = getWidgetIDsFromRemoteBlogID(site.getSiteId());
if (widgetIDs.length == 0) {
return;
}
try {
String currentDate = StatsUtils.getCurrentDateTZ(site);
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(String.valueOf(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, site, 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, mSiteStore);
}
/**
* 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, 0);
}
public static void enqueueStatsRequestForBlog(Context context, long 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(long 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(long 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(long 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, long 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, SiteStore siteStore) {
AppLog.d(AppLog.T.STATS, "setupNewWidget called");
SiteModel site = siteStore.getSiteByLocalId(localBlogID);
if (site == 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), siteStore);
AppLog.e(AppLog.T.STATS, "setupNewWidget: No blog found in the db!");
return;
}
// At this point the remote ID cannot be null.
long remoteBlogID = site.getSiteId();
// Add the following check just to be safe
if (remoteBlogID == 0) {
showMessage(context, new int[]{widgetID},
context.getString(R.string.stats_widget_error_readd_widget), siteStore);
return;
}
AnalyticsUtils.trackWithSiteDetails(AnalyticsTracker.Stat.STATS_WIDGET_ADDED, site);
AnalyticsTracker.flush();
// Store the association between the widget ID and the remote blog id into prefs.
setRemoteBlogIDForWidgetIDs(new int[] {widgetID}, site.getSiteId());
String currentDate = StatsUtils.getCurrentDateTZ(site);
// Load cached data if available and show it immediately
JSONObject cache = getCacheDataForBlog(remoteBlogID, currentDate);
if (cache != null) {
showStatsData(context, new int[] {widgetID}, site, cache);
return;
}
if (!NetworkUtils.isNetworkAvailable(context)) {
showMessage(context, new int[] {widgetID}, context.getString(R.string.no_network_title), siteStore);
} else {
showMessage(context, new int[] {widgetID}, context.getString(R.string.stats_widget_loading_data), siteStore);
enqueueStatsRequestForBlog(context, remoteBlogID, currentDate);
}
}
private static void refreshWidgets(Context context, int[] appWidgetIds, SiteStore siteStore) {
// TODO: FluxC: This file must be refactored, we probably want a "WidgetManager" and keep the bare minimum
// here in the AppWidgetProvider.
// if (!mAccountStore.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), siteStore);
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()]));
SiteModel site = siteStore.getSiteBySiteId(remoteBlogID);
if (site == null) {
// No site in the app
showMessage(context, currentWidgets, context.getString(R.string.stats_widget_error_readd_widget), siteStore);
continue;
}
String currentDate = StatsUtils.getCurrentDateTZ(site);
// Load cached data if available and show it immediately
JSONObject cache = getCacheDataForBlog(remoteBlogID, currentDate);
if (cache != null) {
showStatsData(context, currentWidgets, site, 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), siteStore);
}
} else {
if (cache == null) {
showMessage(context, currentWidgets, context.getString(R.string.stats_widget_loading_data), siteStore);
}
// Make sure to refresh widget data now.
enqueueStatsRequestForBlog(context, remoteBlogID, currentDate);
}
}
}
public static void refreshAllWidgets(Context context, SiteStore siteStore) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName thisWidget = new ComponentName(context, StatsWidgetProvider.class);
int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
refreshWidgets(context, allWidgetIds, siteStore);
}
}