/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
* copy, modify, and distribute this software in source code or binary form for use
* in connection with the web services and APIs provided by Facebook.
*
* As with any software that integrates with the Facebook platform, your use of
* this software is subject to the Facebook Developer Principles and Policies
* [http://developers.facebook.com/policy/]. This copyright notice shall be
* included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.facebook.appevents;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.facebook.FacebookException;
import com.facebook.LoggingBehavior;
import com.facebook.appevents.internal.Constants;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Locale;
import java.util.UUID;
class AppEvent implements Serializable {
private static final long serialVersionUID = 1L;
private static final HashSet<String> validatedIdentifiers = new HashSet<String>();
private final JSONObject jsonObject;
private final boolean isImplicit;
private final String name;
private final String checksum;
public AppEvent(
String contextName,
String eventName,
Double valueToSum,
Bundle parameters,
boolean isImplicitlyLogged,
@Nullable final UUID currentSessionId
) throws JSONException, FacebookException {
jsonObject = getJSONObjectForAppEvent(
contextName,
eventName,
valueToSum,
parameters,
isImplicitlyLogged,
currentSessionId);
isImplicit = isImplicitlyLogged;
name = eventName;
checksum = calculateChecksum();
}
public String getName() {
return name;
}
private AppEvent(
String jsonString,
boolean isImplicit,
String checksum) throws JSONException {
jsonObject = new JSONObject(jsonString);
this.isImplicit = isImplicit;
this.name = jsonObject.optString(Constants.EVENT_NAME_EVENT_KEY);
this.checksum = checksum;
}
public boolean getIsImplicit() {
return isImplicit;
}
public JSONObject getJSONObject() {
return jsonObject;
}
public boolean isChecksumValid() {
if (this.checksum == null) {
// for old events we don't have a checksum
return true;
}
return calculateChecksum().equals(checksum);
}
// throw exception if not valid.
private static void validateIdentifier(String identifier) throws FacebookException {
// Identifier should be 40 chars or less, and only have 0-9A-Za-z, underscore, hyphen,
// and space (but no hyphen or space in the first position).
final String regex = "^[0-9a-zA-Z_]+[0-9a-zA-Z _-]*$";
final int MAX_IDENTIFIER_LENGTH = 40;
if (identifier == null
|| identifier.length() == 0
|| identifier.length() > MAX_IDENTIFIER_LENGTH) {
if (identifier == null) {
identifier = "<None Provided>";
}
throw new FacebookException(
String.format(
Locale.ROOT,
"Identifier '%s' must be less than %d characters",
identifier,
MAX_IDENTIFIER_LENGTH)
);
}
boolean alreadyValidated;
synchronized (validatedIdentifiers) {
alreadyValidated = validatedIdentifiers.contains(identifier);
}
if (!alreadyValidated) {
if (identifier.matches(regex)) {
synchronized (validatedIdentifiers) {
validatedIdentifiers.add(identifier);
}
} else {
throw new FacebookException(
String.format(
"Skipping event named '%s' due to illegal name - must be " +
"under 40 chars and alphanumeric, _, - or space, and " +
"not start with a space or hyphen.",
identifier
)
);
}
}
}
private static JSONObject getJSONObjectForAppEvent(
String contextName,
String eventName,
Double valueToSum,
Bundle parameters,
boolean isImplicitlyLogged,
@Nullable final UUID currentSessionId
) throws FacebookException, JSONException{
validateIdentifier(eventName);
JSONObject eventObject = new JSONObject();
eventObject.put(Constants.EVENT_NAME_EVENT_KEY, eventName);
eventObject.put(Constants.LOG_TIME_APP_EVENT_KEY, System.currentTimeMillis() / 1000);
eventObject.put("_ui", contextName);
if (currentSessionId != null) {
eventObject.put("_session_id", currentSessionId);
}
if (valueToSum != null) {
eventObject.put("_valueToSum", valueToSum.doubleValue());
}
if (isImplicitlyLogged) {
eventObject.put("_implicitlyLogged", "1");
}
String externalAnalyticsUserId = AppEventsLogger.getUserID();
if (externalAnalyticsUserId != null) {
eventObject.put("_app_user_id", externalAnalyticsUserId);
}
if (parameters != null) {
for (String key : parameters.keySet()) {
validateIdentifier(key);
Object value = parameters.get(key);
if (!(value instanceof String) && !(value instanceof Number)) {
throw new FacebookException(
String.format(
"Parameter value '%s' for key '%s' should be a string" +
" or a numeric type.",
value,
key)
);
}
eventObject.put(key, value.toString());
}
}
if (!isImplicitlyLogged) {
Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
"Created app event '%s'", eventObject.toString());
}
return eventObject;
}
// OLD VERSION DO NOT USE
static class SerializationProxyV1 implements Serializable {
private static final long serialVersionUID = -2488473066578201069L;
private final String jsonString;
private final boolean isImplicit;
private SerializationProxyV1(String jsonString, boolean isImplicit) {
this.jsonString = jsonString;
this.isImplicit = isImplicit;
}
private Object readResolve() throws JSONException {
return new AppEvent(jsonString, isImplicit, null);
}
}
static class SerializationProxyV2 implements Serializable {
private static final long serialVersionUID = 2016_08_03_001L;
private final String jsonString;
private final boolean isImplicit;
private final String checksum;
private SerializationProxyV2(String jsonString, boolean isImplicit, String checksum) {
this.jsonString = jsonString;
this.isImplicit = isImplicit;
this.checksum = checksum;
}
private Object readResolve() throws JSONException {
return new AppEvent(jsonString, isImplicit, checksum);
}
}
private Object writeReplace() {
return new SerializationProxyV2(jsonObject.toString(), isImplicit, checksum);
}
@Override
public String toString() {
return String.format(
"\"%s\", implicit: %b, json: %s",
jsonObject.optString("_eventName"),
isImplicit,
jsonObject.toString());
}
private String calculateChecksum() {
return md5Checksum(jsonObject.toString());
}
private static String md5Checksum(String toHash )
{
String hash;
try
{
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = toHash.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
hash = bytesToHex( bytes );
}
catch(NoSuchAlgorithmException e )
{
Utility.logd("Failed to generate checksum: ", e);
return "0";
}
catch(UnsupportedEncodingException e )
{
Utility.logd("Failed to generate checksum: ", e);
return "1";
}
return hash;
}
private static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}