All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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