package net.hockeyapp.android.metrics; import net.hockeyapp.android.metrics.model.Base; import net.hockeyapp.android.metrics.model.Data; import net.hockeyapp.android.metrics.model.Domain; import net.hockeyapp.android.metrics.model.Envelope; import net.hockeyapp.android.metrics.model.TelemetryData; import net.hockeyapp.android.utils.HockeyLog; import net.hockeyapp.android.utils.Util; import java.io.IOException; import java.io.StringWriter; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; /** * <h3>Description</h3> * * Items get queued before they are persisted and sent out as a batch to save battery. This class * manages the queue, and forwards the batch to the persistence layer once the max batch count or * batch interval time limit has been reached. **/ class Channel { private static final String TAG = "HockeyApp-Metrics"; /** * Synchronization lock. */ private static final Object LOCK = new Object(); /** * Number of queue items which will trigger synchronization with the persistence layer. */ protected static int mMaxBatchCount = 50; /** * Maximum time interval in milliseconds after which a synchronize will be triggered, regardless of queue size. */ protected static int mMaxBatchInterval = 15 * 1000; /** * The backing store queue for the channel. */ protected final List<String> mQueue; /** * Telemetry context used by the channel to create the payload. */ protected final TelemetryContext mTelemetryContext; /** * Persistence used for storing telemetry items before they get sent. */ private final Persistence mPersistence; /** * Timer to run scheduled tasks on. */ private final Timer mTimer; /** * Task to be scheduled for synchronizing at a certain max interval. */ private SynchronizeChannelTask mSynchronizeTask; /** * Creates and initializes a new instance. */ public Channel(TelemetryContext telemetryContext, Persistence persistence) { mTelemetryContext = telemetryContext; mQueue = new LinkedList<>(); mPersistence = persistence; mTimer = new Timer("HockeyApp User Metrics Sender Queue", true); } /** * Adds an item to the channel queue. * * @param serializedItem A serialized telemetry item to enqueue. */ protected void enqueue(String serializedItem) { if (serializedItem == null) { return; } synchronized (LOCK) { if (mQueue.add(serializedItem)) { if ((mQueue.size() >= mMaxBatchCount)) { synchronize(); } else if (mQueue.size() == 1) { scheduleSynchronizeTask(); } } else { HockeyLog.verbose(TAG, "Unable to add item to queue"); } } } /** * Synchronize all pending telemetry items with persistence. */ protected void synchronize() { if (mSynchronizeTask != null) { mSynchronizeTask.cancel(); } String[] data; if (!mQueue.isEmpty()) { data = new String[mQueue.size()]; mQueue.toArray(data); mQueue.clear(); if (mPersistence != null) { mPersistence.persist(data); } } } /** * Create a telemetry envelope with the given object as its base data. * * @param data The telemetry we want to wrap inside an Envelope and send to the server. * @return The envelope that includes the telemetry data. */ protected Envelope createEnvelope(Data<Domain> data) { Envelope envelope = new Envelope(); envelope.setData(data); Domain baseData = data.getBaseData(); if (baseData instanceof TelemetryData) { String envelopeName = ((TelemetryData) baseData).getEnvelopeName(); envelope.setName(envelopeName); } mTelemetryContext.updateScreenResolution(); envelope.setTime(Util.dateToISO8601(new Date())); envelope.setIKey(mTelemetryContext.getInstrumentationKey()); Map<String, String> tags = mTelemetryContext.getContextTags(); if (tags != null) { envelope.setTags(tags); } return envelope; } protected void scheduleSynchronizeTask() { mSynchronizeTask = new SynchronizeChannelTask(); mTimer.schedule(mSynchronizeTask, mMaxBatchInterval); } /** * Enqueue data in the channel queue. * * @param data The base data object to enqueue. */ @SuppressWarnings("unchecked") public void enqueueData(Base data) { if (data instanceof Data) { Envelope envelope = null; try { envelope = createEnvelope((Data<Domain>) data); } catch (ClassCastException e) { HockeyLog.debug(TAG, "Telemetry not enqueued, could not create envelope, must be of type ITelemetry"); } if (envelope != null) { // enqueueData to queue String serializedEnvelope = serializeEnvelope(envelope); enqueue(serializedEnvelope); HockeyLog.debug(TAG, "enqueued telemetry: " + envelope.getName()); } } else { HockeyLog.debug(TAG, "Telemetry not enqueued, must be of type ITelemetry"); } } /** * Serializes an envelope to a JSON string according to Common Schema. * * @param envelope The envelope object to serialize. */ protected String serializeEnvelope(Envelope envelope) { try { if (envelope != null) { StringWriter stringWriter = new StringWriter(); envelope.serialize(stringWriter); return stringWriter.toString(); } HockeyLog.debug(TAG, "Envelope wasn't empty but failed to serialize anything, returning null"); return null; } catch (IOException e) { HockeyLog.debug(TAG, "Failed to save data with exception: " + e.toString()); return null; } } /** * Task to fire off after batch time interval has passed. */ private class SynchronizeChannelTask extends TimerTask { public SynchronizeChannelTask() { } @Override public void run() { synchronize(); } } }