package net.hockeyapp.android;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.text.TextUtils;
import net.hockeyapp.android.utils.HockeyLog;
import java.io.File;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
/**
* <h3>Description</h3>
*
* Various constants and meta information loaded from the context.
**/
public class Constants {
/**
* HockeyApp API URL.
*/
public static final String BASE_URL = "https://sdk.hockeyapp.net/";
/**
* Name of this SDK.
*/
public static final String SDK_NAME = "HockeySDK";
/**
* Version of the SDK - retrieved from the build configuration.
*/
public static final String SDK_VERSION = BuildConfig.VERSION_NAME;
public static final String FILES_DIRECTORY_NAME = "HockeyApp";
/**
* The user agent string the SDK will send with every HockeyApp API request.
*/
public static final String SDK_USER_AGENT = "HockeySDK/Android " + BuildConfig.VERSION_NAME;
/**
* Permissions request for the update task.
*/
public static final int UPDATE_PERMISSIONS_REQUEST = 1;
private static final String BUNDLE_BUILD_NUMBER = "buildNumber";
/**
* Path where crash logs and temporary files are stored.
*/
public static String FILES_PATH = null;
/**
* The app's version code.
*/
public static String APP_VERSION = null;
/**
* The app's version name.
*/
public static String APP_VERSION_NAME = null;
/**
* The app's package name.
*/
public static String APP_PACKAGE = null;
/**
* The device's OS version.
*/
public static String ANDROID_VERSION = null;
/**
* The device's OS build.
*/
public static String ANDROID_BUILD = null;
/**
* The device's model name.
*/
public static String PHONE_MODEL = null;
/**
* The device's model manufacturer name.
*/
public static String PHONE_MANUFACTURER = null;
/**
* Unique identifier for crash reports. This is package and device specific.
*/
public static String CRASH_IDENTIFIER = null;
/**
* Unique identifier for device, not dependent on package or device.
*/
public static String DEVICE_IDENTIFIER = null;
/**
* Initializes constants from the given context. The context is used to set
* the package name, version code, and the files path.
*
* @param context The context to use. Usually your Activity object.
*/
public static void loadFromContext(Context context) {
Constants.ANDROID_VERSION = android.os.Build.VERSION.RELEASE;
Constants.ANDROID_BUILD = android.os.Build.DISPLAY;
Constants.PHONE_MODEL = android.os.Build.MODEL;
Constants.PHONE_MANUFACTURER = android.os.Build.MANUFACTURER;
loadFilesPath(context);
loadPackageData(context);
loadCrashIdentifier(context);
loadDeviceIdentifier(context);
}
/**
* Returns a file representing the folder in which screenshots are stored.
*
* @return A file representing the screenshot folder.
*/
public static File getHockeyAppStorageDir() {
File externalStorage = Environment.getExternalStorageDirectory();
File dir = new File(externalStorage.getAbsolutePath() + "/" + Constants.FILES_DIRECTORY_NAME);
boolean success = dir.exists() || dir.mkdirs();
if (!success) {
HockeyLog.warn("Couldn't create HockeyApp Storage dir");
}
return dir;
}
/**
* Helper method to set the files path. If an exception occurs, the files
* path will be null!
*
* @param context The context to use. Usually your Activity object.
*/
private static void loadFilesPath(Context context) {
if (context != null) {
try {
File file = context.getFilesDir();
// The file shouldn't be null, but apparently it still can happen, see
// http://code.google.com/p/android/issues/detail?id=8886
if (file != null) {
Constants.FILES_PATH = file.getAbsolutePath();
}
} catch (Exception e) {
HockeyLog.error("Exception thrown when accessing the files dir:");
e.printStackTrace();
}
}
}
/**
* Helper method to set the package name and version code. If an exception
* occurs, these values will be null!
*
* @param context The context to use. Usually your Activity object.
*/
private static void loadPackageData(Context context) {
if (context != null) {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
Constants.APP_PACKAGE = packageInfo.packageName;
Constants.APP_VERSION = "" + packageInfo.versionCode;
Constants.APP_VERSION_NAME = packageInfo.versionName;
int buildNumber = loadBuildNumber(context, packageManager);
if ((buildNumber != 0) && (buildNumber > packageInfo.versionCode)) {
Constants.APP_VERSION = "" + buildNumber;
}
} catch (PackageManager.NameNotFoundException e) {
HockeyLog.error("Exception thrown when accessing the package info:");
e.printStackTrace();
}
}
}
/**
* Helper method to load the build number from the AndroidManifest.
*
* @param context the context to use. Usually your Activity object.
* @param packageManager an instance of PackageManager
*/
private static int loadBuildNumber(Context context, PackageManager packageManager) {
try {
ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = appInfo.metaData;
if (metaData != null) {
return metaData.getInt(BUNDLE_BUILD_NUMBER, 0);
}
} catch (PackageManager.NameNotFoundException e) {
HockeyLog.error("Exception thrown when accessing the application info:");
e.printStackTrace();
}
return 0;
}
/**
* Helper method to load the crash identifier.
*
* @param context the context to use. Usually your Activity object.
*/
private static void loadCrashIdentifier(Context context) {
String deviceIdentifier = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (!TextUtils.isEmpty(Constants.APP_PACKAGE) && !TextUtils.isEmpty(deviceIdentifier)) {
String combined = Constants.APP_PACKAGE + ":" + deviceIdentifier + ":" + createSalt(context);
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = combined.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
Constants.CRASH_IDENTIFIER = bytesToHex(bytes);
} catch (Throwable e) {
HockeyLog.error("Couldn't create CrashIdentifier with Exception:" + e.toString());
//TODO handle the exception
}
}
}
/**
* Helper method to generate a device identifier for telemetry and crashes,
*
* @param context The context to use. Usually your Activity object.
*/
private static void loadDeviceIdentifier(Context context) {
// get device ID
ContentResolver resolver = context.getContentResolver();
String deviceIdentifier = Settings.Secure.getString(resolver, Settings.Secure.ANDROID_ID);
if (deviceIdentifier != null) {
String deviceIdentifierAnonymized = tryHashStringSha256(context, deviceIdentifier);
// if anonymized device identifier is null we should use a random UUID
Constants.DEVICE_IDENTIFIER = deviceIdentifierAnonymized != null ? deviceIdentifierAnonymized : UUID.randomUUID().toString();
}
}
/**
* Get a SHA-256 hash of the input string if the algorithm is available. If the algorithm is
* unavailable, return empty string.
*
* @param input the string to hash.
* @return a SHA-256 hash of the input or null if SHA-256 is not available (should never happen).
*/
private static String tryHashStringSha256(Context context, String input) {
String salt = createSalt(context);
try {
// Get a Sha256 digest
MessageDigest hash = MessageDigest.getInstance("SHA-256");
hash.reset();
hash.update(input.getBytes());
hash.update(salt.getBytes());
byte[] hashedBytes = hash.digest();
return bytesToHex(hashedBytes);
} catch (NoSuchAlgorithmException e) {
// All android devices should support SHA256, but if unavailable return null
return null;
}
}
/**
* Helper method to create a salt for the crash identifier.
*
* @param context the context to use. Usually your Activity object.
*/
@SuppressLint("InlinedApi")
@SuppressWarnings("deprecation")
private static String createSalt(Context context) {
String abiString;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abiString = Build.SUPPORTED_ABIS[0];
} else {
abiString = Build.CPU_ABI;
}
String fingerprint = "HA" + (Build.BOARD.length() % 10) + (Build.BRAND.length() % 10) +
(abiString.length() % 10) + (Build.PRODUCT.length() % 10);
String serial = "";
try {
serial = android.os.Build.class.getField("SERIAL").get(null).toString();
} catch (Throwable t) {
}
return fingerprint + ":" + serial;
}
/**
* Helper method to convert a byte array to the hex string.
* Based on http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
*
* @param bytes a byte array
*/
private static String bytesToHex(byte[] bytes) {
final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
char[] hex = new char[bytes.length * 2];
for (int index = 0; index < bytes.length; index++) {
int value = bytes[index] & 0xFF;
hex[index * 2] = HEX_ARRAY[value >>> 4];
hex[index * 2 + 1] = HEX_ARRAY[value & 0x0F];
}
String result = new String(hex);
return result.replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5");
}
}