[android] Added visibility listener mechanism for tracking ads in PP

This commit is contained in:
alexzatsepin 2017-03-23 20:26:04 +03:00 committed by Vladimir Byko-Ianko
parent e996549c5e
commit 2e0077ac23
8 changed files with 236 additions and 95 deletions

View file

@ -999,6 +999,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
mNavigationController.onResume();
if (mNavAnimationController != null)
mNavAnimationController.onResume();
mPlacePage.onActivityResume();
}
@Override
@ -1041,6 +1042,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
TtsPlayer.INSTANCE.stop();
LikesManager.INSTANCE.cancelDialogs();
mOnmapDownloader.onPause();
mPlacePage.onActivityPause();
super.onPause();
}

View file

@ -0,0 +1,9 @@
package com.mapswithme.maps.ads;
import android.support.annotation.NonNull;
interface AdTracker
{
void start(@NonNull String bannerId);
void stop(@NonNull String bannerId);
}

View file

@ -2,7 +2,6 @@ package com.mapswithme.maps.ads;
import android.content.Context;
import android.os.SystemClock;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
@ -22,57 +21,47 @@ import java.util.Map;
import java.util.Set;
@NotThreadSafe
public class FacebookAdsLoader
public class FacebookAdsLoader implements AdListener
{
private static final Logger LOGGER = LoggerFactory.INSTANCE.getLogger(LoggerFactory.Type.MISC);
private static final String TAG = FacebookAdsLoader.class.getSimpleName();
private static final long EXPIRATION_TIME_MS = 60 * 60 * 1000;
private static final Map<String, FacebookAd> CACHE = new HashMap<>();
private static final Set<String> PENDING_REQUESTS = new HashSet<>();
private static final long REQUEST_INTERVAL_MS = 30 * 1000;
private final Map<String, FacebookAd> mCache = new HashMap<>();
private final Set<String> mPendingRequests = new HashSet<>();
@Nullable
private FacebookAdsListener mAdsListener;
@Nullable
private OnAdCacheModifiedListener mCacheListener;
/**
* 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 a listener will
* be notified when a requested ad is loaded.
* that ad will be returned immediately. Otherwise, this method returns null, and {@link #mAdsListener} will
* be notified when the requested ad is loaded.
*
* @param context An activity context.
* @param placementId A placement id that ad will be loaded for.
* @param listener A listener to be notified when an ad is loaded.
* @return A cached banner if it presents, otherwise <code>null</code>
*/
@Nullable
@UiThread
public NativeAd load(@NonNull Context context, @NonNull String placementId,
@NonNull FacebookAdsListener listener)
public NativeAd load(@NonNull Context context, @NonNull String placementId)
{
LOGGER.d(TAG, "Load a facebook ad for a placement id '" + placementId + "'");
if (!PENDING_REQUESTS.contains(placementId))
{
NativeAd ad = new NativeAd(context, placementId);
ad.setAdListener(listener);
ad.loadAd(EnumSet.of(NativeAd.MediaCacheFlag.ICON));
LOGGER.d(TAG, "Loading is started");
PENDING_REQUESTS.add(placementId);
} else
{
LOGGER.d(TAG, "The ad request for placement id '" + placementId + "' hasn't been completed yet.");
}
FacebookAd cachedAd = CACHE.get(placementId);
FacebookAd cachedAd = mCache.get(placementId);
if (cachedAd == null)
{
LOGGER.d(TAG, "There is no an ad in a cache for a placement id '" + placementId + "'");
LOGGER.d(TAG, "There is no an ad in a cache");
loadAdInternal(context, placementId);
return null;
}
long cacheTime = SystemClock.elapsedRealtime() - cachedAd.getLoadedTime();
if (cacheTime >= EXPIRATION_TIME_MS)
if (/** Tracker.checkShowTime(placmenetId) &&**/
SystemClock.elapsedRealtime() - cachedAd.getLoadedTime() >= REQUEST_INTERVAL_MS)
{
LOGGER.d(TAG, "A facebook ad is expired for a placement id '" + placementId + "'. " +
"An expiration time is " + (cacheTime - EXPIRATION_TIME_MS) + " ms");
CACHE.remove(placementId);
return null;
LOGGER.d(TAG, "Ad should be reloaded");
loadAdInternal(context, placementId);
}
return cachedAd.getAd();
@ -84,9 +73,9 @@ public class FacebookAdsLoader
* @param placementId A placement id that an ad is loading for.
* @return true if an ad is loading, otherwise - false.
*/
public static boolean isAdLoadingForId(@NonNull String placementId)
public boolean isAdLoadingForId(@NonNull String placementId)
{
return PENDING_REQUESTS.contains(placementId);
return mPendingRequests.contains(placementId);
}
/**
@ -95,9 +84,9 @@ public class FacebookAdsLoader
* @return A cached ad or <code>null</code> if it doesn't exist.
*/
@Nullable
public static NativeAd getAdByIdFromCache(@NonNull String placementId)
public NativeAd getAdByIdFromCache(@NonNull String placementId)
{
FacebookAd ad = CACHE.get(placementId);
FacebookAd ad = mCache.get(placementId);
return ad != null ? ad.getAd() : null;
}
@ -106,38 +95,84 @@ public class FacebookAdsLoader
*
* @return <code>true</code> if there is a cached ad, otherwise - <code>false</code>
*/
public static boolean isCacheEmptyForId(@NonNull String placementId)
public boolean isCacheEmptyForId(@NonNull String placementId)
{
return getAdByIdFromCache(placementId) == null;
}
public abstract static class FacebookAdsListener implements AdListener
@Override
public void onError(Ad ad, AdError adError)
{
@Override
public final void onAdLoaded(Ad ad)
LOGGER.w(TAG, "A error '" + adError + "' is occurred while loading an ad for placement id " +
"'" + ad.getPlacementId() + "'");
mPendingRequests.remove(ad.getPlacementId());
if (mAdsListener != null)
mAdsListener.onError(ad, adError, isCacheEmptyForId(ad.getPlacementId()));
}
@Override
public void onAdLoaded(Ad ad)
{
LOGGER.d(TAG, "An ad for id '" + ad.getPlacementId() + "' is loaded");
mPendingRequests.remove(ad.getPlacementId());
boolean isCacheWasEmpty = isCacheEmptyForId(ad.getPlacementId());
LOGGER.d(TAG, "Put a facebook ad to cache");
putInCache(ad.getPlacementId(), new FacebookAd((NativeAd)ad, SystemClock.elapsedRealtime()));
if (isCacheWasEmpty && mAdsListener != null)
mAdsListener.onFacebookAdLoaded((NativeAd) ad);
}
@Override
public void onAdClicked(Ad ad)
{
if (mAdsListener != null)
mAdsListener.onAdClicked(ad);
}
public void setAdsListener(@Nullable FacebookAdsListener adsListener)
{
mAdsListener = adsListener;
}
public void setCacheListener(@Nullable OnAdCacheModifiedListener cacheListener)
{
mCacheListener = cacheListener;
}
private void loadAdInternal(@NonNull Context context, @NonNull String placementId)
{
if (mPendingRequests.contains(placementId))
{
LOGGER.d(TAG, "An ad for id '" + ad.getPlacementId() + "' is loaded");
PENDING_REQUESTS.remove(ad.getPlacementId());
boolean isCacheWasEmpty = isCacheEmptyForId(ad.getPlacementId());
LOGGER.d(TAG, "Put a facebook ad to cache for a placement id '" + ad.getPlacementId());
CACHE.put(ad.getPlacementId(), new FacebookAd((NativeAd)ad, SystemClock.elapsedRealtime()));
if (isCacheWasEmpty)
onFacebookAdLoaded((NativeAd) ad);
LOGGER.d(TAG, "The ad request for placement id '" + placementId + "' hasn't been completed yet.");
return;
}
@Override
@CallSuper
public final void onError(Ad ad, AdError adError)
{
LOGGER.w(TAG, "A error '" + adError + "' is occurred while loading an ad for placement id " +
"'" + ad.getPlacementId() + "'");
PENDING_REQUESTS.remove(ad.getPlacementId());
onError(ad, adError, isCacheEmptyForId(ad.getPlacementId()));
}
NativeAd ad = new NativeAd(context, placementId);
ad.setAdListener(this);
LOGGER.d(TAG, "Loading is started");
ad.loadAd(EnumSet.of(NativeAd.MediaCacheFlag.ICON));
mPendingRequests.add(placementId);
}
private void putInCache(@NonNull String key, @NonNull FacebookAd value)
{
mCache.put(key, value);
if (mCacheListener != null)
mCacheListener.onPut(key);
}
private void removeFromCache(@NonNull String key, @NonNull FacebookAd value)
{
mCache.remove(key);
if (mCacheListener != null)
mCacheListener.onRemove(key);
}
public interface FacebookAdsListener
{
/**
* Called <b>only if</b> there is no a cached ad for a corresponding placement id
* contained within a downloaded ad object, i.e. {@link NativeAd#getPlacementId()}.
@ -147,7 +182,7 @@ public class FacebookAdsLoader
* @param ad A downloaded ad.
*/
@UiThread
protected abstract void onFacebookAdLoaded(@NonNull NativeAd ad);
void onFacebookAdLoaded(@NonNull NativeAd ad);
/**
* Notifies about a error occurred while loading the ad.
@ -156,7 +191,10 @@ public class FacebookAdsLoader
* otherwise - false
*/
@UiThread
protected abstract void onError(@NonNull Ad ad, @NonNull AdError adError, boolean isCacheEmpty);
void onError(@NonNull Ad ad, @NonNull AdError adError, boolean isCacheEmpty);
@UiThread
void onAdClicked(@NonNull Ad ad);
}
private static class FacebookAd

View file

@ -0,0 +1,9 @@
package com.mapswithme.maps.ads;
import android.support.annotation.NonNull;
interface OnAdCacheModifiedListener
{
void onRemove(@NonNull String id);
void onPut(@NonNull String id);
}

View file

@ -61,4 +61,21 @@ public final class Banner implements Parcelable
"mId='" + mId + '\'' +
'}';
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Banner banner = (Banner) o;
return mId != null ? mId.equals(banner.mId) : banner.mId == null;
}
@Override
public int hashCode()
{
return mId != null ? mId.hashCode() : 0;
}
}

