package org.wordpress.android.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.text.Html;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import org.json.JSONException;
import org.json.JSONObject;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsMetadata;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.analytics.AnalyticsTrackerMixpanel;
import org.wordpress.android.analytics.AnalyticsTrackerNosara;
import org.wordpress.android.datasets.ReaderPostTable;
import org.wordpress.android.fluxc.model.SiteModel;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.fluxc.store.SiteStore;
import org.wordpress.android.models.ReaderPost;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_COMMENTED_ON;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_LIKED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_OPENED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_GLOBAL_RELATED_POST_CLICKED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_LOCAL_RELATED_POST_CLICKED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_SEARCH_RESULT_TAPPED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.TRAIN_TRACKS_INTERACT;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.TRAIN_TRACKS_RENDER;
public class AnalyticsUtils {
private static String BLOG_ID_KEY = "blog_id";
private static String POST_ID_KEY = "post_id";
private static String COMMENT_ID_KEY = "comment_id";
private static String FEED_ID_KEY = "feed_id";
private static String FEED_ITEM_ID_KEY = "feed_item_id";
private static String IS_JETPACK_KEY = "is_jetpack";
private static String INTENT_ACTION = "intent_action";
private static String INTENT_DATA = "intent_data";
private static String INTERCEPTED_URI = "intercepted_uri";
private static String INTERCEPTOR_CLASSNAME = "interceptor_classname";
/**
* Utility methods to refresh Mixpanel metadata.
*/
public static void refreshMetadata(AccountStore accountStore, SiteStore siteStore) {
AnalyticsMetadata metadata = new AnalyticsMetadata();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
metadata.setSessionCount(preferences.getInt(AnalyticsTrackerMixpanel.SESSION_COUNT, 0));
metadata.setUserConnected(FluxCUtils.isSignedInWPComOrHasWPOrgSite(accountStore, siteStore));
metadata.setWordPressComUser(accountStore.hasAccessToken());
metadata.setJetpackUser(isJetpackUser(siteStore));
metadata.setNumBlogs(siteStore.getSitesCount());
metadata.setUsername(accountStore.getAccount().getUserName());
metadata.setEmail(accountStore.getAccount().getEmail());
AnalyticsTracker.refreshMetadata(metadata);
}
/***
* @return true if the siteStore has sites accessed via the WPCom Rest API that are not WPCom sites. This only
* counts Jetpack sites connected via WPCom Rest API. If there are Jetpack sites in the site store and they're
* all accessed via XMLRPC, this method returns false.
*/
private static boolean isJetpackUser(SiteStore siteStore) {
return siteStore.getSitesAccessedViaWPComRestCount() - siteStore.getWPComSitesCount() > 0;
}
public static void refreshMetadataNewUser(String username, String email) {
AnalyticsMetadata metadata = new AnalyticsMetadata();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
metadata.setSessionCount(preferences.getInt(AnalyticsTrackerMixpanel.SESSION_COUNT, 0));
metadata.setUserConnected(true);
metadata.setWordPressComUser(true);
metadata.setJetpackUser(false);
metadata.setNumBlogs(1);
metadata.setUsername(username);
metadata.setEmail(email);
AnalyticsTracker.refreshMetadata(metadata);
}
public static int getWordCount(String content) {
String text = Html.fromHtml(content.replaceAll("<img[^>]*>", "")).toString();
return text.split("\\s+").length;
}
/**
* Bump Analytics for the passed Stat and add blog details into properties.
*
* @param stat The Stat to bump
* @param site The site object
*
*/
public static void trackWithSiteDetails(AnalyticsTracker.Stat stat, SiteModel site) {
trackWithSiteDetails(stat, site, null);
}
/**
* Bump Analytics for the passed Stat and add blog details into properties.
*
* @param stat The Stat to bump
* @param site The site object
* @param properties Properties to attach to the event
*/
public static void trackWithSiteDetails(AnalyticsTracker.Stat stat, SiteModel site,
Map<String, Object> properties) {
if (site == null || !SiteUtils.isAccessedViaWPComRest(site)) {
AppLog.w(AppLog.T.STATS, "The passed blog obj is null or it's not a wpcom or Jetpack. Tracking analytics without blog info");
AnalyticsTracker.track(stat, properties);
return;
}
if (SiteUtils.isAccessedViaWPComRest(site)) {
if (properties == null) {
properties = new HashMap<>();
}
properties.put(BLOG_ID_KEY, site.getSiteId());
properties.put(IS_JETPACK_KEY, site.isJetpackConnected());
}
if (properties == null) {
AnalyticsTracker.track(stat);
} else {
AnalyticsTracker.track(stat, properties);
}
}
/**
* Bump Analytics and add blog_id into properties
*
* @param stat The Stat to bump
* @param blogID The REMOTE blog ID.
*/
public static void trackWithSiteId(AnalyticsTracker.Stat stat, long blogID) {
Map<String, Object> properties = new HashMap<>();
if (blogID != 0) {
properties.put(BLOG_ID_KEY, blogID);
}
AnalyticsTracker.track(stat, properties);
}
/**
* Bump Analytics for a reader post
*
* @param stat The Stat to bump
* @param post The reader post to track
*/
public static void trackWithReaderPostDetails(AnalyticsTracker.Stat stat, ReaderPost post) {
if (post == null) return;
// wpcom/jetpack posts should pass: feed_id, feed_item_id, blog_id, post_id, is_jetpack
// RSS pass should pass: feed_id, feed_item_id, is_jetpack
Map<String, Object> properties = new HashMap<>();
if (post.isWP() || post.isJetpack) {
properties.put(BLOG_ID_KEY, post.blogId);
properties.put(POST_ID_KEY, post.postId);
}
properties.put(FEED_ID_KEY, post.feedId);
properties.put(FEED_ITEM_ID_KEY, post.feedItemId);
properties.put(IS_JETPACK_KEY, post.isJetpack);
AnalyticsTracker.track(stat, properties);
// record a railcar interact event if the post has a railcar and this can be tracked
// as an interaction
if (canTrackRailcarInteraction(stat) && post.hasRailcar()) {
trackRailcarInteraction(stat, post.getRailcarJson());
}
}
/**
* Track when a railcar item has been rendered
*
* @param railcarJson The JSON string of the railcar
*
*/
public static void trackWithReaderPostDetails(AnalyticsTracker.Stat stat, long blogId, long postId) {
trackWithReaderPostDetails(stat, ReaderPostTable.getBlogPost(blogId, postId, true));
}
public static void trackWithBlogPostDetails(AnalyticsTracker.Stat stat, long blogId, long postId) {
Map<String, Object> properties = new HashMap<>();
properties.put(BLOG_ID_KEY, blogId);
properties.put(POST_ID_KEY, postId);
AnalyticsTracker.track(stat, properties);
}
public static void trackWithBlogPostDetails(AnalyticsTracker.Stat stat, String blogId, String postId, int
commentId) {
Map<String, Object> properties = new HashMap<>();
properties.put(BLOG_ID_KEY, blogId);
properties.put(POST_ID_KEY, postId);
properties.put(COMMENT_ID_KEY, commentId);
AnalyticsTracker.track(stat, properties);
}
public static void trackWithFeedPostDetails(AnalyticsTracker.Stat stat, long feedId, long feedItemId) {
Map<String, Object> properties = new HashMap<>();
properties.put(FEED_ID_KEY, feedId);
properties.put(FEED_ITEM_ID_KEY, feedItemId);
AnalyticsTracker.track(stat, properties);
}
/**
* Track when app launched via deep-linking
*
* @param stat The Stat to bump
* @param action The Intent action the app was started with
* @param data The data URI the app was started with
*/
public static void trackWithDeepLinkData(AnalyticsTracker.Stat stat, String action, Uri data) {
Map<String, Object> properties = new HashMap<>();
properties.put(INTENT_ACTION, action);
properties.put(INTENT_DATA, data != null ? data.toString() : null);
AnalyticsTracker.track(stat, properties);
}
/**
* Track when app launched via deep-linking but then fell back to external browser
*
* @param stat The Stat to bump
* @param interceptedUri The fallback URI the app was started with
*/
public static void trackWithInterceptedUri(AnalyticsTracker.Stat stat, String interceptedUri) {
Map<String, Object> properties = new HashMap<>();
properties.put(INTERCEPTED_URI, interceptedUri);
AnalyticsTracker.track(stat, properties);
}
/**
* Track when app launched via deep-linking but then fell back to external browser
*
* @param stat The Stat to bump
* @param interceptorClassname The name of the class that handles the intercept by default
*/
public static void trackWithDefaultInterceptor(AnalyticsTracker.Stat stat, String interceptorClassname) {
Map<String, Object> properties = new HashMap<>();
properties.put(INTERCEPTOR_CLASSNAME, interceptorClassname);
AnalyticsTracker.track(stat, properties);
}
/**
* Track when a railcar item has been rendered
*
* @param railcarJson The JSON string of the railcar
*
*/
public static void trackRailcarRender(String railcarJson) {
if (TextUtils.isEmpty(railcarJson)) {
return;
}
AnalyticsTracker.track(TRAIN_TRACKS_RENDER, railcarJsonToProperties(railcarJson));
}
/**
* Track when a railcar item has been interacted with
*
* @param stat The event that caused the interaction
* @param railcarJson The JSON string of the railcar
*
*/
private static void trackRailcarInteraction(AnalyticsTracker.Stat stat, String railcarJson) {
if (TextUtils.isEmpty(railcarJson)) return;
Map<String, Object> properties = railcarJsonToProperties(railcarJson);
properties.put("action", AnalyticsTrackerNosara.getEventNameForStat(stat));
AnalyticsTracker.track(TRAIN_TRACKS_INTERACT, properties);
}
/**
* @param stat The event that would cause the interaction
* @return True if the passed stat event can be recorded as a railcar interaction
*/
private static boolean canTrackRailcarInteraction(AnalyticsTracker.Stat stat) {
return stat == READER_ARTICLE_LIKED
|| stat == READER_ARTICLE_OPENED
|| stat == READER_SEARCH_RESULT_TAPPED
|| stat == READER_ARTICLE_COMMENTED_ON
|| stat == READER_GLOBAL_RELATED_POST_CLICKED
|| stat == READER_LOCAL_RELATED_POST_CLICKED;
}
/*
* Converts the JSON string of a railcar to a properties list using the existing json key names
*/
private static Map<String, Object> railcarJsonToProperties(@NonNull String railcarJson) {
Map<String, Object> properties = new HashMap<>();
try {
JSONObject jsonRailcar = new JSONObject(railcarJson);
Iterator<String> iter = jsonRailcar.keys();
while (iter.hasNext()) {
String key = iter.next();
Object value = jsonRailcar.get(key);
properties.put(key, value);
}
} catch (JSONException e) {
AppLog.e(AppLog.T.READER, e);
}
return properties;
}
public static Map<String, Object> getMediaProperties(Context context, boolean isVideo, Uri mediaURI, String path) {
Map<String, Object> properties = new HashMap<>();
if(context == null) {
AppLog.e(AppLog.T.MEDIA, "In order to track media properties Context cannot be null.");
return properties;
}
if(mediaURI == null && TextUtils.isEmpty(path)) {
AppLog.e(AppLog.T.MEDIA, "In order to track media properties mediaURI and path cannot be both null!!");
return properties;
}
if(mediaURI != null) {
path = MediaUtils.getRealPathFromURI(context, mediaURI);
}
if (TextUtils.isEmpty(path) ) {
return properties;
}
// File not found
File file = new File(path);
try {
if (!file.exists()) {
AppLog.e(AppLog.T.MEDIA, "Can't access the media file. It doesn't exists anymore!! Properties are not being tracked.");
return properties;
}
} catch (SecurityException e) {
AppLog.e(AppLog.T.MEDIA, "Can't access the media file. Properties are not being tracked.", e);
return properties;
}
String mimeType = MediaUtils.getMediaFileMimeType(file);
properties.put("mime", mimeType);
String fileName = MediaUtils.getMediaFileName(file, mimeType);
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileName).toLowerCase();
properties.put("ext", fileExtension);
if (!isVideo) {
int[] dimensions = ImageUtils.getImageSize(Uri.fromFile(file), context);
double megapixels = dimensions[0] * dimensions[1];
megapixels = megapixels / 1000000;
megapixels = Math.floor(megapixels);
properties.put("megapixels", (int) megapixels);
} else {
long videoDurationMS = MediaUtils.getVideoDurationMS(context, file);
properties.put("duration_secs", (int)videoDurationMS/1000);
}
properties.put("bytes", file.length());
return properties;
}
}