/** * 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; } }
@SuppressWarnings("resource") public List<AppStats> readStats(InputStream in) throws ServiceException { List<AppStats> appStats = new ArrayList<AppStats>(); CSVReader reader; try { // we don't own the stream, it's closed by the caller reader = new CSVReader(new InputStreamReader(in)); String[] firstLine = reader.readNext(); if (firstLine != null) { String[] nextLine = null; while ((nextLine = reader.readNext()) != null) { AppStats stats = new AppStats(); stats.setPackageName(nextLine[0]); stats.setDate(createTimestampFormat().parse(nextLine[1])); stats.setTotalDownloads(Integer.parseInt(nextLine[2])); stats.setActiveInstalls(Integer.parseInt(nextLine[3])); stats.setNumberOfComments(Integer.parseInt(nextLine[4])); stats.setRating1(Integer.parseInt(nextLine[5])); stats.setRating2(Integer.parseInt(nextLine[6])); stats.setRating3(Integer.parseInt(nextLine[7])); stats.setRating4(Integer.parseInt(nextLine[8])); stats.setRating5(Integer.parseInt(nextLine[9])); if (nextLine.length > 10) { stats.setVersionCode(Integer.parseInt(nextLine[10])); } if (nextLine.length > 11) { String numErrorsStr = nextLine[11]; stats.setNumberOfErrors(parseInt(numErrorsStr)); } if (nextLine.length > 12) { String totalRevenueStr = nextLine[12]; if (!TextUtils.isEmpty(totalRevenueStr)) { String currency = nextLine[13]; stats.setTotalRevenue( new Revenue(Revenue.Type.TOTAL, parseDouble(totalRevenueStr.trim()), currency)); } } appStats.add(stats); } } } catch (FileNotFoundException e) { throw new ServiceException(e); } catch (IOException e) { throw new ServiceException(e); } catch (ParseException e) { throw new ServiceException(e); } return appStats; }
/** * 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: // 1 : { 1: package name, // 2 : { 1: [{1 : lang, 2: name, 3: description, 4: ??, 5: what's new}], 2 : ?? }, // 3 : ??, // 4 : update history, // 5 : price, // 6 : update date, // 7 : state? // } // 2 : {} // 3 : { 1: active dnd, 2: # ratings, 3: avg rating, 4: ???, 5: total dnd } // arrays have changed to objects, with the index as the key /* * Per app: * null * [ APP_INFO_ARRAY * * null * * packageName * * Nested array with details * * null * * Nested array with version details * * Nested array with price details * * Last update Date * * Number [1=published, 5 = draft?] * ] * null * [ APP_STATS_ARRAY * * null, * * Active installs * * Total ratings * * Average rating * * Errors * * Total installs * ] */ JSONObject jsonApp = jsonApps.getJSONObject(i); 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); /* * Per app details: * 1: Country code * 2: App Name * 3: Description * 4: Promo text * 5: Last what's new */ // skip if we can't get all the data // XXX should we just let this crash so we know there is a problem? if (!jsonAppInfo.has("2")) { 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; } if (!jsonAppInfo.has("4")) { if (skipIncomplete) { Log.d( TAG, String.format( "Skipping app %d because no versions info found: package name=%s", i, packageName)); } else { Log.d(TAG, "Adding incomplete app: " + packageName); apps.add(app); } continue; } JSONObject appDetails = jsonAppInfo.getJSONObject("2").getJSONArray("1").getJSONObject(0); if (DEBUG) { pp("appDetails", appDetails); } app.setName(appDetails.getString("2")); String description = appDetails.getString("3"); String changelog = appDetails.optString("5"); Long lastPlayStoreUpdate = jsonAppInfo.getJSONObject("11").getLong("1"); AppDetails details = new AppDetails(description, changelog, lastPlayStoreUpdate); app.setDetails(details); /* * Per app version details: * null * null * packageName * versionNumber * versionName * null * Array with app icon [null,null,null,icon] */ // XXX JSONArray appVersions = jsonAppInfo.getJSONObject("4").getJSONObject("1").optJSONArray("1"); if (DEBUG) { pp("appVersions", appVersions); } if (appVersions == null) { if (skipIncomplete) { Log.d( TAG, String.format( "Skipping app %d because no versions info found: package name=%s", i, packageName)); } else { Log.d(TAG, "Adding incomplete app: " + packageName); apps.add(app); } continue; } JSONObject lastAppVersionDetails = appVersions.getJSONObject(appVersions.length() - 1).getJSONObject("2"); if (DEBUG) { pp("lastAppVersionDetails", lastAppVersionDetails); } app.setVersionName(lastAppVersionDetails.getString("4")); app.setIconUrl(lastAppVersionDetails.getJSONObject("6").getString("3")); // App stats /* * null, * Active installs * Total ratings * Average rating * Errors * Total installs */ // XXX this index might not be correct for all apps? // 3 : { 1: active dnd, 2: # ratings, 3: avg rating, 4: #errors?, 5: total dnd } JSONObject jsonAppStats = jsonApp.optJSONObject("3"); if (DEBUG) { pp("jsonAppStats", jsonAppStats); } if (jsonAppStats == null) { if (skipIncomplete) { Log.d( TAG, String.format( "Skipping app %d because no stats found: package name=%s", i, packageName)); } else { Log.d(TAG, "Adding incomplete app: " + packageName); apps.add(app); } continue; } 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.getInt("1")); stats.setTotalDownloads(jsonAppStats.getInt("5")); stats.setNumberOfErrors(jsonAppStats.optInt("4")); } app.setLatestStats(stats); apps.add(app); } return apps; }
/** * Parses the supplied JSON string and builds a list of apps from it * * @param json * @param accountName * @return List of apps * @throws JSONException */ static List<AppInfo> parseAppInfos(String json, String accountName) throws JSONException { Date now = new Date(); List<AppInfo> apps = new ArrayList<AppInfo>(); // Extract the base array containing apps JSONArray jsonApps = new JSONObject(json).getJSONArray("result").getJSONArray(1); if (DEBUG) { pp("jsonApps", jsonApps); } int numberOfApps = jsonApps.length(); for (int i = 0; i < numberOfApps; i++) { AppInfo app = new AppInfo(); app.setAccount(accountName); app.setLastUpdate(now); /* * Per app: * null * [ APP_INFO_ARRAY * * null * * packageName * * Nested array with details * * null * * Nested array with version details * * Nested array with price details * * Last update Date * * Number [1=published, 5 = draft?] * ] * null * [ APP_STATS_ARRAY * * null, * * Active installs * * Total ratings * * Average rating * * Errors * * Total installs * ] */ JSONArray jsonApp = jsonApps.getJSONArray(i); JSONArray jsonAppInfo = jsonApp.getJSONArray(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)))) { 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.getInt(7); Log.d(TAG, String.format("%s: publishState=%d", packageName, publishState)); if (publishState != 1) { // Not a published app, skipping continue; } app.setPublishState(publishState); app.setPackageName(packageName); /* * Per app details: * null * Country code * App Name * Description * Unknown * Last what's new */ if (jsonAppInfo.length() < 5) { // skip if we can't get all the data continue; } JSONArray appDetails = jsonAppInfo.getJSONArray(2).getJSONArray(1).getJSONArray(0); if (DEBUG) { pp("appDetails", appDetails); } app.setName(appDetails.getString(2)); /* * Per app version details: * null * null * packageName * versionNumber * versionName * null * Array with app icon [null,null,null,icon] */ JSONArray appVersions = jsonAppInfo.optJSONArray(4); if (DEBUG) { pp("appVersions", appVersions); } if (appVersions == null) { continue; } JSONArray lastAppVersionDetails = appVersions.getJSONArray(appVersions.length() - 1).getJSONArray(2); if (DEBUG) { pp("lastAppVersionDetails", lastAppVersionDetails); } app.setVersionName(lastAppVersionDetails.getString(4)); app.setIconUrl(lastAppVersionDetails.getJSONArray(6).getString(3)); // App stats /* * null, * Active installs * Total ratings * Average rating * Errors * Total installs */ // XXX this index might not be correct for all apps? JSONArray jsonAppStats = jsonApp.optJSONArray(3); if (DEBUG) { pp("jsonAppStats", jsonAppStats); } if (jsonAppStats == null) { continue; } AppStats stats = new AppStats(); stats.setRequestDate(now); if (jsonAppStats.length() < 6) { // no statistics (yet?) or weird format // TODO do we need differentiate? stats.setActiveInstalls(0); stats.setTotalDownloads(0); stats.setNumberOfErrors(0); } else { stats.setActiveInstalls(jsonAppStats.getInt(1)); stats.setTotalDownloads(jsonAppStats.getInt(5)); stats.setNumberOfErrors(jsonAppStats.optInt(4)); } app.setLatestStats(stats); apps.add(app); } return apps; }