package net.hockeyapp.android.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import net.hockeyapp.android.UpdateInfoListener;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Scanner;
import java.util.regex.Pattern;
/**
* <h3>Description</h3>
*
* Internal helper class. Provides helper methods to parse the
* version JSON and create the release notes as HTML.
*
**/
public class VersionHelper {
public static final String VERSION_MAX = "99.0";
private ArrayList<JSONObject> mSortedVersions;
private JSONObject mNewest;
private UpdateInfoListener mListener;
private int mCurrentVersionCode;
public VersionHelper(Context context, String infoJSON, UpdateInfoListener listener) {
this.mListener = listener;
loadVersions(context, infoJSON);
sortVersions();
}
private void loadVersions(Context context, String infoJSON) {
this.mNewest = new JSONObject();
this.mSortedVersions = new ArrayList<JSONObject>();
this.mCurrentVersionCode = mListener.getCurrentVersionCode();
try {
JSONArray versions = new JSONArray(infoJSON);
int versionCode = mListener.getCurrentVersionCode();
for (int index = 0; index < versions.length(); index++) {
JSONObject entry = versions.getJSONObject(index);
boolean largerVersionCode = (entry.getInt("version") > versionCode);
boolean newerApkFile = ((entry.getInt("version") == versionCode) && VersionHelper.isNewerThanLastUpdateTime(context, entry.getLong("timestamp")));
if (largerVersionCode || newerApkFile) {
mNewest = entry;
versionCode = entry.getInt("version");
}
mSortedVersions.add(entry);
}
} catch (JSONException je) {
} catch (NullPointerException ne) {
}
}
private void sortVersions() {
Collections.sort(mSortedVersions, new Comparator<JSONObject>() {
public int compare(JSONObject object1, JSONObject object2) {
try {
if (object1.getInt("version") > object2.getInt("version")) {
return 0;
}
} catch (JSONException je) {
} catch (NullPointerException ne) {
}
return 0;
}
});
}
public String getVersionString() {
return failSafeGetStringFromJSON(mNewest, "shortversion", "") + " (" + failSafeGetStringFromJSON(mNewest, "version", "") + ")";
}
@SuppressLint("SimpleDateFormat")
public String getFileDateString() {
long timestamp = failSafeGetLongFromJSON(mNewest, "timestamp", 0L);
Date date = new Date(timestamp * 1000L);
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy");
return dateFormat.format(date);
}
public long getFileSizeBytes() {
boolean external = Boolean.valueOf(failSafeGetStringFromJSON(mNewest, "external", "false"));
long appSize = failSafeGetLongFromJSON(mNewest, "appsize", 0L);
// In case of external builds a size of 0 most likely means that the size could not be determined because the URL
// is not accessible from the HockeyApp servers via the Internet. Return -1 in that case in order to try retrieving
// the size at runtime from the HTTP header later.
return (external && appSize == 0L) ? -1L : appSize;
}
private static String failSafeGetStringFromJSON(JSONObject json, String name, String defaultValue) {
try {
return json.getString(name);
} catch (JSONException e) {
return defaultValue;
}
}
private static long failSafeGetLongFromJSON(JSONObject json, String name, long defaultValue) {
try {
return json.getLong(name);
} catch (JSONException e) {
return defaultValue;
}
}
public String getReleaseNotes(boolean showRestore) {
StringBuilder result = new StringBuilder();
result.append("<html>");
result.append("<body style='padding: 0px 0px 20px 0px'>");
int count = 0;
for (JSONObject version : mSortedVersions) {
if (count > 0) {
result.append(getSeparator());
if (showRestore) {
result.append(getRestoreButton(count, version));
}
}
result.append(getVersionLine(count, version));
result.append(getVersionNotes(count, version));
count++;
}
result.append("</body>");
result.append("</html>");
return result.toString();
}
private Object getSeparator() {
return "<hr style='border-top: 1px solid #c8c8c8; border-bottom: 0px; margin: 40px 10px 0px 10px;' />";
}
private String getRestoreButton(int count, JSONObject version) {
StringBuilder result = new StringBuilder();
String versionID = getVersionID(version);
if (!TextUtils.isEmpty(versionID)) {
result.append("<a href='restore:" + versionID + "' style='background: #c8c8c8; color: #000; display: block; float: right; padding: 7px; margin: 0px 10px 10px; text-decoration: none;'>Restore</a>");
}
return result.toString();
}
private String getVersionID(JSONObject version) {
String versionID = "";
try {
versionID = version.getString("id");
} catch (JSONException e) {
}
return versionID;
}
private String getVersionLine(int count, JSONObject version) {
StringBuilder result = new StringBuilder();
int newestCode = getVersionCode(mNewest);
int versionCode = getVersionCode(version);
String versionName = getVersionName(version);
result.append("<div style='padding: 20px 10px 10px;'><strong>");
if (count == 0) {
result.append("Newest version:");
} else {
result.append("Version " + versionName + " (" + versionCode + "): ");
if ((versionCode != newestCode) && (versionCode == mCurrentVersionCode)) {
mCurrentVersionCode = -1;
result.append("[INSTALLED]");
}
}
result.append("</strong></div>");
return result.toString();
}
private int getVersionCode(JSONObject version) {
int versionCode = 0;
try {
versionCode = version.getInt("version");
} catch (JSONException e) {
}
return versionCode;
}
private String getVersionName(JSONObject version) {
String versionName = "";
try {
versionName = version.getString("shortversion");
} catch (JSONException e) {
}
return versionName;
}
private String getVersionNotes(int count, JSONObject version) {
StringBuilder result = new StringBuilder();
String notes = failSafeGetStringFromJSON(version, "notes", "");
result.append("<div style='padding: 0px 10px;'>");
if (notes.trim().length() == 0) {
result.append("<em>No information.</em>");
} else {
result.append(notes);
}
result.append("</div>");
return result.toString();
}
/**
* Compare two versions strings with each other by splitting at the .
* and comparing the integer values. Additional string like "-update1"
* are ignored, i.e. "2.2" is considered equal to "2.2-update1".
*
* @param left A version string, e.g. "2.1".
* @param right A version string, e.g. "4.2.2".
* @return 0 if the versions are equal.
* 1 if the left side is bigger.
* -1 if the right side is bigger.
*/
public static int compareVersionStrings(String left, String right) {
// If either side is null, we consider the versions equal
if ((left == null) || (right == null)) {
return 0;
}
try {
// Strip out any "-update1" stuff, then build a scanner for the strings
Scanner leftScanner = new Scanner(left.replaceAll("\\-.*", ""));
Scanner rightScanner = new Scanner(right.replaceAll("\\-.*", ""));
leftScanner.useDelimiter("\\.");
rightScanner.useDelimiter("\\.");
// Compare the parts
while ((leftScanner.hasNextInt()) && (rightScanner.hasNextInt())) {
int leftValue = leftScanner.nextInt();
int rightValue = rightScanner.nextInt();
if (leftValue < rightValue) {
return -1;
} else if (leftValue > rightValue) {
return 1;
}
}
// Left side has more parts, so consider it bigger
if (leftScanner.hasNextInt()) {
return 1;
}
// Right side has more parts, so consider it bigger
else if (rightScanner.hasNextInt()) {
return -1;
}
// Ok, they are equal
else {
return 0;
}
} catch (Exception e) {
// If any exceptions happen, return zero
return 0;
}
}
/**
* Returns true of the given timestamp is larger / newer than the last modified timestamp of
* the APK file of the app.
*
* @param context the context to use
* @param timestamp a Unix-style timestamp
* @return true if the timestamp is larger / never
*/
public static boolean isNewerThanLastUpdateTime(Context context, long timestamp) {
if (context == null) {
return false;
}
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), 0);
String appFile = appInfo.sourceDir;
// Get the last modified time stamp and adjust by half an hour
// to avoid issues with time deviations between client and server
long lastModified = new File(appFile).lastModified() / 1000 + 1800;
return timestamp > lastModified;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return false;
}
}
/**
* Map internal Google version letter to a semantic version (currently L to 5.0, M to 6.0).
* All other pre release versions (versions consisting of only letters) will return VERSION_MAX,
* to indicate they are newer, to prevent having new releases of the SDK with every Android
* pre release.
*
* @param version value of Build.VERSION.RELEASE
* @return mapped version number
*/
public static String mapGoogleVersion(String version) {
if ((version == null) || (version.equalsIgnoreCase("L"))) {
return "5.0";
} else if (version.equalsIgnoreCase("M")) {
return "6.0";
} else if (version.equalsIgnoreCase("N")) {
return "7.0";
} else if (Pattern.matches("^[a-zA-Z]+", version)) {
return VERSION_MAX;
} else {
return version;
}
}
}