com.namomedia.android.AdPlacerImpl Maven / Gradle / Ivy
package com.namomedia.android;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.namomedia.android.volley.Response;
import com.namomedia.android.volley.VolleyError;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Implementation for NamoListAdapter interface.
*/
class AdPlacerImpl implements NamoAdPlacer {
private static final int NAMO_MAX_REDIRECTS = 20;
private final Context context;
private final BackendClient backendClient;
private final PositionAdjusterImpl adjuster;
private final WeakHashMap adDataWeakMap;
private final AdResponseListener adResponseListener;
private final Response.ErrorListener errorListener;
private final AdClickListener adClickListener;
private final String adUnitId;
private NamoAdViewBinder adViewBinder;
private NamoAdListener clientAdListener;
private final Set viewedAdIds;
private JsonRequest pendingRequest;
private boolean isInitialRequest;
private String continuationToken;
private NamoTargeting continuationTargeting;
private WebView hiddenWebView;
private final ViewabilityTracker viewabilityTracker;
AdPlacerImpl(Context context, PositionAdjusterImpl positionAdjuster,
String adUnitId,
BackendClient backendClient) {
this.context = context;
this.backendClient = backendClient;
this.adjuster = positionAdjuster;
this.adUnitId = adUnitId;
this.adViewBinder = new SimpleAdViewBinder(
context, R.layout.namo_ad_view_sample_1, "namo-ad-view-sample-1");
adDataWeakMap = new WeakHashMap(8, 0.75f);
viewedAdIds = new HashSet();
adResponseListener = new AdResponseListener();
errorListener = new AdResponseErrorListener();
adClickListener = new AdClickListener();
// Tracks views for impression purposes.
AdPlacerImpl.AdViewedListener adViewedListener = new AdPlacerImpl.AdViewedListener();
viewabilityTracker = new ViewabilityTracker(adViewedListener);
}
void clearPendingImpressions() {
viewabilityTracker.clearTracking();
}
@Override
public void registerAdViewBinder(NamoAdViewBinder adViewBinder) {
this.adViewBinder = adViewBinder;
}
@Override
public void setAdListener(NamoAdListener listener) {
this.clientAdListener = listener;
}
@Override
public void clearAdListener() {
this.clientAdListener = null;
}
@Override
public void requestAds() {
requestAds(/* targeting */ null);
}
@Override
public void requestAds(NamoTargeting targeting) {
// Drop any outstanding requests.
if (pendingRequest != null) {
pendingRequest.cancel();
pendingRequest = null;
}
isInitialRequest = true;
requestAdsInternal(targeting, /* continuation token */ null, /* Min ad position */ 0);
continuationTargeting = targeting;
}
private void requestMoreAds(int minAdPosition) {
// Make sure no requests are outstanding and that we have a valid continuation token.
if (pendingRequest != null || TextUtils.isEmpty(continuationToken)) {
return;
}
requestAdsInternal(continuationTargeting, continuationToken, minAdPosition);
}
private void requestAdsInternal(
NamoTargeting targeting, String continuationToken, int minAdPosition) {
// Don't allow a request while another is outstanding for this placer.
Preconditions.checkState(pendingRequest == null);
// The instance id is our best guess as to whether the user is in the same view.
long viewId = System.identityHashCode(this);
String formatId = adViewBinder.getFormatIdentifier();
pendingRequest = backendClient.requestAds(formatId, targeting, viewId, this.adUnitId,
continuationToken, minAdPosition,
adResponseListener, errorListener);
}
@Override
public NamoPositionAdjuster getPositionAdjuster() {
return adjuster;
}
@Override
public View placeAd(NamoAdData adData, View convertView, ViewGroup parent) {
Preconditions.checkNotNull(adData);
View view = convertView;
if (view == null) {
view = adViewBinder.createView(parent);
}
AdDataImpl adDataImpl = (AdDataImpl) adData;
AdDataImpl oldAdData = adDataWeakMap.get(view);
if (oldAdData != adDataImpl) {
adDataWeakMap.put(view, adDataImpl);
adViewBinder.bindAdData(view, adDataImpl);
// Must be set every time, since convertView might be a view that was used for non-ad content.
view.setOnClickListener(adClickListener);
final Map debugInfo = adDataImpl.getDebugInfo();
if (debugInfo != null) {
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
StringBuilder builder = new StringBuilder();
for (Map.Entry entry : debugInfo.entrySet()) {
builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}
Toast.makeText(context, builder.toString(), Toast.LENGTH_LONG).show();
return true;
}
});
}
// Since ad data is different, stop the current tracking.
viewabilityTracker.stopTrackingView(view);
}
// Make sure this view is being tracked. Needed even if the ad data is different, because the
// view tree observer for this view may have changed.
viewabilityTracker.trackView(view);
// Request more ads if needed.
NamoAdData[] ads = adjuster.getAds();
NamoAdData lastAd = (ads.length > 0) ? ads[ads.length - 1] : null;
NamoAdData secondLastAd = (ads.length > 1) ? ads[ads.length - 2] : null;
if (adData.equals(lastAd) || adData.equals(secondLastAd)) {
int minAdPosition = adjuster.getAdPositions()[ads.length - 1] + 1;
requestMoreAds(minAdPosition);
}
return view;
}
@Override
public void clearAd(View view) {
if (view == null) {
return;
}
// Immediately nullifies the the impression handler callback without waiting for GC.
AdDataImpl adData = adDataWeakMap.get(view);
if (adData != null) {
adDataWeakMap.remove(view);
}
view.setOnClickListener(null);
view.setOnLongClickListener(null);
}
private final class AdResponseErrorListener implements Response.ErrorListener {
@Override
public void onErrorResponse(VolleyError error) {
pendingRequest = null;
}
}
private final class AdResponseListener implements Response.Listener {
@Override
public void onResponse(AdResponse response) {
pendingRequest = null;
if (isInitialRequest) {
clearPendingImpressions();
adjuster.clearAds();
isInitialRequest = false;
}
adjuster.insertAds(response.ads, response.positioning.positions);
continuationToken = response.positioning.continuationToken;
if (clientAdListener != null) {
clientAdListener.onAdsLoaded(adjuster);
}
}
}
/**
* Checks to see if the URL conforms to known Play Store URL patterns. If it is a play.google.com
* url, it's transformed to the market:// scheme so that the Play Store app will be opened by
* the intent.
*
* @param url The url to parse.
* @return null if the url is not a market url, otherwise return the market url.
*/
private String parseMarketUrl(String url) {
if (url.startsWith("market://")) {
LogHelper.d("Market URL found: %s", url);
return url;
} else if (url.matches("^(http|https)://play.google.com.*$")) {
LogHelper.d("Play URL found: %s", url);
// Extract app ID from URL
Uri uri = Uri.parse(url);
String id = uri.getQueryParameter("id");
if (id != null && !id.equals("")) {
// Construct market URL
String marketUri = String.format("market://details?id=%s", id);
LogHelper.d("Play URL %s transformed to Market URL %s", url, marketUri);
return marketUri;
}
}
return null;
}
private final class AdViewedListener implements ViewabilityTracker.Listener {
@Override
public void onViewed(View view) {
AdDataImpl adData = adDataWeakMap.get(view);
if (adData == null || viewedAdIds.contains(adData.getId())) {
return;
}
// Log the impression.
viewedAdIds.add(adData.getId());
long viewId = System.identityHashCode(AdPlacerImpl.this);
backendClient.logImpression(adData, viewId);
}
}
private final class AdClickListener implements View.OnClickListener {
private int redirects = 0;
private ViewGroup viewGroup = null;
private View loadingView = null;
private DialogFragment dialogFragment;
private ProgressDialog progressDialog;
@Override
public void onClick(View view) {
AdDataImpl adData = adDataWeakMap.get(view);
if (adData == null) {
return;
}
long viewId = System.identityHashCode(AdPlacerImpl.this);
backendClient.logClick(adData, viewId);
String actionUrl = adData.getActionUrl();
if (actionUrl == null) {
LogHelper.w("Ad contained an empty action URL.");
return;
}
NamoActionType actionType = adData.getActionType();
if (actionType == NamoActionType.INSTALL) {
executeCPIAction(view, actionUrl);
} else if (actionType == NamoActionType.LINK) {
openUrlWithIntent(actionUrl);
} else if (actionType == NamoActionType.VIDEO) {
Intent intent = new Intent(context, NamoAdActivity.class);
intent.putExtra(NamoAdActivity.VIDEO_URL, actionUrl);
try {
context.startActivity(intent);
} catch (Exception e) {
LogHelper.wtf(e, "Couldn't start activity for URL: %s", actionUrl);
}
}
}
private void executeCPIAction(View view, final String actionUrl) {
showLoading(view);
// Load the CPI URL in a hidden WebView until we get to a "real" market URL.
if (hiddenWebView != null) {
hiddenWebView.stopLoading();
redirects = 0;
} else {
hiddenWebView = new WebView(context);
hiddenWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
String marketUrl = parseMarketUrl(url);
if (marketUrl == null) {
redirects += 1;
if (redirects > NAMO_MAX_REDIRECTS) {
LogHelper.w("Could not load CPI ad. Too many redirects. " +
"First URL was: %s, Last url was: %s", actionUrl, url);
// Try to open the URL as it is - probably a web browser.
openUrlWithIntent(url);
}
return super.shouldOverrideUrlLoading(view, url);
}
openUrlWithIntent(marketUrl);
return true;
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
LogHelper.w("Failed to load URL: %s. Original action URL was: %s",
failingUrl, actionUrl);
super.onReceivedError(view, errorCode, description, failingUrl);
}
});
hiddenWebView.setVisibility(View.INVISIBLE);
}
String url = parseMarketUrl(actionUrl);
if (url != null) {
openUrlWithIntent(url);
} else {
hiddenWebView.loadUrl(actionUrl);
}
}
private void openUrlWithIntent(String marketUrl) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(marketUrl));
try {
context.startActivity(intent);
} catch (Exception e) {
LogHelper.wtf(e, "Couldn't start activity for URL: %s", marketUrl);
}
hideLoading();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private void showLoading(View view) {
// Hide any previous loading indicator.
hideLoading();
// If we have a suitable layout, show the loading view
if (view instanceof FrameLayout || view instanceof RelativeLayout) {
viewGroup = (ViewGroup) view;
LayoutInflater inflater =(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
loadingView = inflater.inflate(R.layout.loading_view, null);
loadingView.setLayoutParams(new ViewGroup.LayoutParams(
view.getWidth(), view.getHeight()));
viewGroup.addView(loadingView);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { // Fragment support.
Activity activity = (Activity) view.getContext();
FragmentManager fragmentManager = activity.getFragmentManager();
// Launch our dialog fragment:
dialogFragment = new SDKProgressDialog();
dialogFragment.show(fragmentManager, "namo_progress_dialog");
} else { // No fragment support :(
// Show a progress dialog
viewGroup = null;
progressDialog = new ProgressDialog(context);
progressDialog.setTitle("Loading...");
progressDialog.setMessage("Opening Google Play");
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.show();
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private void hideLoading() {
// Hide loading indicator if it's showing.
if (viewGroup != null) {
if (loadingView != null) {
viewGroup.removeView(loadingView);
}
}
if (progressDialog != null) {
try {
// This can throw exceptions after orientation change!
progressDialog.dismiss();
} catch (Exception e) {
// Do nothing!
}
}
if (dialogFragment != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
dialogFragment.dismissAllowingStateLoss();
}
viewGroup = null;
loadingView = null;
progressDialog = null;
dialogFragment = null;
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public static class SDKProgressDialog extends DialogFragment {
public SDKProgressDialog() {
// Empty constructor required by super class
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final ProgressDialog progressDialog = new ProgressDialog(getActivity());
progressDialog.setTitle("Loading...");
progressDialog.setMessage("Opening Google Play");
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
return progressDialog;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy