From b7650c297ca90b7c4f395c7d5623d448d7f78e15 Mon Sep 17 00:00:00 2001 From: alexzatsepin Date: Fri, 24 Mar 2017 14:21:08 +0300 Subject: [PATCH] [android] Added the ad lifecycle tracker --- .../com/mapswithme/maps/ads/AdTracker.java | 8 +- .../mapswithme/maps/ads/DefaultAdTracker.java | 125 ++++++++++++++++++ .../maps/ads/FacebookAdsLoader.java | 26 ++-- .../maps/ads/OnAdCacheModifiedListener.java | 2 +- .../widget/placepage/BannerController.java | 31 +++-- 5 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 android/src/com/mapswithme/maps/ads/DefaultAdTracker.java diff --git a/android/src/com/mapswithme/maps/ads/AdTracker.java b/android/src/com/mapswithme/maps/ads/AdTracker.java index 5f18f85e44..fb02094816 100644 --- a/android/src/com/mapswithme/maps/ads/AdTracker.java +++ b/android/src/com/mapswithme/maps/ads/AdTracker.java @@ -2,8 +2,10 @@ package com.mapswithme.maps.ads; import android.support.annotation.NonNull; -interface AdTracker +public interface AdTracker { - void start(@NonNull String bannerId); - void stop(@NonNull String bannerId); + void onViewShown(@NonNull String bannerId); + void onViewHidden(@NonNull String bannerId); + void onContentObtained(@NonNull String bannerId); + boolean isImpressionGood(@NonNull String bannerId); } diff --git a/android/src/com/mapswithme/maps/ads/DefaultAdTracker.java b/android/src/com/mapswithme/maps/ads/DefaultAdTracker.java new file mode 100644 index 0000000000..703954506a --- /dev/null +++ b/android/src/com/mapswithme/maps/ads/DefaultAdTracker.java @@ -0,0 +1,125 @@ +package com.mapswithme.maps.ads; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import com.mapswithme.util.log.Logger; +import com.mapswithme.util.log.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class DefaultAdTracker implements AdTracker, OnAdCacheModifiedListener +{ + private final static Logger LOGGER = LoggerFactory.INSTANCE.getLogger(LoggerFactory.Type.MISC); + private final static String TAG = DefaultAdTracker.class.getSimpleName(); + private final static int GOOD_IMPRESSION_TIME_MS = 2000; + private final Map mTracks = new HashMap<>(); + + @Override + public void onViewShown(@NonNull String bannerId) + { + LOGGER.d(TAG, "onViewShown bannerId = " + bannerId); + TrackInfo info = mTracks.get(bannerId); + if (info != null) + info.setVisible(true); + } + + @Override + public void onViewHidden(@NonNull String bannerId) + { + LOGGER.d(TAG, "onViewHidden bannerId = " + bannerId); + TrackInfo info = mTracks.get(bannerId); + if (info != null) + info.setVisible(false); + } + + @Override + public void onContentObtained(@NonNull String bannerId) + { + LOGGER.d(TAG, "onContentObtained bannerId = " + bannerId); + TrackInfo info = mTracks.get(bannerId); + if (info == null) + throw new AssertionError("A track info must be put in a cache before a content is obtained"); + + info.fill(); + } + + @Override + public boolean isImpressionGood(@NonNull String bannerId) + { + TrackInfo info = mTracks.get(bannerId); + return info != null && info.getShowTime() > GOOD_IMPRESSION_TIME_MS; + } + + @Override + public void onRemoved(@NonNull String id) + { + LOGGER.d(TAG, "onRemoved id = " + id); + mTracks.remove(id); + } + + @Override + public void onPut(@NonNull String id) + { + LOGGER.d(TAG, "onPut id = " + id); + mTracks.put(id, new TrackInfo()); + } + + private static class TrackInfo + { + private long mTimestamp; + /** + * Accumulates amount of time that ad is already shown. + */ + private long mShowTime; + /** + * Indicates whether the ad view is visible or not. + */ + private boolean mVisible; + /** + * Indicates whether the ad content is obtained or not. + */ + private boolean mFilled; + + public void setVisible(boolean visible) + { + // No need tracking if the ad is not filled with a content + if (!mFilled) + return; + + // If ad becomes visible, and it's filled with a content the timestamp must be stored. + if (visible && !mVisible) + { + mTimestamp = SystemClock.elapsedRealtime(); + } + // If ad is hidden the show time must be accumulated. + else if (!visible && mVisible) + { + mShowTime += SystemClock.elapsedRealtime() - mTimestamp; + mTimestamp = 0; + } + mVisible = visible; + } + + public void fill() + { + // If the visible ad is filled with the content the timestamp must be stored + if (mVisible) + { + mTimestamp = SystemClock.elapsedRealtime(); + } + mFilled = true; + } + + private boolean isRealShow() + { + return mFilled && mVisible; + } + + long getShowTime() + { + return mShowTime; + } + } +} diff --git a/android/src/com/mapswithme/maps/ads/FacebookAdsLoader.java b/android/src/com/mapswithme/maps/ads/FacebookAdsLoader.java index 14794c4402..db0fe081c3 100644 --- a/android/src/com/mapswithme/maps/ads/FacebookAdsLoader.java +++ b/android/src/com/mapswithme/maps/ads/FacebookAdsLoader.java @@ -35,16 +35,19 @@ public class FacebookAdsLoader implements AdListener /** * Loads an ad for a specified placement id. If there is a cached ad, and it's not expired, - * that ad will be returned immediately. Otherwise, this method returns null, and {@link #mAdsListener} will - * be notified when the requested ad is loaded. + * the caller will be notified immediately through {@link FacebookAdsListener#onFacebookAdLoaded(NativeAd)}. + * Otherwise, the caller will be notified once an ad is loaded through the mentioned method. + * + *

Important note: if there is a cached ad for the requested placement id, and that ad + * has a good impression indicator, and there is at least {@link #REQUEST_INTERVAL_MS} between the + * first time that ad was requested and the current time the new ad will be loaded. * * @param context An activity context. * @param placementId A placement id that ad will be loaded for. - * @return A cached banner if it presents, otherwise null + * @param tracker An ad tracker */ - @Nullable @UiThread - public NativeAd load(@NonNull Context context, @NonNull String placementId) + public void load(@NonNull Context context, @NonNull String placementId, @NonNull AdTracker tracker) { LOGGER.d(TAG, "Load a facebook ad for a placement id '" + placementId + "'"); @@ -54,17 +57,18 @@ public class FacebookAdsLoader implements AdListener { LOGGER.d(TAG, "There is no an ad in a cache"); loadAdInternal(context, placementId); - return null; + return; } - if (/** Tracker.checkShowTime(placmenetId) &&**/ - SystemClock.elapsedRealtime() - cachedAd.getLoadedTime() >= REQUEST_INTERVAL_MS) + if (tracker.isImpressionGood(placementId) + && SystemClock.elapsedRealtime() - cachedAd.getLoadedTime() >= REQUEST_INTERVAL_MS) { - LOGGER.d(TAG, "Ad should be reloaded"); + LOGGER.d(TAG, "A new ad will be loaded because the previous one has a good impression"); loadAdInternal(context, placementId); } - return cachedAd.getAd(); + if (mAdsListener != null) + mAdsListener.onFacebookAdLoaded(cachedAd.getAd()); } /** @@ -168,7 +172,7 @@ public class FacebookAdsLoader implements AdListener { mCache.remove(key); if (mCacheListener != null) - mCacheListener.onRemove(key); + mCacheListener.onRemoved(key); } public interface FacebookAdsListener diff --git a/android/src/com/mapswithme/maps/ads/OnAdCacheModifiedListener.java b/android/src/com/mapswithme/maps/ads/OnAdCacheModifiedListener.java index e81b5d8aa6..4792c8c73d 100644 --- a/android/src/com/mapswithme/maps/ads/OnAdCacheModifiedListener.java +++ b/android/src/com/mapswithme/maps/ads/OnAdCacheModifiedListener.java @@ -4,6 +4,6 @@ import android.support.annotation.NonNull; interface OnAdCacheModifiedListener { - void onRemove(@NonNull String id); + void onRemoved(@NonNull String id); void onPut(@NonNull String id); } diff --git a/android/src/com/mapswithme/maps/widget/placepage/BannerController.java b/android/src/com/mapswithme/maps/widget/placepage/BannerController.java index 98ef81ff26..b8be02d496 100644 --- a/android/src/com/mapswithme/maps/widget/placepage/BannerController.java +++ b/android/src/com/mapswithme/maps/widget/placepage/BannerController.java @@ -14,6 +14,7 @@ import com.facebook.ads.Ad; import com.facebook.ads.AdError; import com.facebook.ads.NativeAd; import com.mapswithme.maps.R; +import com.mapswithme.maps.ads.DefaultAdTracker; import com.mapswithme.maps.ads.FacebookAdsLoader; import com.mapswithme.maps.bookmarks.data.Banner; import com.mapswithme.util.Config; @@ -74,6 +75,8 @@ final class BannerController @NonNull private final FacebookAdsLoader mAdsLoader; + @NonNull + private final DefaultAdTracker mAdTracker; BannerController(@NonNull View bannerView, @Nullable BannerListener listener) { @@ -90,6 +93,8 @@ final class BannerController //TODO: pass as constructor arguments mAdsLoader = new FacebookAdsLoader(); mAdsLoader.setAdsListener(new NativeAdsListener()); + mAdTracker = new DefaultAdTracker(); + mAdsLoader.setCacheListener(mAdTracker); } private void setErrorStatus(boolean value) @@ -141,19 +146,7 @@ final class BannerController UiUtils.show(mFrame); - NativeAd data = mAdsLoader.load(mFrame.getContext(), mBanner.getId()); - updateVisibility(); - - if (data != null) - { - LOGGER.d(TAG, "A cached ad '" + mBanner + "' is shown"); - fillViews(data); - registerViewsForInteraction(data); - loadIconAndOpenIfNeeded(data, mBanner); - } - - if (mOpened && mListener != null) - mListener.onSizeChanged(); + mAdsLoader.load(mFrame.getContext(), mBanner.getId(), mAdTracker); } boolean isBannerVisible() @@ -213,6 +206,16 @@ final class BannerController private void onChangedVisibility(@NonNull Banner banner, boolean isVisible) { + if (TextUtils.isEmpty(banner.getId())) + { + LOGGER.e(TAG, "Banner must have a non-null id!", new Throwable()); + return; + } + + if (isVisible) + mAdTracker.onViewShown(banner.getId()); + else + mAdTracker.onViewHidden(banner.getId()); } void onChangedVisibility(boolean isVisible) @@ -305,6 +308,8 @@ final class BannerController loadIconAndOpenIfNeeded(ad, mBanner); + mAdTracker.onContentObtained(ad.getPlacementId()); + if (mListener != null && mOpened) mListener.onSizeChanged(); }