/*
* Copyright 2012 Evernote Corporation.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.evernote.client.android;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Build;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.util.Base64;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.ValueCallback;
import com.evernote.client.android.helper.Cat;
import com.evernote.edam.type.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Locale;
@SuppressWarnings("unused")
public final class EvernoteUtil {
private static final Cat CAT = new Cat("EvernoteUtil");
private EvernoteUtil() {
}
/**
* Action for an {@link Intent} to authorize this app.
*/
public static final String ACTION_AUTHORIZE = "com.evernote.action.AUTHORIZE";
/**
* Action for an {@link Intent} to receive the bootstrap profile name from the main Evernote app.
*/
public static final String ACTION_GET_BOOTSTRAP_PROFILE_NAME = "com.evernote.action.GET_BOOTSTRAP_PROFILE_NAME";
/**
* Extra URL to authorize this app.
*/
public static final String EXTRA_AUTHORIZATION_URL = "authorization_url";
/**
* Returned OAuth callback from the main Evernote app.
*/
public static final String EXTRA_OAUTH_CALLBACK_URL = "oauth_callback_url";
/**
* Returned bootstrap profile name from the main Evernote app.
*/
public static final String EXTRA_BOOTSTRAP_PROFILE_NAME = "bootstrap_profile_name";
/**
* The ENML preamble to every Evernote note.
* Note content goes between <en-note> and </en-note>
*/
public static final String NOTE_PREFIX =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" + "<en-note>";
/**
* The ENML postamble to every Evernote note.
*/
public static final String NOTE_SUFFIX = "</en-note>";
/**
* One-way hashing function used for providing a checksum of EDAM data.
*/
private static final String EDAM_HASH_ALGORITHM = "MD5";
private static final MessageDigest HASH_DIGEST;
private static final String PACKAGE_NAME = "com.evernote";
private static final String EVERNOTE_SIGNATURE = "XS7HhF3x8-kho4iOnAQIdP7_m4UsbRKgaEAr1HaXwnc=";
static {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
// notify in hash method
messageDigest = null;
}
HASH_DIGEST = messageDigest;
}
/**
* Create an ENML <en-media> tag for the specified Resource object.
*/
public static String createEnMediaTag(Resource resource) {
return "<en-media hash=\"" + bytesToHex(resource.getData().getBodyHash()) + "\" type=\"" + resource.getMime() + "\"/>";
}
/**
* Returns an MD5 checksum of the provided array of bytes.
*/
public static byte[] hash(byte[] body) {
if (HASH_DIGEST != null) {
return HASH_DIGEST.digest(body);
} else {
throw new EvernoteUtilException(EDAM_HASH_ALGORITHM + " not supported", new NoSuchAlgorithmException(EDAM_HASH_ALGORITHM));
}
}
/**
* Returns an MD5 checksum of the contents of the provided InputStream.
*/
public static byte[] hash(InputStream in) throws IOException {
if (HASH_DIGEST == null) {
throw new EvernoteUtilException(EDAM_HASH_ALGORITHM + " not supported", new NoSuchAlgorithmException(EDAM_HASH_ALGORITHM));
}
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) != -1) {
HASH_DIGEST.update(buf, 0, n);
}
return HASH_DIGEST.digest();
}
/**
* Converts the provided byte array into a hexadecimal string
* with two characters per byte.
*/
public static String bytesToHex(byte[] bytes) {
return bytesToHex(bytes, false);
}
/**
* Takes the provided byte array and converts it into a hexadecimal string
* with two characters per byte.
*
* @param withSpaces if true, include a space character between each hex-rendered
* byte for readability.
*/
public static String bytesToHex(byte[] bytes, boolean withSpaces) {
StringBuilder sb = new StringBuilder();
for (byte hashByte : bytes) {
int intVal = 0xff & hashByte;
if (intVal < 0x10) {
sb.append('0');
}
sb.append(Integer.toHexString(intVal));
if (withSpaces) {
sb.append(' ');
}
}
return sb.toString();
}
/**
* Takes a string in hexadecimal format and converts it to a binary byte
* array. This does no checking of the format of the input, so this should
* only be used after confirming the format or origin of the string. The input
* string should only contain the hex data, two characters per byte.
*/
public static byte[] hexToBytes(String hexString) {
byte[] result = new byte[hexString.length() / 2];
for (int i = 0; i < result.length; ++i) {
int offset = i * 2;
result[i] = (byte) Integer.parseInt(hexString.substring(offset,
offset + 2), 16);
}
return result;
}
/**
* Removes all cookies for this application.
*/
public static void removeAllCookies(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
removeAllCookiesV21();
} else {
removeAllCookiesV14(context.getApplicationContext());
}
}
@SuppressWarnings("deprecation")
private static void removeAllCookiesV14(Context context) {
CookieSyncManager.createInstance(context);
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static void removeAllCookiesV21() {
final CookieManager cookieManager = CookieManager.getInstance();
Looper looper = Looper.myLooper();
boolean prepared = false;
if (looper == null) {
Looper.prepare();
prepared = true;
}
// requires a looper
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean value) {
Thread thread = new Thread() {
@Override
public void run() {
// is synchronous, run in background
cookieManager.flush();
}
};
thread.start();
}
});
if (prepared) {
looper = Looper.myLooper();
if (looper != null) {
looper.quit();
}
}
}
/**
* Checks if Evernote is installed and if the app can resolve this action.
*/
public static EvernoteInstallStatus getEvernoteInstallStatus(Context context, String action) {
PackageManager packageManager = context.getPackageManager();
Intent intent = new Intent(action).setPackage(PACKAGE_NAME);
List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (!resolveInfos.isEmpty()) {
return validateSignature(packageManager);
}
try {
// authentication feature not available, yet
packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
return EvernoteInstallStatus.OLD_VERSION;
} catch (Exception e) {
return EvernoteInstallStatus.NOT_INSTALLED;
}
}
@SuppressLint("PackageManagerGetSignatures")
private static EvernoteInstallStatus validateSignature(PackageManager packageManager) {
MessageDigest digest = getSha256Digest();
if (digest == null) {
// can't compare signatures
return EvernoteInstallStatus.NOT_INSTALLED;
}
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
return EvernoteInstallStatus.NOT_INSTALLED;
}
if (packageInfo.signatures == null || packageInfo.signatures.length == 0) {
// must have at least one signature
return EvernoteInstallStatus.NOT_INSTALLED;
}
// check all signatures
for (Signature signature : packageInfo.signatures) {
digest.update(signature.toByteArray());
String appSignature = encodeBase64(digest.digest());
if (EVERNOTE_SIGNATURE.equals(appSignature)) {
return EvernoteInstallStatus.INSTALLED;
}
}
return EvernoteInstallStatus.NOT_INSTALLED;
}
/**
* Creates an {@link Intent} to authorize this app by the main Evernote app.
*
* @param context The {@link Context} starting the {@link Intent}.
* @param authorizationUrl The OAuth authorization URL.
* @param forceThirdPartyApp If {@code true}, never use Evernote app to authenticate user.
* @return The {@link Intent}.
*/
public static Intent createAuthorizationIntent(Context context, String authorizationUrl, boolean forceThirdPartyApp) {
Intent intent;
if (!forceThirdPartyApp && EvernoteInstallStatus.INSTALLED.equals(getEvernoteInstallStatus(context, ACTION_AUTHORIZE))) {
intent = new Intent(ACTION_AUTHORIZE);
intent.setPackage(PACKAGE_NAME);
} else {
intent = new Intent(context, EvernoteOAuthActivity.class);
}
intent.putExtra(EXTRA_AUTHORIZATION_URL, authorizationUrl);
return intent;
}
/**
* Returns an Intent to query the bootstrap profile name from the main Evernote app. This is useful
* if you want to use the main app to authenticate the user and he is already signed in.
*
* @param context The {@link Context} starting the {@link Intent}.
* @param evernoteSession The current session.
* @return An Intent to query the bootstrap profile name. Returns {@code null}, if the main app
* is not installed, not up to date or you do not want to use the main app to authenticate the
* user.
*/
public static Intent createGetBootstrapProfileNameIntent(Context context, EvernoteSession evernoteSession) {
if (evernoteSession.isForceAuthenticationInThirdPartyApp()) {
// we don't want to use the main app, return null
return null;
}
EvernoteUtil.EvernoteInstallStatus installStatus = EvernoteUtil.getEvernoteInstallStatus(context, EvernoteUtil.ACTION_GET_BOOTSTRAP_PROFILE_NAME);
if (!EvernoteUtil.EvernoteInstallStatus.INSTALLED.equals(installStatus)) {
return null;
}
return new Intent(EvernoteUtil.ACTION_GET_BOOTSTRAP_PROFILE_NAME).setPackage(PACKAGE_NAME);
}
/**
* Construct a user-agent string based on the running application and
* the device and operating system information. This information is
* included in HTTP requests made to the Evernote service and assists
* in measuring traffic and diagnosing problems.
*/
public static String generateUserAgentString(Context ctx) {
String packageName = null;
int packageVersion = 0;
try {
packageName = ctx.getPackageName();
packageVersion = ctx.getPackageManager().getPackageInfo(packageName, 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
CAT.e(e.getMessage());
}
String userAgent = packageName + " Android/" + packageVersion;
Locale locale = java.util.Locale.getDefault();
if (locale == null) {
userAgent += " (" + Locale.US + ");";
} else {
userAgent += " (" + locale.toString() + "); ";
}
userAgent += "Android/" + Build.VERSION.RELEASE + "; ";
userAgent += Build.MODEL + "/" + Build.VERSION.SDK_INT + ";";
return userAgent;
}
public enum EvernoteInstallStatus {
INSTALLED,
OLD_VERSION,
NOT_INSTALLED
}
/**
* A runtime exception that will be thrown when we hit an error that should
* "never" occur ... e.g. if the JVM doesn't know about UTF-8 or MD5.
*/
@SuppressWarnings("serial")
private static final class EvernoteUtilException extends RuntimeException {
public EvernoteUtilException(String message, Throwable cause) {
super(message, cause);
}
}
@Nullable
private static MessageDigest getSha256Digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ignored) {
return null;
}
}
private static String encodeBase64(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP | Base64.URL_SAFE);
}
}