View file

@ -30,7 +30,7 @@ import static com.mapswithme.util.SharedPropertiesUtils.isShowcaseSwitchedOnLoca
import static com.mapswithme.util.statistics.Statistics.EventName.PP_BANNER_CLICK;
import static com.mapswithme.util.statistics.Statistics.EventName.PP_BANNER_SHOW;
final class BannerController extends FacebookAdsLoader.FacebookAdsListener
final class BannerController
{
private static final Logger LOGGER = LoggerFactory.INSTANCE
.getLogger(LoggerFactory.Type.MISC);
@ -89,6 +89,7 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
mAds = bannerView.findViewById(R.id.tv__ads);
//TODO: pass as constructor arguments
mAdsLoader = new FacebookAdsLoader();
mAdsLoader.setAdsListener(new NativeAdsListener());
}
private void setErrorStatus(boolean value)
@ -111,6 +112,7 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
&& mAdsLoader.isCacheEmptyForId(mBanner.getId()))
{
UiUtils.hide(mIcon, mTitle, mMessage, mActionSmall, mActionLarge, mAds);
onChangedVisibility(mBanner, false);
}
else
{
@ -124,6 +126,9 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
void updateData(@Nullable Banner banner)
{
if (mBanner != null && !mBanner.equals(banner))
onChangedVisibility(mBanner, false);
UiUtils.hide(mFrame);
setErrorStatus(false);
@ -136,7 +141,9 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
UiUtils.show(mFrame);
NativeAd data = mAdsLoader.load(mFrame.getContext(), mBanner.getId(), this);
NativeAd data = mAdsLoader.load(mFrame.getContext(), mBanner.getId());
updateVisibility();
if (data != null)
{
LOGGER.d(TAG, "A cached ad '" + mBanner + "' is shown");
@ -145,8 +152,6 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
loadIconAndOpenIfNeeded(data, mBanner);
}
updateVisibility();
if (mOpened && mListener != null)
mListener.onSizeChanged();
}
@ -206,33 +211,14 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
NativeAd.downloadAndDisplayImage(icon, mIcon);
}
@Override
public void onError(@NonNull Ad ad, @NonNull AdError adError, boolean isCacheEmpty)
private void onChangedVisibility(@NonNull Banner banner, boolean isVisible)
{
setErrorStatus(isCacheEmpty);
updateVisibility();
if (mListener != null && isCacheEmpty)
mListener.onSizeChanged();
Statistics.INSTANCE.trackFacebookBannerError(mBanner, adError, mOpened ? 1 : 0);
}
@Override
public void onFacebookAdLoaded(@NonNull NativeAd ad)
void onChangedVisibility(boolean isVisible)
{
if (mBanner == null)
return;
updateVisibility();
fillViews(ad);
registerViewsForInteraction(ad);
loadIconAndOpenIfNeeded(ad, mBanner);
if (mListener != null && mOpened)
mListener.onSizeChanged();
if (mBanner != null)
onChangedVisibility(mBanner, isVisible);
}
private void registerViewsForInteraction(@NonNull NativeAd ad)
@ -272,15 +258,6 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
}
}
@Override
public void onAdClicked(Ad ad)
{
if (mBanner == null)
return;
Statistics.INSTANCE.trackFacebookBanner(PP_BANNER_CLICK, mBanner, mOpened ? 1 : 0);
}
boolean isActionButtonTouched(@NonNull MotionEvent event)
{
return isTouched(mActionSmall, event) || isTouched(mActionLarge, event)
@ -291,4 +268,45 @@ final class BannerController extends FacebookAdsLoader.FacebookAdsListener
{
void onSizeChanged();
}
private class NativeAdsListener implements FacebookAdsLoader.FacebookAdsListener
{
@Override
public void onAdClicked(@NonNull Ad ad)
{
if (mBanner == null)
return;
Statistics.INSTANCE.trackFacebookBanner(PP_BANNER_CLICK, mBanner, mOpened ? 1 : 0);
}
@Override
public void onError(@NonNull Ad ad, @NonNull AdError adError, boolean isCacheEmpty)
{
setErrorStatus(isCacheEmpty);
updateVisibility();
if (mListener != null && isCacheEmpty)
mListener.onSizeChanged();
Statistics.INSTANCE.trackFacebookBannerError(mBanner, adError, mOpened ? 1 : 0);
}
@Override
public void onFacebookAdLoaded(@NonNull NativeAd ad)
{
if (mBanner == null)
return;
updateVisibility();
fillViews(ad);
registerViewsForInteraction(ad);
loadIconAndOpenIfNeeded(ad, mBanner);
if (mListener != null && mOpened)
mListener.onSizeChanged();
}
}
}

