com.trk.aboutme.facebook.InsightsLogger.java Source code

Java tutorial

Introduction

Here is the source code for com.trk.aboutme.facebook.InsightsLogger.java

Source

/**
 * 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.trk.aboutme.facebook;

import android.content.Context;
import android.os.Bundle;
import com.trk.aboutme.facebook.internal.Logger;
import com.trk.aboutme.facebook.internal.Utility;
import com.trk.aboutme.facebook.internal.Validate;
import com.trk.aboutme.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);
    }
}