package com.mopub.mobileads;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import com.mopub.common.AdReport;
import com.mopub.common.ClientMetadata;
import com.mopub.common.Constants;
import com.mopub.common.VisibleForTesting;
import com.mopub.common.event.MoPubEvents;
import com.mopub.common.logging.MoPubLog;
import com.mopub.common.util.Dips;
import com.mopub.common.util.Utils;
import com.mopub.mraid.MraidNativeCommandHandler;
import com.mopub.network.AdRequest;
import com.mopub.network.AdResponse;
import com.mopub.network.MoPubNetworkError;
import com.mopub.network.Networking;
import com.mopub.network.TrackingRequest;
import com.mopub.volley.RequestQueue;
import com.mopub.volley.VolleyError;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.WeakHashMap;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static com.mopub.network.MoPubNetworkError.Reason.NO_FILL;
import static com.mopub.network.MoPubNetworkError.Reason.WARMING_UP;
public class AdViewController {
static final int MINIMUM_REFRESH_TIME_MILLISECONDS = 10000; // 10 seconds
static final int DEFAULT_REFRESH_TIME_MILLISECONDS = 60000; // 1 minute
static final int MAX_REFRESH_TIME_MILLISECONDS = 600000; // 10 minutes
static final double BACKOFF_FACTOR = 1.5;
private static final FrameLayout.LayoutParams WRAP_AND_CENTER_LAYOUT_PARAMS =
new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER);
private final static WeakHashMap<View,Boolean> sViewShouldHonorServerDimensions = new WeakHashMap<View, Boolean>();
private final Context mContext;
private final long mBroadcastIdentifier;
private MoPubView mMoPubView;
private final WebViewAdUrlGenerator mUrlGenerator;
@Nullable
private AdResponse mAdResponse;
private final Runnable mRefreshRunnable;
@Nullable
private final AdRequest.Listener mAdListener;
private boolean mIsDestroyed;
private Handler mHandler;
private boolean mIsLoading;
private String mUrl;
// This is the power of the exponential term in the exponential backoff calculation.
private int mBackoffPower = 1;
private Map<String, Object> mLocalExtras = new HashMap<String, Object>();
private boolean mAutoRefreshEnabled = true;
private boolean mPreviousAutoRefreshSetting = true;
private String mKeywords;
private Location mLocation;
private boolean mIsTesting;
private boolean mAdWasLoaded;
@Nullable
private String mAdUnitId;
private int mTimeoutMilliseconds;
@Nullable
private AdRequest mActiveRequest;
@Nullable
private Integer mRefreshTimeMillis;
public static void setShouldHonorServerDimensions(View view) {
sViewShouldHonorServerDimensions.put(view, true);
}
private static boolean getShouldHonorServerDimensions(View view) {
return sViewShouldHonorServerDimensions.get(view) != null;
}
public AdViewController(Context context, MoPubView view) {
mContext = context;
mMoPubView = view;
// Default timeout means "never refresh"
mTimeoutMilliseconds = -1;
mBroadcastIdentifier = Utils.generateUniqueId();
mUrlGenerator = new WebViewAdUrlGenerator(context,
MraidNativeCommandHandler.isStorePictureSupported(mContext));
mAdListener = new AdRequest.Listener() {
@Override
public void onSuccess(final AdResponse response) {
onAdLoadSuccess(response);
}
@Override
public void onErrorResponse(final VolleyError volleyError) {
onAdLoadError(volleyError);
}
};
mRefreshRunnable = new Runnable() {
public void run() {
internalLoadAd();
}
};
mRefreshTimeMillis = DEFAULT_REFRESH_TIME_MILLISECONDS;
mHandler = new Handler();
}
@VisibleForTesting
void onAdLoadSuccess(@NonNull final AdResponse adResponse) {
mBackoffPower = 1;
mAdResponse = adResponse;
// Do other ad loading setup. See AdFetcher & AdLoadTask.
mTimeoutMilliseconds = mAdResponse.getAdTimeoutMillis() == null
? mTimeoutMilliseconds : mAdResponse.getAdTimeoutMillis();
Integer refreshTime = mAdResponse.getRefreshTimeMillis();
if (refreshTime != null) {
mRefreshTimeMillis = refreshTime;
}
setNotLoading();
// Get our custom event from the ad response and load into the view.
AdLoader adLoader = AdLoader.fromAdResponse(mAdResponse, this);
if (adLoader != null) {
adLoader.load();
}
scheduleRefreshTimerIfEnabled();
}
@VisibleForTesting
void onAdLoadError(final VolleyError error) {
MoPubErrorCode errorCode = MoPubErrorCode.UNSPECIFIED;
// Handle errors. Do backoff & retry if it makes sense.
if (error instanceof MoPubNetworkError) {
MoPubNetworkError mpError = (MoPubNetworkError) error;
if (mpError.getReason() == NO_FILL || mpError.getReason() == WARMING_UP) {
errorCode = MoPubErrorCode.NO_FILL;
}
}
if (error.networkResponse != null && error.networkResponse.statusCode >= 400) {
// Backoff with the retry timer.
mBackoffPower += 1;
errorCode = MoPubErrorCode.SERVER_ERROR;
}
setNotLoading();
adDidFail(errorCode);
}
public MoPubView getMoPubView() {
return mMoPubView;
}
public void loadAd() {
mBackoffPower = 1;
internalLoadAd();
}
private void internalLoadAd() {
mAdWasLoaded = true;
if (TextUtils.isEmpty(mAdUnitId)) {
MoPubLog.d("Can't load an ad in this ad view because the ad unit ID is not set. " +
"Did you forget to call setAdUnitId()?");
return;
}
if (!isNetworkAvailable()) {
MoPubLog.d("Can't load an ad because there is no network connectivity.");
scheduleRefreshTimerIfEnabled();
return;
}
String adUrl = generateAdUrl();
loadNonJavascript(adUrl);
}
void loadNonJavascript(String url) {
if (url == null) return;
MoPubLog.d("Loading url: " + url);
if (mIsLoading) {
if (!TextUtils.isEmpty(mAdUnitId)) { // This shouldn't be able to happen?
MoPubLog.i("Already loading an ad for " + mAdUnitId + ", wait to finish.");
}
return;
}
mUrl = url;
mIsLoading = true;
fetchAd(mUrl);
}
public void reload() {
MoPubLog.d("Reload ad: " + mUrl);
loadNonJavascript(mUrl);
}
void loadFailUrl(MoPubErrorCode errorCode) {
mIsLoading = false;
Log.v("MoPub", "MoPubErrorCode: " + (errorCode == null ? "" : errorCode.toString()));
final String failUrl = mAdResponse == null ? "" : mAdResponse.getFailoverUrl();
if (!TextUtils.isEmpty(failUrl)) {
MoPubLog.d("Loading failover url: " + failUrl);
loadNonJavascript(failUrl);
} else {
// No other URLs to try, so signal a failure.
adDidFail(MoPubErrorCode.NO_FILL);
}
}
@Deprecated
void setFailUrl(String failUrl) {
// Does nothing.
}
void setNotLoading() {
this.mIsLoading = false;
if (mActiveRequest != null) {
if (!mActiveRequest.isCanceled()) {
mActiveRequest.cancel();
}
mActiveRequest = null;
}
}
public String getKeywords() {
return mKeywords;
}
public void setKeywords(String keywords) {
mKeywords = keywords;
}
public Location getLocation() {
return mLocation;
}
public void setLocation(Location location) {
mLocation = location;
}
public String getAdUnitId() {
return mAdUnitId;
}
public void setAdUnitId(String adUnitId) {
mAdUnitId = adUnitId;
}
public long getBroadcastIdentifier() {
return mBroadcastIdentifier;
}
public void setTimeout(int milliseconds) {
mTimeoutMilliseconds = milliseconds;
}
public int getAdWidth() {
if (mAdResponse != null && mAdResponse.getWidth() != null) {
return mAdResponse.getWidth();
}
return 0;
}
public int getAdHeight() {
if (mAdResponse != null && mAdResponse.getHeight() != null) {
return mAdResponse.getHeight();
}
return 0;
}
@Deprecated
public String getClickTrackingUrl() {
return mAdResponse == null ? null : mAdResponse.getClickTrackingUrl();
}
@Deprecated
public String getRedirectUrl() {
return mAdResponse == null ? null : mAdResponse.getRedirectUrl();
}
@Deprecated
public String getResponseString() {
return mAdResponse == null ? null : mAdResponse.getStringBody();
}
public boolean getAutorefreshEnabled() {
return mAutoRefreshEnabled;
}
void pauseRefresh() {
mPreviousAutoRefreshSetting = mAutoRefreshEnabled;
setAutorefreshEnabled(false);
}
void unpauseRefresh() {
setAutorefreshEnabled(mPreviousAutoRefreshSetting);
}
void forceSetAutorefreshEnabled(boolean enabled) {
mPreviousAutoRefreshSetting = enabled;
setAutorefreshEnabled(enabled);
}
private void setAutorefreshEnabled(boolean enabled) {
final boolean autorefreshChanged = mAdWasLoaded && (mAutoRefreshEnabled != enabled);
if (autorefreshChanged) {
final String enabledString = (enabled) ? "enabled" : "disabled";
MoPubLog.d("Refresh " + enabledString + " for ad unit (" + mAdUnitId + ").");
}
mAutoRefreshEnabled = enabled;
if (mAdWasLoaded && mAutoRefreshEnabled) {
scheduleRefreshTimerIfEnabled();
} else if (!mAutoRefreshEnabled) {
cancelRefreshTimer();
}
}
@Nullable
public AdReport getAdReport() {
if (mAdUnitId != null && mAdResponse != null) {
return new AdReport(mAdUnitId, ClientMetadata.getInstance(mContext), mAdResponse);
}
return null;
}
public boolean getTesting() {
return mIsTesting;
}
public void setTesting(boolean enabled) {
mIsTesting = enabled;
}
@Deprecated
Object getAdConfiguration() {
return null;
}
boolean isDestroyed() {
return mIsDestroyed;
}
/*
* Clean up the internal state of the AdViewController.
*/
void cleanup() {
if (mIsDestroyed) {
return;
}
if (mActiveRequest != null) {
mActiveRequest.cancel();
mActiveRequest = null;
}
setAutorefreshEnabled(false);
cancelRefreshTimer();
// WebView subclasses are not garbage-collected in a timely fashion on Froyo and below,
// thanks to some persistent references in WebViewCore. We manually release some resources
// to compensate for this "leak".
mMoPubView = null;
// Flag as destroyed. LoadUrlTask checks this before proceeding in its onPostExecute().
mIsDestroyed = true;
}
Integer getAdTimeoutDelay() {
return mAdResponse == null ? null : mAdResponse.getAdTimeoutMillis();
}
void trackImpression() {
if (mAdResponse != null) {
TrackingRequest.makeTrackingHttpRequest(mAdResponse.getImpressionTrackingUrl(),
mContext, MoPubEvents.Type.IMPRESSION_REQUEST);
}
}
void registerClick() {
if (mAdResponse != null) {
TrackingRequest.makeTrackingHttpRequest(mAdResponse.getClickTrackingUrl(),
mContext, MoPubEvents.Type.CLICK_REQUEST);
}
}
void fetchAd(String url) {
AdRequest adRequest = new AdRequest(url,
mMoPubView.getAdFormat(),
mAdListener
);
RequestQueue requestQueue = Networking.getRequestQueue(mContext);
requestQueue.add(adRequest);
mActiveRequest = adRequest;
}
void forceRefresh() {
setNotLoading();
loadAd();
}
String generateAdUrl() {
return mUrlGenerator
.withAdUnitId(mAdUnitId)
.withKeywords(mKeywords)
.withLocation(mLocation)
.generateUrlString(Constants.HOST);
}
void adDidFail(MoPubErrorCode errorCode) {
MoPubLog.i("Ad failed to load.");
setNotLoading();
scheduleRefreshTimerIfEnabled();
getMoPubView().adFailed(errorCode);
}
void scheduleRefreshTimerIfEnabled() {
cancelRefreshTimer();
if (mAutoRefreshEnabled && mRefreshTimeMillis != null && mRefreshTimeMillis > 0) {
mHandler.postDelayed(mRefreshRunnable,
Math.min(MAX_REFRESH_TIME_MILLISECONDS,
mRefreshTimeMillis * (long) Math.pow(BACKOFF_FACTOR, mBackoffPower)));
}
}
void setLocalExtras(Map<String, Object> localExtras) {
mLocalExtras = (localExtras != null)
? new TreeMap<String,Object>(localExtras)
: new TreeMap<String,Object>();
}
/**
* Returns a copied map of localExtras
*/
Map<String, Object> getLocalExtras() {
return (mLocalExtras != null)
? new TreeMap<String,Object>(mLocalExtras)
: new TreeMap<String,Object>();
}
private void cancelRefreshTimer() {
mHandler.removeCallbacks(mRefreshRunnable);
}
private boolean isNetworkAvailable() {
// If we don't have network state access, just assume the network is up.
int result = mContext.checkCallingPermission(ACCESS_NETWORK_STATE);
if (result == PackageManager.PERMISSION_DENIED) return true;
// Otherwise, perform the connectivity check.
ConnectivityManager cm
= (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
void setAdContentView(final View view) {
// XXX: This method is called from the WebViewClient's callbacks, which has caused an error on a small portion of devices
// We suspect that the code below may somehow be running on the wrong UI Thread in the rare case.
// see: http://stackoverflow.com/questions/10426120/android-got-calledfromwrongthreadexception-in-onpostexecute-how-could-it-be
mHandler.post(new Runnable() {
@Override
public void run() {
MoPubView moPubView = getMoPubView();
if (moPubView == null) {
return;
}
moPubView.removeAllViews();
moPubView.addView(view, getAdLayoutParams(view));
}
});
}
private FrameLayout.LayoutParams getAdLayoutParams(View view) {
Integer width = null;
Integer height = null;
if (mAdResponse != null) {
width = mAdResponse.getWidth();
height = mAdResponse.getHeight();
}
if (width != null && height != null && getShouldHonorServerDimensions(view) && width > 0 && height > 0) {
int scaledWidth = Dips.asIntPixels(width, mContext);
int scaledHeight = Dips.asIntPixels(height, mContext);
return new FrameLayout.LayoutParams(scaledWidth, scaledHeight, Gravity.CENTER);
} else {
return WRAP_AND_CENTER_LAYOUT_PARAMS;
}
}
@Deprecated
public void customEventDidLoadAd() {
setNotLoading();
trackImpression();
scheduleRefreshTimerIfEnabled();
}
@Deprecated
public void customEventDidFailToLoadAd() {
loadFailUrl(MoPubErrorCode.UNSPECIFIED);
}
@Deprecated
public void customEventActionWillBegin() {
registerClick();
}
@Deprecated
public void setClickthroughUrl(String clickthroughUrl) {
// Does nothing
}
/**
* @deprecated As of release 2.4
*/
@Deprecated
public boolean isFacebookSupported() {
return false;
}
/**
* @deprecated As of release 2.4
*/
@Deprecated
public void setFacebookSupported(boolean enabled) {}
}