package com.mopub.nativeads;
import android.content.Context;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import com.mopub.common.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import static com.mopub.nativeads.VisibilityTracker.VisibilityChecker;
import static com.mopub.nativeads.VisibilityTracker.VisibilityTrackerListener;
class ImpressionTracker {
private static final int PERIOD = 250;
// Object tracking visibility of added views
@NonNull private final VisibilityTracker mVisibilityTracker;
// All views and responses being tracked for impressions
@NonNull private final Map<View, NativeResponse> mTrackedViews;
// Visible views being polled for time on screen before tracking impression
@NonNull private final Map<View, TimestampWrapper<NativeResponse>> mPollingViews;
// Handler for polling visible views
@NonNull private final Handler mPollHandler;
// Runnable to run on each visibility loop
@NonNull private final PollingRunnable mPollingRunnable;
// Object to check actual visibility
@NonNull private final VisibilityChecker mVisibilityChecker;
// Listener for when a view becomes visible or non visible
@Nullable private VisibilityTrackerListener mVisibilityTrackerListener;
ImpressionTracker(@NonNull final Context context) {
this(new WeakHashMap<View, NativeResponse>(),
new WeakHashMap<View, TimestampWrapper<NativeResponse>>(),
new VisibilityChecker(),
new VisibilityTracker(context),
new Handler());
}
@VisibleForTesting
ImpressionTracker(@NonNull final Map<View, NativeResponse> trackedViews,
@NonNull final Map<View, TimestampWrapper<NativeResponse>> pollingViews,
@NonNull final VisibilityChecker visibilityChecker,
@NonNull final VisibilityTracker visibilityTracker,
@NonNull final Handler handler) {
mTrackedViews = trackedViews;
mPollingViews = pollingViews;
mVisibilityChecker = visibilityChecker;
mVisibilityTracker = visibilityTracker;
mVisibilityTrackerListener = new VisibilityTrackerListener() {
@Override
public void onVisibilityChanged(@NonNull final List<View> visibleViews, @NonNull final List<View> invisibleViews) {
for (final View view : visibleViews) {
// It's possible for native response to be null if the view was GC'd from this class
// but not from VisibilityTracker
// If it's null then clean up the view from this class
final NativeResponse nativeResponse = mTrackedViews.get(view);
if (nativeResponse == null) {
removeView(view);
continue;
}
// If the native response is already polling, don't recreate it
final TimestampWrapper<NativeResponse> polling = mPollingViews.get(view);
if (polling != null && nativeResponse.equals(polling.mInstance)) {
continue;
}
// Add a new polling view
mPollingViews.put(view, new TimestampWrapper<NativeResponse>(nativeResponse));
}
for (final View view : invisibleViews) {
mPollingViews.remove(view);
}
scheduleNextPoll();
}
};
mVisibilityTracker.setVisibilityTrackerListener(mVisibilityTrackerListener);
mPollHandler = handler;
mPollingRunnable = new PollingRunnable();
}
/**
* Tracks the given view for impressions.
*/
void addView(final View view, @NonNull final NativeResponse nativeResponse) {
// View is already associated with same native response
if (mTrackedViews.get(view) == nativeResponse) {
return;
}
// Clean up state if view is being recycled and associated with a different response
removeView(view);
if (nativeResponse.getRecordedImpression() || nativeResponse.isDestroyed()) {
return;
}
mTrackedViews.put(view, nativeResponse);
mVisibilityTracker.addView(view, nativeResponse.getImpressionMinPercentageViewed());
}
void removeView(final View view) {
mTrackedViews.remove(view);
removePollingView(view);
mVisibilityTracker.removeView(view);
}
/**
* Immediately clear all views. Useful for when we re-request ads for an ad placer
*/
void clear() {
mTrackedViews.clear();
mPollingViews.clear();
mVisibilityTracker.clear();
mPollHandler.removeMessages(0);
}
void destroy() {
clear();
mVisibilityTracker.destroy();
mVisibilityTrackerListener = null;
}
@VisibleForTesting
void scheduleNextPoll() {
// Only schedule if there are no messages already scheduled.
if (mPollHandler.hasMessages(0)) {
return;
}
mPollHandler.postDelayed(mPollingRunnable, PERIOD);
}
private void removePollingView(final View view) {
mPollingViews.remove(view);
}
@VisibleForTesting
class PollingRunnable implements Runnable {
// Create this once to avoid excessive garbage collection observed when calculating
// these on each pass.
@NonNull private final ArrayList<View> mRemovedViews;
PollingRunnable() {
mRemovedViews = new ArrayList<View>();
}
@Override
public void run() {
for (final Map.Entry<View, TimestampWrapper<NativeResponse>> entry : mPollingViews.entrySet()) {
final View view = entry.getKey();
final TimestampWrapper<NativeResponse> timestampWrapper = entry.getValue();
// If it's been visible for the min impression time, trigger the callback
if (!mVisibilityChecker.hasRequiredTimeElapsed(
timestampWrapper.mCreatedTimestamp,
timestampWrapper.mInstance.getImpressionMinTimeViewed())) {
continue;
}
timestampWrapper.mInstance.recordImpression(view);
// Removed in a separate loop to avoid a ConcurrentModification exception.
mRemovedViews.add(view);
}
for (View view : mRemovedViews) {
removeView(view);
}
mRemovedViews.clear();
if (!mPollingViews.isEmpty()) {
scheduleNextPoll();
}
}
}
@Nullable
@Deprecated
@VisibleForTesting
VisibilityTrackerListener getVisibilityTrackerListener() {
return mVisibilityTrackerListener;
}
}