/**
* Copyright 2010-present Facebook.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook;
import android.content.Context;
import android.os.Bundle;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
import com.facebook.model.GraphObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Set;
/**
* The InsightsLogger class allows the developer to log various types of events back to Facebook. In order to log
* events, the app must create an instance of this class via a {@link #newLogger newLogger} method, and then call
* the various "log" methods off of that. Note that a Client Token for the app is required in calls to newLogger so
* apps that have not authenticated their users can still get meaningful user-demographics from the logged events
* on Facebook.
*/
public class InsightsLogger {
// Constants
// Event names, these match what the server expects.
private static final String EVENT_NAME_LOG_CONVERSION_PIXEL = "fb_log_offsite_pixel";
private static final String EVENT_NAME_LOG_MOBILE_PURCHASE = "fb_mobile_purchase";
// Event parameter names, these match what the server expects.
private static final String EVENT_PARAMETER_CURRENCY = "fb_currency";
private static final String EVENT_PARAMETER_PIXEL_ID = "fb_offsite_pixel_id";
private static final String EVENT_PARAMETER_PIXEL_VALUE = "fb_offsite_pixel_value";
// Static member variables
private static Session appAuthSession = null;
// Instance member variables
private final Context context;
private final String clientToken;
private final String applicationId;
private final Session specifiedSession;
/**
* Constructor is private, newLogger() methods should be used to build an instance.
*/
private InsightsLogger(Context context, String clientToken, String applicationId, Session session) {
Validate.notNull(context, "context");
// Always ensure the client token is present, even if not needed for this particular logging (because at
// some point it will be required). Be harsh by throwing an exception because this is all too easy to miss
// and things will work with authenticated sessions, but start failing with users that don't have
// authenticated sessions.
Validate.notNullOrEmpty(clientToken, "clientToken");
if (applicationId == null) {
applicationId = Utility.getMetadataApplicationId(context);
}
this.context = context;
this.clientToken = clientToken;
this.applicationId = applicationId;
this.specifiedSession = session;
}
/**
* Build an InsightsLogger instance to log events through. The Facebook app that these events are targeted at
* comes from this application's metadata.
*
* @param context Used to access the applicationId and the attributionId for non-authenticated users.
* @param clientToken The Facebook app's "client token", which, for a given appid can be found in the Security
* section of the Advanced tab of the Facebook App settings found
* at <https://developers.facebook.com/apps/[your-app-id]>.
*
* @return InsightsLogger instance to invoke log* methods on.
*/
public static InsightsLogger newLogger(Context context, String clientToken) {
return new InsightsLogger(context, clientToken, null, null);
}
/**
* Build an InsightsLogger instance to log events through. Allow explicit specification of an Facebook app
* to target.
*
* @param context Used to access the attributionId for non-authenticated users.
* @param clientToken The Facebook app's "client token", which, for a given appid can be found in the Security
* section of the Advanced tab of the Facebook App settings found
* at <https://developers.facebook.com/apps/[your-app-id]>
* @param applicationId Explicitly specified Facebook applicationId to log events against. If null, the
* applicationId embedded in the application metadata accessible from 'context' will
* be used.
*
* @return InsightsLogger instance to invoke log* methods on.
*/
public static InsightsLogger newLogger(Context context, String clientToken, String applicationId) {
return new InsightsLogger(context, clientToken, applicationId, null);
}
/**
* Build an InsightsLogger instance to log events through.
*
* @param context Used to access the attributionId for non-authenticated users.
* @param clientToken The Facebook app's "client token", which, for a given appid can be found in the Security
* section of the Advanced tab of the Facebook App settings found
* at <https://developers.facebook.com/apps/[your-app-id]>
* @param applicationId Explicitly specified Facebook applicationId to log events against. If null, the
* applicationId embedded in the application metadata accessible from 'context' will
* be used.
* @param session Explicitly specified Session to log events against. If null, the activeSession
* will be used if it's open, otherwise the logging will happen via the "clientToken"
* and specified appId.
*
* @return InsightsLogger instance to invoke log* methods on.
*/
public static InsightsLogger newLogger(Context context, String clientToken, String applicationId, Session session) {
return new InsightsLogger(context, clientToken, applicationId, session);
}
/**
* Logs a purchase event with Facebook, in the specified amount and with the specified currency.
*
* @param purchaseAmount Amount of purchase, in the currency specified by the 'currency' parameter. This value
* will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
* @param currency Currency used to specify the amount.
*/
public void logPurchase(BigDecimal purchaseAmount, Currency currency) {
logPurchase(purchaseAmount, currency, null);
}
/**
* Logs a purchase event with Facebook, in the specified amount and with the specified currency. Additional
* detail about the purchase can be passed in through the parameters bundle.
*
* @param purchaseAmount Amount of purchase, in the currency specified by the 'currency' parameter. This value
* will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
* @param currency Currency used to specify the amount.
* @param parameters Arbitrary additional information for describing this event. Should have no more than
* 10 entries, and keys should be mostly consistent from one purchase event to the next.
*/
public void logPurchase(BigDecimal purchaseAmount, Currency currency, Bundle parameters) {
if (purchaseAmount == null) {
notifyDeveloperError("purchaseAmount cannot be null");
return;
} else if (currency == null) {
notifyDeveloperError("currency cannot be null");
return;
}
if (parameters == null) {
parameters = new Bundle();
}
parameters.putString(EVENT_PARAMETER_CURRENCY, currency.getCurrencyCode());
logEventNow(EVENT_NAME_LOG_MOBILE_PURCHASE, purchaseAmount.doubleValue(), parameters);
}
/**
* Log, or "Fire" a Conversion Pixel. Conversion Pixels are used for Ads Conversion Tracking. See
* https://www.facebook.com/help/435189689870514 to learn more.
*
* @param pixelId Numeric ID for the conversion pixel to be logged. See
* https://www.facebook.com/help/435189689870514 to learn how to create a conversion pixel.
* @param valueOfPixel Value of what the logging of this pixel is worth to the calling app. The currency that this
* is expressed in doesn't matter, so long as it is consistent across all logging for this
* pixel. This value will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
*/
public void logConversionPixel(String pixelId, double valueOfPixel) {
if (pixelId == null) {
notifyDeveloperError("pixelID cannot be null");
return;
}
Bundle parameters = new Bundle();
parameters.putString(EVENT_PARAMETER_PIXEL_ID, pixelId);
parameters.putDouble(EVENT_PARAMETER_PIXEL_VALUE, valueOfPixel);
logEventNow(EVENT_NAME_LOG_CONVERSION_PIXEL, valueOfPixel, parameters);
}
/**
* This is the workhorse function of the InsightsLogger class and does the packaging and POST. As InsightsLogger
* is expanded to support more custom app events, this logic will become more complicated and allow for batching
* and flushing of multiple events, of persisting to disk so as to survive network outages, implicitly logging
* (with the dev's permission) SDK actions, etc.
*/
private void logEventNow(
final String eventName,
final double valueToSum,
final Bundle parameters) {
// Run everything synchronously on a worker thread.
Settings.getExecutor().execute(new Runnable() {
@Override
public void run() {
final String eventJSON = buildJSONForEvent(eventName, valueToSum, parameters);
if (eventJSON == null) {
// Failure in building JSON, already reported, so just return.
return;
}
GraphObject publishParams = GraphObject.Factory.create();
publishParams.setProperty("event", "CUSTOM_APP_EVENTS");
publishParams.setProperty("custom_events", eventJSON);
if (Utility.queryAppAttributionSupportAndWait(applicationId)) {
String attributionId = Settings.getAttributionId(context.getContentResolver());
if (attributionId != null) {
publishParams.setProperty("attribution", attributionId);
}
}
String publishUrl = String.format("%s/activities", applicationId);
try {
Request postRequest = Request.newPostRequest(sessionToLogTo(), publishUrl, publishParams, null);
Response response = postRequest.executeAndWait();
// A -1 error code happens if there is no connectivity. No need to notify the
// developer in that case.
final int NO_CONNECTIVITY_ERROR_CODE = -1;
if (response.getError() != null &&
response.getError().getErrorCode() != NO_CONNECTIVITY_ERROR_CODE) {
notifyDeveloperError(
String.format(
"Error publishing Insights event '%s'\n Response: %s\n Error: %s",
eventJSON,
response.toString(),
response.getError().toString()));
}
} catch (Exception e) {
Utility.logd("Insights-exception: ", e);
}
}
});
}
private static String buildJSONForEvent(String eventName, double valueToSum, Bundle parameters) {
String result;
try {
// Build custom event payload
JSONObject eventObject = new JSONObject();
eventObject.put("_eventName", eventName);
if (valueToSum != 1.0) {
eventObject.put("_valueToSum", valueToSum);
}
if (parameters != null) {
Set<String> keys = parameters.keySet();
for (String key : keys) {
Object value = parameters.get(key);
if (!(value instanceof String) &&
!(value instanceof Number)) {
notifyDeveloperError(
String.format("Parameter '%s' must be a string or a numeric type.", key));
}
eventObject.put(key, value);
}
}
JSONArray eventArray = new JSONArray();
eventArray.put(eventObject);
result = eventArray.toString();
} catch (JSONException exception) {
notifyDeveloperError(exception.toString());
result = null;
}
return result;
}
/**
* Using the specifiedSession member variable (which may be nil), find the real session to log to
* (with an access token). Precedence: 1) specified session, 2) activeSession, 3) app authenticated
* session via Client Token.
*/
private Session sessionToLogTo() {
synchronized (this) {
Session session = specifiedSession;
// Require an open session.
if (session == null || !session.isOpened()) {
session = Session.getActiveSession();
}
if (session == null || !session.isOpened() || session.getAccessToken() == null) {
if (appAuthSession == null) {
// Build and stash a client-token based session.
// Form the clientToken based access token from appID and client token.
String tokenString = String.format("%s|%s", applicationId, clientToken);
AccessToken token = AccessToken.createFromString(tokenString, null, AccessTokenSource.CLIENT_TOKEN);
appAuthSession = new Session(null, applicationId, new NonCachingTokenCachingStrategy(), false);
appAuthSession.open(token, null);
}
session = appAuthSession;
}
return session;
}
}
/**
* Invoke this method, rather than throwing an Exception, for situations where user/server input might reasonably
* cause this to occur, and thus don't want an exception thrown at production time, but do want logging
* notification.
*/
private static void notifyDeveloperError(String message) {
Logger.log(LoggingBehavior.DEVELOPER_ERRORS, "Insights", message);
}
}