package com.github.andlyticsproject.console.v2;
import android.util.Log;
import com.github.andlyticsproject.console.DevConsoleException;
import com.github.andlyticsproject.model.AppDetails;
import com.github.andlyticsproject.model.AppInfo;
import com.github.andlyticsproject.model.AppStats;
import com.github.andlyticsproject.model.Comment;
import com.github.andlyticsproject.model.RevenueSummary;
import com.github.andlyticsproject.util.FileUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
* This class contains static methods used to parse JSON from {@link DevConsoleV2}
*
* See {@link https://github.com/AndlyticsProject/andlytics/wiki/Developer-Console-v2} for some more
* documentation
*
*/
public class JsonParser {
private static final int REVENUE_LAST_30DAYS = 30;
private static final int REVENUE_LAST_7DAYS = 7;
private static final int REVENUE_LAST_DAY = 1;
private static final int REVENUE_OVERALL = -1;
private static final String TAG = JsonParser.class.getSimpleName();
private static final boolean DEBUG = false;
private JsonParser() {
}
/**
* Parses the supplied JSON string and adds the extracted ratings to the supplied
* {@link AppStats} object
*
* @param json
* @param stats
* @throws JSONException
*/
static void parseRatings(String json, AppStats stats) throws JSONException {
JSONObject data = new JSONObject(json).getJSONObject("result").getJSONArray("1")
.getJSONObject(0);
// Ratings values is in 8
JSONObject values = data.getJSONObject("8");
// Ratings are at index 1 - 5
stats.setRating(values.getInt("1"), values.getInt("2"), values.getInt("3"),
values.getInt("4"), values.getInt("5"));
// Comment count is in 7
stats.setNumberOfComments(data.getInt("7"));
}
/**
* Parses the supplied JSON string and adds the extracted statistics to the supplied
* {@link AppStats} object
* based on the supplied statsType
* Not used at the moment
*
* @param json
* @param stats
* @param statsType
* @throws JSONException
*/
static void parseStatistics(String json, AppStats stats, int statsType) throws JSONException {
// Extract the top level values array
JSONObject values = new JSONObject(json).getJSONObject("result").getJSONObject("1");
/*
* null
* Nested array [null, [null, Array containing historical data]]
* null
* null
* null
* Nested arrays containing summary and historical data broken down by dimension e.g.
* Android version
* null
* null
* App name
*/
// For now we just care about todays value, later we may delve into the historical and
// dimensioned data
JSONArray historicalData = values.getJSONObject("1").getJSONArray("1");
JSONObject latestData = historicalData.getJSONObject(historicalData.length() - 1);
/*
* null
* Date
* [null, value]
*/
int latestValue = latestData.getJSONObject("2").getInt("1");
switch (statsType) {
case DevConsoleV2Protocol.STATS_TYPE_TOTAL_USER_INSTALLS:
stats.setTotalDownloads(latestValue);
break;
case DevConsoleV2Protocol.STATS_TYPE_ACTIVE_DEVICE_INSTALLS:
stats.setActiveInstalls(latestValue);
break;
default:
break;
}
}
/**
* Parses the supplied JSON string and builds a list of apps from it
*
* @param json
* @param accountName
* @param skipIncomplete
* @return List of apps
* @throws JSONException
*/
static List<AppInfo> parseAppInfos(String json, String accountName, boolean skipIncomplete)
throws JSONException {
Date now = new Date();
List<AppInfo> apps = new ArrayList<AppInfo>();
// Extract the base array containing apps
JSONObject result = new JSONObject(json).getJSONObject("result");
if (DEBUG) {
pp("result", result);
}
JSONArray jsonApps = result.optJSONArray("1");
if (DEBUG) {
pp("jsonApps", jsonApps);
}
if (jsonApps == null) {
// no apps yet?
return apps;
}
int numberOfApps = jsonApps.length();
Log.d(TAG, String.format("Found %d apps in JSON", numberOfApps));
for (int i = 0; i < numberOfApps; i++) {
AppInfo app = new AppInfo();
app.setAccount(accountName);
app.setLastUpdate(now);
/*
* Per app key indexed objects:
* "1" -> [ APP_INFO
* * packageName
* * publishState
* * ?
* ]
* "3" -> [ APP_STATS
* * Active installs
* * Total ratings
* * Average rating
* * Errors
* * Total installs
* ]
* "6" -> [ APP_DETAILS
* * App name
* * Low res icon
* * High res icon
* * Version
* * Price
* * ...
* * Last update
* ]
*/
JSONObject jsonApp = jsonApps.getJSONObject(i);
/*
* [ APP_INFO
* * "1" -> packageName
* * "7" -> publishState
* * "11" -> ?
* ]
*/
JSONObject jsonAppInfo = jsonApp.getJSONObject("1");
if (DEBUG) {
pp("jsonAppInfo", jsonAppInfo);
}
String packageName = jsonAppInfo.getString("1");
// Look for "tmp.7238057230750432756094760456.235728507238057230542"
if (packageName == null
|| (packageName.startsWith("tmp.") && Character.isDigit(packageName.charAt(4)))) {
Log.d(TAG, String.format("Skipping draft app %d, package name=%s", i, packageName));
continue;
// Draft app
}
// Check number code and last updated date
// Published: 1
// Unpublished: 2
// Draft: 5
// Draft w/ in-app items?: 6
// TODO figure out the rest and add don't just skip, filter, etc. Cf. #223
int publishState = jsonAppInfo.optInt("7");
Log.d(TAG, String.format("%s: publishState=%d", packageName, publishState));
if (publishState != 1) {
// Not a published app, skipping
Log.d(TAG, String.format(
"Skipping app %d with state != 1: package name=%s: state=%d", i,
packageName, publishState));
continue;
}
app.setPublishState(publishState);
app.setPackageName(packageName);
/* App details
* * App name
* * Low res icon
* * High res icon
* * Version
* * Price?
* * ...
* * Last update
*/
// skip if we can't get all the data
// XXX should we just let this crash so we know there is a problem?
if (!jsonApp.has("6")) {
if (skipIncomplete) {
Log.d(TAG, String.format(
"Skipping app %d because no app details found: package name=%s", i,
packageName));
} else {
Log.d(TAG, "Adding incomplete app: " + packageName);
apps.add(app);
}
continue;
}
JSONObject jsonAppDetails = jsonApp.getJSONObject("6");
if (DEBUG) {
pp("jsonAppDetails", jsonAppDetails);
}
app.setName(jsonAppDetails.getString("1"));
String description = ""; //appDetails.optString("3");
String changelog = ""; //appDetails.optString("5");
Long lastPlayStoreUpdate = jsonAppDetails.getLong("8");
AppDetails details = new AppDetails(description, changelog, lastPlayStoreUpdate);
app.setDetails(details);
app.setVersionName(jsonAppDetails.getString("4"));
if (jsonAppDetails.has("3")) {
app.setIconUrl(jsonAppDetails.getString("3"));
}
// App stats
/*
* Active installs
* Total ratings
* Average rating
* Errors
* Total installs
*/
if (!jsonApp.has("3")) {
if (skipIncomplete) {
Log.d(TAG, String.format(
"Skipping app %d because no app stats found: package name=%s", i,
packageName));
} else {
Log.d(TAG, "Adding incomplete app: " + packageName);
apps.add(app);
}
continue;
}
JSONObject jsonAppStats = jsonApp.getJSONObject("3");
if (DEBUG) {
pp("jsonAppStats", jsonAppStats);
}
AppStats stats = new AppStats();
stats.setDate(now);
if (jsonAppStats.length() < 4) {
// no statistics (yet?) or weird format
// TODO do we need differentiate?
stats.setActiveInstalls(0);
stats.setTotalDownloads(0);
stats.setNumberOfErrors(0);
} else {
stats.setActiveInstalls(jsonAppStats.optInt("1", 0));
stats.setTotalDownloads(jsonAppStats.getInt("5"));
stats.setNumberOfErrors(jsonAppStats.optInt("4"));
}
app.setLatestStats(stats);
apps.add(app);
}
return apps;
}
private static void pp(String name, JSONArray jsonArr) {
try {
String pp = jsonArr == null ? "null" : jsonArr.toString(2);
Log.d(TAG, String.format("%s: %s", name, pp));
FileUtils.writeToDebugDir(name + "-pp.json", pp);
} catch (JSONException e) {
Log.w(TAG, "Error printing JSON: " + e.getMessage(), e);
}
}
private static void pp(String name, JSONObject jsonObj) {
try {
String pp = jsonObj == null ? "null" : jsonObj.toString(2);
Log.d(TAG, String.format("%s: %s", name, pp));
FileUtils.writeToDebugDir(name + "-pp.json", pp);
} catch (JSONException e) {
Log.w(TAG, "Error printing JSON: " + e.getMessage(), e);
}
}
/**
* Parses the supplied JSON string and returns a list of comments.
*
* @param json
* @return
* @throws JSONException
*/
static List<Comment> parseComments(String json) throws JSONException {
List<Comment> comments = new ArrayList<Comment>();
/*
* null
* Array containing arrays of comments
* numberOfComments
*/
JSONArray jsonComments = new JSONObject(json).getJSONObject("result").getJSONArray("1");
int count = jsonComments.length();
for (int i = 0; i < count; i++) {
Comment comment = new Comment();
JSONObject jsonComment = jsonComments.getJSONObject(i);
// TODO These examples are out of date and need updating
/*
* null
* "gaia:17919762185957048423:1:vm:11887109942373535891", -- ID?
* "REVIEWERS_NAME",
* "1343652956570", -- DATE?
* RATING,
* null
* "COMMENT",
* null,
* "VERSION_NAME",
* [ null,
* "DEVICE_CODE_NAME",
* "DEVICE_MANFACTURER",
* "DEVICE_MODEL"
* ],
* "LOCALE",
* null,
* 0
*/
// Example with developer reply
/*
* [
* null,
* "gaia:12824185113034449316:1:vm:18363775304595766012",
* "Micka�l",
* "1350333837326",
* 1,
* "",
* "Nul\tNul!! N'arrive pas a scanner le moindre code barre!",
* 73,
* "3.2.5",
* [
* null,
* "X10i",
* "SEMC",
* "Xperia X10"
* ],
* "fr_FR",
* [
* null,
* "Prixing fonctionne pourtant bien sur Xperia X10. Essayez de prendre un minimum de recul, au moins 20 � 30cm, �vitez les ombres et les reflets. N'h�sitez pas � nous �crire sur contact@prixing.fr pour une assistance personnalis�e."
* ,
* null,
* "1350393460968"
* ],
* 1
* ]
*/
String uniqueId = jsonComment.getString("1");
comment.setUniqueId(uniqueId);
String user = jsonComment.optString("2");
if (user != null && !"".equals(user) && !"null".equals(user)) {
comment.setUser(user);
}
comment.setDate(parseDate(jsonComment.getLong("3")));
comment.setRating(jsonComment.getInt("4"));
String version = jsonComment.optString("7");
if (version != null && !"".equals(version) && !version.equals("null")) {
comment.setAppVersion(version);
}
JSONObject jsonCommentReview = jsonComment.optJSONObject("5");
String commentLang = jsonCommentReview.getString("1");
String commentText = jsonCommentReview.getString("3");
String commentTitle = jsonCommentReview.getString("2");
if (commentTitle.length() == 0) {
// Title field is empty, see if the title is part of the comment text
String originalTitleAndComment[] = commentText.split("\\t");
if (originalTitleAndComment.length == 2) {
commentTitle = originalTitleAndComment[0];
commentText = originalTitleAndComment[1];
}
}
comment.setLanguage(commentLang);
comment.setOriginalText(commentText);
// overwritten if translation is available
comment.setText(commentText);
comment.setOriginalTitle(commentTitle);
comment.setTitle(commentTitle);
JSONObject translation = jsonComment.optJSONObject("11");
if (translation != null) {
String displayLanguage = Locale.getDefault().getLanguage();
String translationLang = translation.getString("1");
if (translation.has("2")) {
String translationTitle = translation.getString("2");
if (translationLang.contains(displayLanguage)) {
comment.setTitle(translationTitle);
}
}
// Apparently, a translation body is not always provided
// Possibly happens if the translation fails or equals the original
if (translation.has("3")) {
String translationText = translation.getString("3");
if (translationLang.contains(displayLanguage)) {
comment.setText(translationText);
}
}
}
JSONObject jsonDevice = jsonComment.optJSONObject("16");
if (jsonDevice != null) {
String device = jsonDevice.optString("3");
JSONArray extraInfo = jsonDevice.optJSONArray("2");
if (extraInfo != null) {
device += " " + extraInfo.optString(0);
}
comment.setDevice(device.trim());
}
JSONObject jsonReply = jsonComment.optJSONObject("9");
if (jsonReply != null) {
Comment reply = new Comment(true);
reply.setText(jsonReply.getString("1"));
reply.setDate(parseDate(jsonReply.getLong("3")));
reply.setOriginalCommentDate(comment.getDate());
comment.setReply(reply);
}
comments.add(comment);
}
return comments;
}
static Comment parseCommentReplyResponse(String json) throws JSONException {
// {"result":{"1":{"1":"REPLY","3":"TIME_STAMP"},"2":true},"xsrf":"XSRF_TOKEN"}
// or
// {"error":{"data":{"1":ERROR_CODE},"code":ERROR_CODE}}
JSONObject jsonObj = new JSONObject(json);
if (jsonObj.has("error")) {
throw parseError(jsonObj, "replying to comments");
}
JSONObject replyObj = jsonObj.getJSONObject("result").getJSONObject("1");
Comment result = new Comment(true);
result.setText(replyObj.getString("1"));
result.setDate(parseDate(Long.parseLong(replyObj.getString("3"))));
return result;
}
private static DevConsoleException parseError(JSONObject jsonObj, String message)
throws JSONException {
JSONObject errorObj = jsonObj.getJSONObject("error");
String data = errorObj.getJSONObject("data").optString("1");
String errorCode = errorObj.optString("code");
return new DevConsoleException(String.format("Error %s: %s, errorCode=%s", message, data,
errorCode));
}
static RevenueSummary parseRevenueResponse(String json, String currency) throws JSONException {
JSONObject jsonObj = new JSONObject(json);
if (jsonObj.has("error")) {
throw parseError(jsonObj, "fetch revenue summary");
}
JSONArray arr = jsonObj.getJSONObject("result").getJSONArray("1");
if (arr.length() == 0) {
return null;
}
JSONObject summaryObj = arr.getJSONObject(0);
// free app or no revenue available
if (!summaryObj.has("1")) {
return null;
}
JSONArray revenueArr = summaryObj.getJSONArray("1");
double overall = 0;
double lastDay = 0;
double last30Days = 0;
double last7Days = 0;
for (int i = 0; i < revenueArr.length(); i++) {
JSONObject revenueObj = revenueArr.getJSONObject(i);
int period = revenueObj.getJSONArray("1").getInt(0);
double value = revenueObj.getJSONArray("2").optJSONObject(0).optDouble("1", 0.0) / 1000000;
switch (period) {
case REVENUE_OVERALL:
overall = value;
break;
case REVENUE_LAST_DAY:
lastDay = value;
break;
case REVENUE_LAST_7DAYS:
last7Days = value;
break;
case REVENUE_LAST_30DAYS:
last30Days = value;
break;
default:
throw new IllegalArgumentException("Unknown revenue period: " + period);
}
}
long timeInMillis = summaryObj.getLong("2");
Calendar cal = Calendar.getInstance();
/*
// TODO Work out timezone
String tzStr = summaryObj.optString("3");
TimeZone tz = TimeZone.getTimeZone(tzStr);
cal = Calendar.getInstance(tz);
*/
cal.setTimeInMillis(timeInMillis);
return RevenueSummary.createTotal(currency, cal.getTime(), lastDay, last7Days, last30Days,
overall);
}
/**
* Parses the given date
*
* @param unixDateCode
* @return
*/
private static Date parseDate(long unixDateCode) {
return new Date(unixDateCode);
}
}