package com.mixpanel.android.mpmetrics;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Point;
import android.os.Build;
import android.view.Display;
import android.view.WindowManager;
import com.mixpanel.android.util.ImageStore;
import com.mixpanel.android.util.MPLog;
import com.mixpanel.android.util.RemoteService;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.SSLSocketFactory;
/* package */ class DecideChecker {
/* package */ static class Result {
public Result() {
notifications = new ArrayList<>();
eventBindings = EMPTY_JSON_ARRAY;
variants = EMPTY_JSON_ARRAY;
}
public final List<InAppNotification> notifications;
public JSONArray eventBindings;
public JSONArray variants;
}
public DecideChecker(final Context context, final MPConfig config, final SystemInformation systemInformation) {
mContext = context;
mConfig = config;
mChecks = new LinkedList<DecideMessages>();
mImageStore = createImageStore(context);
mSystemInformation = systemInformation;
}
protected ImageStore createImageStore(final Context context) {
return new ImageStore(context, "DecideChecker");
}
public void addDecideCheck(final DecideMessages check) {
mChecks.add(check);
}
public void runDecideChecks(final RemoteService poster) throws RemoteService.ServiceUnavailableException {
final Iterator<DecideMessages> itr = mChecks.iterator();
while (itr.hasNext()) {
final DecideMessages updates = itr.next();
final String distinctId = updates.getDistinctId();
try {
final Result result = runDecideCheck(updates.getToken(), distinctId, poster);
updates.reportResults(result.notifications, result.eventBindings, result.variants);
} catch (final UnintelligibleMessageException e) {
MPLog.e(LOGTAG, e.getMessage(), e);
}
}
}
/* package */ static class UnintelligibleMessageException extends Exception {
private static final long serialVersionUID = -6501269367559104957L;
public UnintelligibleMessageException(String message, JSONException cause) {
super(message, cause);
}
}
private Result runDecideCheck(final String token, final String distinctId, final RemoteService poster)
throws RemoteService.ServiceUnavailableException, UnintelligibleMessageException {
final String responseString = getDecideResponseFromServer(token, distinctId, poster);
MPLog.v(LOGTAG, "Mixpanel decide server response was:\n" + responseString);
Result parsed = new Result();
if (null != responseString) {
parsed = parseDecideResponse(responseString);
}
final Iterator<InAppNotification> notificationIterator = parsed.notifications.iterator();
while (notificationIterator.hasNext()) {
final InAppNotification notification = notificationIterator.next();
final Bitmap image = getNotificationImage(notification, mContext);
if (null == image) {
MPLog.i(LOGTAG, "Could not retrieve image for notification " + notification.getId() +
", will not show the notification.");
notificationIterator.remove();
} else {
notification.setImage(image);
}
}
return parsed;
}// runDecideCheck
/* package */ static Result parseDecideResponse(String responseString)
throws UnintelligibleMessageException {
JSONObject response;
final Result ret = new Result();
try {
response = new JSONObject(responseString);
} catch (final JSONException e) {
final String message = "Mixpanel endpoint returned unparsable result:\n" + responseString;
throw new UnintelligibleMessageException(message, e);
}
JSONArray notifications = null;
if (response.has("notifications")) {
try {
notifications = response.getJSONArray("notifications");
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Mixpanel endpoint returned non-array JSON for notifications: " + response);
}
}
if (null != notifications) {
final int notificationsToRead = Math.min(notifications.length(), MPConfig.MAX_NOTIFICATION_CACHE_COUNT);
for (int i = 0; i < notificationsToRead; i++) {
try {
final JSONObject notificationJson = notifications.getJSONObject(i);
final String notificationType = notificationJson.getString("type");
if (notificationType.equalsIgnoreCase("takeover")) {
final TakeoverInAppNotification notification = new TakeoverInAppNotification(notificationJson);
ret.notifications.add(notification);
} else if (notificationType.equalsIgnoreCase("mini")) {
final MiniInAppNotification notification = new MiniInAppNotification(notificationJson);
ret.notifications.add(notification);
}
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Received a strange response from notifications service: " + notifications.toString(), e);
} catch (final BadDecideObjectException e) {
MPLog.e(LOGTAG, "Received a strange response from notifications service: " + notifications.toString(), e);
} catch (final OutOfMemoryError e) {
MPLog.e(LOGTAG, "Not enough memory to show load notification from package: " + notifications.toString(), e);
}
}
}
if (response.has("event_bindings")) {
try {
ret.eventBindings = response.getJSONArray("event_bindings");
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Mixpanel endpoint returned non-array JSON for event bindings: " + response);
}
}
if (response.has("variants")) {
try {
ret.variants = response.getJSONArray("variants");
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Mixpanel endpoint returned non-array JSON for variants: " + response);
}
}
return ret;
}
private String getDecideResponseFromServer(String unescapedToken, String unescapedDistinctId, RemoteService poster)
throws RemoteService.ServiceUnavailableException {
final String escapedToken;
final String escapedId;
try {
escapedToken = URLEncoder.encode(unescapedToken, "utf-8");
if (null != unescapedDistinctId) {
escapedId = URLEncoder.encode(unescapedDistinctId, "utf-8");
} else {
escapedId = null;
}
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException("Mixpanel library requires utf-8 string encoding to be available", e);
}
final StringBuilder queryBuilder = new StringBuilder()
.append("?version=1&lib=android&token=")
.append(escapedToken);
if (null != escapedId) {
queryBuilder.append("&distinct_id=").append(escapedId);
}
queryBuilder.append("&properties=");
JSONObject properties = new JSONObject();
try {
properties.putOpt("$android_lib_version", MPConfig.VERSION);
properties.putOpt("$android_app_version", mSystemInformation.getAppVersionName());
properties.putOpt("$android_version", Build.VERSION.RELEASE);
properties.putOpt("$android_app_release", mSystemInformation.getAppVersionCode());
properties.putOpt("$android_device_model", Build.MODEL);
queryBuilder.append(URLEncoder.encode(properties.toString(), "utf-8"));
} catch (Exception e) {
MPLog.e(LOGTAG, "Exception constructing properties JSON", e.getCause());
}
final String checkQuery = queryBuilder.toString();
final String[] urls;
if (mConfig.getDisableFallback()) {
urls = new String[]{mConfig.getDecideEndpoint() + checkQuery};
} else {
urls = new String[]{mConfig.getDecideEndpoint() + checkQuery,
mConfig.getDecideFallbackEndpoint() + checkQuery};
}
MPLog.v(LOGTAG, "Querying decide server, urls:");
for (int i = 0; i < urls.length; i++) {
MPLog.v(LOGTAG, " >> " + urls[i]);
}
final byte[] response = getUrls(poster, mContext, urls);
if (null == response) {
return null;
}
try {
return new String(response, "UTF-8");
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException("UTF not supported on this platform?", e);
}
}
private Bitmap getNotificationImage(InAppNotification notification, Context context)
throws RemoteService.ServiceUnavailableException {
String[] urls = {notification.getImage2xUrl(), notification.getImageUrl()};
final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Display display = wm.getDefaultDisplay();
final int displayWidth = getDisplayWidth(display);
if (notification.getType() == InAppNotification.Type.TAKEOVER && displayWidth >= 720) {
urls = new String[]{notification.getImage4xUrl(), notification.getImage2xUrl(), notification.getImageUrl()};
}
for (String url : urls) {
try {
return mImageStore.getImage(url);
} catch (ImageStore.CantGetImageException e) {
MPLog.v(LOGTAG, "Can't load image " + url + " for a notification", e);
}
}
return null;
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private static int getDisplayWidth(final Display display) {
if (Build.VERSION.SDK_INT < 13) {
return display.getWidth();
} else {
final Point displaySize = new Point();
display.getSize(displaySize);
return displaySize.x;
}
}
private static byte[] getUrls(RemoteService poster, Context context, String[] urls)
throws RemoteService.ServiceUnavailableException {
final MPConfig config = MPConfig.getInstance(context);
if (!poster.isOnline(context, config.getOfflineMode())) {
return null;
}
byte[] response = null;
for (String url : urls) {
try {
final SSLSocketFactory socketFactory = config.getSSLSocketFactory();
response = poster.performRequest(url, null, socketFactory);
break;
} catch (final MalformedURLException e) {
MPLog.e(LOGTAG, "Cannot interpret " + url + " as a URL.", e);
} catch (final FileNotFoundException e) {
MPLog.v(LOGTAG, "Cannot get " + url + ", file not found.", e);
} catch (final IOException e) {
MPLog.v(LOGTAG, "Cannot get " + url + ".", e);
} catch (final OutOfMemoryError e) {
MPLog.e(LOGTAG, "Out of memory when getting to " + url + ".", e);
break;
}
}
return response;
}
private final MPConfig mConfig;
private final Context mContext;
private final List<DecideMessages> mChecks;
private final ImageStore mImageStore;
private final SystemInformation mSystemInformation;
private static final JSONArray EMPTY_JSON_ARRAY = new JSONArray();
private static final String LOGTAG = "MixpanelAPI.DChecker";
}