/** * Copyright 2016-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * <p> * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * <p> * http://aws.amazon.com/apache2.0 * <p> * or in the "license" file accompanying this file. This file 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.amazonaws.mobileconnectors.pinpoint.internal.event; import android.database.Cursor; import android.net.Uri; import com.amazonaws.AmazonServiceException; import com.amazonaws.mobileconnectors.pinpoint.PinpointManager; import com.amazonaws.mobileconnectors.pinpoint.analytics.AnalyticsEvent; import com.amazonaws.mobileconnectors.pinpoint.internal.core.PinpointContext; import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.StringUtil; import com.amazonaws.mobileconnectors.pinpoint.targeting.TargetingClient; import com.amazonaws.services.pinpointanalytics.model.Event; import com.amazonaws.services.pinpointanalytics.model.PutEventsRequest; import com.amazonaws.services.pinpointanalytics.model.Session; import com.amazonaws.util.Base64; import com.amazonaws.util.DateUtils; import com.amazonaws.util.VersionInfoUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Provides methods to record events and submit events to Pinpoint. */ public class EventRecorder { private static final String USER_AGENT = PinpointManager.class.getName() + "/" + VersionInfoUtils.getVersion(); private static final int CLIPPED_EVENT_LENGTH = 10; private final static int MAX_EVENT_OPERATIONS = 1000; static final String KEY_MAX_SUBMISSION_SIZE = "maxSubmissionSize"; static final long DEFAULT_MAX_SUBMISSION_SIZE = 1024 * 100; static final String KEY_MAX_PENDING_SIZE = "maxPendingSize"; static final long DEFAULT_MAX_PENDING_SIZE = 5 * 1024 * 1024; static final String KEY_MAX_SUBMISSIONS_ALLOWED = "maxSubmissionAllowed"; static final int DEFAULT_MAX_SUBMISSIONS_ALLOWED = 3; private static final long MINIMUM_PENDING_SIZE = 16 * 1024; private final PinpointDBUtil dbUtil; private final ExecutorService submissionRunnableQueue; private final PinpointContext pinpointContext; private static final Log log = LogFactory.getLog(EventRecorder.class); /** * Constructs a new EventRecorder specifying the client to use. * * @param pinpointContext The pinpoint pinpointContext */ public static EventRecorder newInstance(PinpointContext pinpointContext) { return newInstance(pinpointContext, new PinpointDBUtil(pinpointContext.getApplicationContext().getApplicationContext())); } public static EventRecorder newInstance(PinpointContext pinpointContext, PinpointDBUtil dbUtil) { final ExecutorService submissionRunnableQueue = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>( MAX_EVENT_OPERATIONS), new ThreadPoolExecutor.DiscardPolicy()); return new EventRecorder(pinpointContext, dbUtil, submissionRunnableQueue); } public void closeDB() { dbUtil.closeDB(); } EventRecorder(PinpointContext pinpointContext, PinpointDBUtil dbUtil, ExecutorService submissionRunnableQueue) { this.pinpointContext = pinpointContext; this.dbUtil = dbUtil; this.submissionRunnableQueue = submissionRunnableQueue; } public Uri recordEvent(AnalyticsEvent event) { log.info(String.format("Event Recorded to database: %s", event.toString())); long maxPendingSize = pinpointContext.getConfiguration().optLong( KEY_MAX_PENDING_SIZE, DEFAULT_MAX_PENDING_SIZE); if (maxPendingSize < MINIMUM_PENDING_SIZE) { maxPendingSize = MINIMUM_PENDING_SIZE; } final Uri uri = this.dbUtil.saveEvent(event); if (uri != null) { while(this.dbUtil.getTotalSize() > maxPendingSize) { final Cursor cursor = this.dbUtil.queryOldestEvents(5); while(this.dbUtil.getTotalSize() > maxPendingSize && cursor.moveToNext()) { this.dbUtil.deleteEvent(cursor.getInt(EventTable.COLUMN_INDEX.ID.getValue()), cursor.getInt(EventTable.COLUMN_INDEX.SIZE.getValue())); } } return uri; } else { log.warn(String.format("Event: '%s' failed to record to local database", StringUtil.clipString(event.getEventType(), CLIPPED_EVENT_LENGTH, true))); return null; } } JSONObject translateFromCursor(Cursor cursor) { try { return new JSONObject(cursor.getString(EventTable.COLUMN_INDEX.JSON.getValue())); } catch (final JSONException e) { log.error(String.format("Unable to format events")); } return null; } public void submitEvents() { submissionRunnableQueue.execute(new Runnable() { @Override public void run() { processEvents(); } }); } JSONArray getBatchOfEvents(Cursor cursor, List<Integer> idsToDeletes, List<Integer> sizeToDeletes) { final JSONArray eventArray = new JSONArray(); long currentRequestSize = 0; long eventLength; final long maxRequestSize = pinpointContext.getConfiguration().optLong( KEY_MAX_SUBMISSION_SIZE, DEFAULT_MAX_SUBMISSION_SIZE); JSONObject json = translateFromCursor(cursor); idsToDeletes.add(cursor.getInt(EventTable.COLUMN_INDEX.ID.getValue())); sizeToDeletes.add(cursor.getInt(EventTable.COLUMN_INDEX.ID.getValue())); if (json != null) { eventLength = json.length(); currentRequestSize += eventLength; eventArray.put(json); } while (cursor.moveToNext()) { json = translateFromCursor(cursor); idsToDeletes.add(cursor.getInt(EventTable.COLUMN_INDEX.ID.getValue())); sizeToDeletes.add(cursor.getInt(EventTable.COLUMN_INDEX.ID.getValue())); if (json != null) { eventLength = json.length(); currentRequestSize += eventLength; eventArray.put(json); if (currentRequestSize > maxRequestSize) { break; } } } return eventArray; } public List<JSONObject> getAllEvents() { final List<JSONObject> events = new ArrayList<JSONObject>(); final Cursor cursor = dbUtil.queryAllEvents(); while (cursor.moveToNext()) { events.add(translateFromCursor(cursor)); } cursor.close(); return events; } void processEvents() { final long start = System.currentTimeMillis(); final Cursor cursor = dbUtil.queryAllEvents(); final List<Integer> idsToDeletes = new ArrayList<Integer>(); final List<Integer> sizeToDeletes = new ArrayList<Integer>(); boolean successful; int submissions = 0; final long maxSubmissionsAllowed = pinpointContext.getConfiguration().optInt( KEY_MAX_SUBMISSIONS_ALLOWED, DEFAULT_MAX_SUBMISSIONS_ALLOWED); while (cursor.moveToNext()) { final List<Integer> batchIdsToDeletes = new ArrayList<Integer>(); final List<Integer> batchSizeToDeletes = new ArrayList<Integer>(); successful = submitEvents(this.getBatchOfEvents(cursor, batchIdsToDeletes, batchSizeToDeletes)); if (successful) { idsToDeletes.addAll(batchIdsToDeletes); sizeToDeletes.addAll(batchSizeToDeletes); submissions++; } if (submissions >= maxSubmissionsAllowed) { break; } } cursor.close(); if (sizeToDeletes.size() > 0) { for(int i = 0; i < sizeToDeletes.size(); i++) { try { dbUtil.deleteEvent(idsToDeletes.get(i), sizeToDeletes.get(i)); } catch (final Exception exc) { log.error("Failed to delete event: " + idsToDeletes.get(i), exc); } } } log.info(String.format("Time of attemptDelivery: %d", System.currentTimeMillis() - start)); } boolean submitEvents(final JSONArray eventArray) { boolean submitted = false; // package them into an ers request final PutEventsRequest request = this.createRecordEventsRequest(eventArray, pinpointContext.getNetworkType(), pinpointContext.getTargetingClient()); request.withClientContextEncoding("base64"); request.getRequestClientOptions().appendUserAgent(USER_AGENT); try { pinpointContext.getAnalyticsServiceClient().putEvents(request); submitted = true; log.info(String.format("Successful submission of %d events", eventArray.length())); return submitted; } catch (final AmazonServiceException e) { log.error("AmazonServiceException occured during send of put event ", e); final String errorCode = e.getErrorCode(); if (errorCode.equalsIgnoreCase("ValidationException") || errorCode.equalsIgnoreCase("SerializationException") || errorCode.equalsIgnoreCase("BadRequestException")) { submitted = true; log.error(String.format( "Failed to submit events to EventService: statusCode: " + e.getStatusCode() + " errorCode: ", errorCode)); log.error(String.format("Failed submission of %d events, events will be removed", eventArray.length()), e); return submitted; } else { log.warn( "Unable to successfully deliver events to server. Events will be saved, error likely recoverable. Response status code " + e.getStatusCode() + " , response error code " + e.getErrorCode() + e.getMessage()); log.warn("Recieved an error response: " + e.getMessage()); } } catch (final Exception e2) { log.warn("Unable to successfully deliver events to server. Events will be saved, error likely recoverable." + e2.getMessage()); } return submitted; } public PutEventsRequest createRecordEventsRequest(JSONArray events, String networkType, TargetingClient targetingClient) { if (events == null || events.length() == 0) { return null; } final PutEventsRequest putRequest = new PutEventsRequest(); final List<Event> eventList = new ArrayList<Event>(); ClientContext clientContext = null; for (int i = 0; i < events.length(); i++) { JSONObject eventJSON = null; AnalyticsEvent internalEvent = null; try { eventJSON = events.getJSONObject(i); internalEvent = AnalyticsEvent.translateToEvent(eventJSON); } catch (final JSONException e) { log.error("Stored event was invalid JSON", e); continue; } clientContext = internalEvent.createClientContext(networkType); //Add EndpointProfile profile to client pinpointContext if (targetingClient != null && targetingClient.currentEndpoint() != null) { final String endpoint = targetingClient.currentEndpoint().toJSONObject().toString(); final Map<String, String> customAttribute = new HashMap<String, String>(); customAttribute.put("endpoint", endpoint); clientContext.setCustom(customAttribute); log.info("Recorded profile to client pinpointContext: " + clientContext.toJSONObject()); } else { log.error("Event Client is null"); } final Event event = new Event(); final Session session = new Session(); session.withId(internalEvent.getSession().getSessionId()); session.withStartTimestamp(DateUtils.formatISO8601Date(new Date(internalEvent .getSession().getSessionStart()))); if (internalEvent.getSession().getSessionStop() != null && internalEvent.getSession().getSessionStop() != 0L) { session.withStopTimestamp(DateUtils.formatISO8601Date(new Date(internalEvent .getSession().getSessionStop()))); } if (internalEvent.getSession().getSessionDuration() != null && internalEvent.getSession().getSessionDuration() != 0L) { session.withDuration(internalEvent.getSession().getSessionDuration()); } event.withAttributes(internalEvent.getAllAttributes()) .withMetrics(internalEvent.getAllMetrics()) .withEventType(internalEvent.getEventType()) .withTimestamp( DateUtils.formatISO8601Date(new Date(internalEvent.getEventTimestamp()))) .withSession(session); eventList.add(event); } if (clientContext != null && eventList.size() > 0) { putRequest.withEvents(eventList).withClientContext( Base64.encodeAsString(clientContext.toJSONObject().toString().getBytes())); } else { log.error("ClientContext is null or event list is empty"); } return putRequest; } }