View file

@ -281,6 +281,19 @@ public class PlacePageView extends RelativeLayout
return UiUtils.isViewTouched(event, mHotelGallery);
}
public void onActivityResume()
{
if (mBannerController != null)
mBannerController.onChangedVisibility(true);
}
public void onActivityPause()
{
if (mBannerController != null)
mBannerController.onChangedVisibility(false);
}
private void initViews()
{
LayoutInflater.from(getContext()).inflate(R.layout.place_page, this);
@ -1431,7 +1444,7 @@ public class PlacePageView extends RelativeLayout
public void setOnVisibilityChangedListener(BasePlacePageAnimationController.OnVisibilityChangedListener listener)
{
mAnimationController.setOnVisibilityChangedListener(listener);
mAnimationController.setOnVisibilityChangedListener(new PlacePageVisibilityProxy(listener, mBannerController));
}
public void setOnAnimationListener(@Nullable BasePlacePageAnimationController.OnAnimationListener listener)

View file

@ -0,0 +1,35 @@
package com.mapswithme.maps.widget.placepage;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class PlacePageVisibilityProxy implements BasePlacePageAnimationController.OnVisibilityChangedListener
{
@NonNull
private final BasePlacePageAnimationController.OnVisibilityChangedListener mListener;
@Nullable
private final BannerController mBannerController;
public PlacePageVisibilityProxy(@NonNull BasePlacePageAnimationController.OnVisibilityChangedListener listener,
@Nullable BannerController bannerController)
{
mListener = listener;
mBannerController = bannerController;
}
@Override
public void onPreviewVisibilityChanged(boolean isVisible)
{
if (mBannerController != null)
mBannerController.onChangedVisibility(isVisible);
mListener.onPreviewVisibilityChanged(isVisible);
}
@Override
public void onPlacePageVisibilityChanged(boolean isVisible)
{
mListener.onPlacePageVisibilityChanged(isVisible);
}
}