/*
* AdFetcher.java
*
* Copyright (c) 2012, MoPub Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'MoPub Inc.' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.mopub.mobileads;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
/*
* AdFetcher is a delegate of an AdView that handles loading ad data over a
* network connection. The ad is fetched in a background thread by executing
* AdFetchTask, which is an AsyncTask subclass. This class gracefully handles
* the changes to AsyncTask in Android 4.0.1 (we continue to run parallel to
* the app developer's background tasks). Further, AdFetcher keeps track of
* the last completed task to prevent out-of-order execution.
*/
public class AdFetcher {
private int mTimeoutMilliseconds = 10000;
// This is equivalent to Build.VERSION_CODES.ICE_CREAM_SANDWICH
private static final int VERSION_CODE_ICE_CREAM_SANDWICH = 14;
private AdView mAdView;
private AdFetchTask mCurrentTask;
private String mUserAgent;
private long mCurrentTaskId;
private long mLastCompletedTaskId;
private enum FetchStatus {
NOT_SET,
FETCH_CANCELLED,
INVALID_SERVER_RESPONSE_BACKOFF,
INVALID_SERVER_RESPONSE_NOBACKOFF,
CLEAR_AD_TYPE,
AD_WARMING_UP;
}
public AdFetcher(AdView adview, String userAgent) {
mAdView = adview;
mUserAgent = userAgent;
mCurrentTaskId = -1;
mLastCompletedTaskId = -1;
}
public void fetchAdForUrl(String url) {
mCurrentTaskId++;
Log.i("MoPub", "Fetching ad for task #" + mCurrentTaskId);
if (mCurrentTask != null) {
mCurrentTask.cancel(true);
}
mCurrentTask = new AdFetchTask(this);
if (Build.VERSION.SDK_INT >= VERSION_CODE_ICE_CREAM_SANDWICH) {
Class<?> cls = AdFetchTask.class;
Class<?>[] parameterTypes = {Executor.class, Object[].class};
String[] parameters = {url};
try {
Method method = cls.getMethod("executeOnExecutor", parameterTypes);
Field field = cls.getField("THREAD_POOL_EXECUTOR");
method.invoke(mCurrentTask, field.get(cls), parameters);
} catch (NoSuchMethodException exception) {
Log.d("MoPub", "Error executing AdFetchTask on ICS+, method not found.");
} catch (InvocationTargetException exception) {
Log.d("MoPub", "Error executing AdFetchTask on ICS+, thrown by executeOnExecutor.");
} catch (Exception exception) {
Log.d("MoPub", "Error executing AdFetchTask on ICS+: " + exception.toString());
}
} else {
mCurrentTask.execute(url);
}
}
public void cancelFetch() {
if (mCurrentTask != null) {
Log.i("MoPub", "Canceling fetch ad for task #" + mCurrentTaskId);
mCurrentTask.cancel(true);
}
}
private void markTaskCompleted(long taskId) {
if (taskId > mLastCompletedTaskId) {
mLastCompletedTaskId = taskId;
}
}
public void cleanup() {
cancelFetch();
mAdView = null;
mUserAgent = "";
}
protected void setTimeout(int milliseconds) {
mTimeoutMilliseconds = milliseconds;
}
protected int getTimeout() {
return mTimeoutMilliseconds;
}
private static class AdFetchTask extends AsyncTask<String, Void, AdFetchResult> {
private AdFetcher mAdFetcher;
private AdView mAdView;
private Exception mException;
private HttpClient mHttpClient;
private long mTaskId;
private FetchStatus mFetchStatus = FetchStatus.NOT_SET;
private static final int MAXIMUM_REFRESH_TIME_MILLISECONDS = 600000;
private static final double EXPONENTIAL_BACKOFF_FACTOR = 1.5;
private AdFetchTask(AdFetcher adFetcher) {
mAdFetcher = adFetcher;
mAdView = mAdFetcher.mAdView;
mHttpClient = getDefaultHttpClient();
mTaskId = mAdFetcher.mCurrentTaskId;
}
@Override
protected AdFetchResult doInBackground(String... urls) {
AdFetchResult result = null;
try {
result = fetch(urls[0]);
} catch (Exception exception) {
mException = exception;
} finally {
shutdownHttpClient();
}
return result;
}
private AdFetchResult fetch(String url) throws Exception {
HttpGet httpget = new HttpGet(url);
httpget.addHeader("User-Agent", mAdFetcher.mUserAgent);
// We check to see if this AsyncTask was cancelled, as per
// http://developer.android.com/reference/android/os/AsyncTask.html
if (isCancelled()) {
mFetchStatus = FetchStatus.FETCH_CANCELLED;
return null;
}
if (mAdView == null || mAdView.isDestroyed()) {
Log.d("MoPub", "Error loading ad: AdView has already been GCed or destroyed.");
return null;
}
HttpResponse response = mHttpClient.execute(httpget);
HttpEntity entity = response.getEntity();
if (response == null || entity == null) {
Log.d("MoPub", "MoPub server returned null response.");
mFetchStatus = FetchStatus.INVALID_SERVER_RESPONSE_NOBACKOFF;
return null;
}
final int statusCode = response.getStatusLine().getStatusCode();
// Client and Server HTTP errors should result in an exponential backoff
if (statusCode >= 400) {
Log.d("MoPub", "Server error: returned HTTP status code " + Integer.toString(statusCode) +
". Please try again.");
mFetchStatus = FetchStatus.INVALID_SERVER_RESPONSE_BACKOFF;
return null;
}
// Other non-200 HTTP status codes should still fail
else if (statusCode != HttpStatus.SC_OK) {
Log.d("MoPub", "MoPub server returned invalid response: HTTP status code " +
Integer.toString(statusCode) + ".");
mFetchStatus = FetchStatus.INVALID_SERVER_RESPONSE_NOBACKOFF;
return null;
}
mAdView.configureAdViewUsingHeadersFromHttpResponse(response);
// Ensure that the ad is not warming up.
Header warmupHeader = response.getFirstHeader("X-Warmup");
if (warmupHeader != null && warmupHeader.getValue().equals("1")) {
Log.d("MoPub", "Ad Unit (" + mAdView.getAdUnitId() + ") is still warming up. " +
"Please try again in a few minutes.");
mFetchStatus = FetchStatus.AD_WARMING_UP;
return null;
}
// Ensure that the ad type header is valid and not "clear".
Header atHeader = response.getFirstHeader("X-Adtype");
if (atHeader == null || atHeader.getValue().equals("clear")) {
Log.d("MoPub", "No inventory found for adunit (" + mAdView.getAdUnitId() + ").");
mFetchStatus = FetchStatus.CLEAR_AD_TYPE;
return null;
}
// Handle custom native ad type.
else if (atHeader.getValue().equals("custom")) {
Log.i("MoPub", "Performing custom event.");
// If applicable, try to invoke the new custom event system (which uses custom classes)
Header customEventClassNameHeader =
response.getFirstHeader("X-Custom-Event-Class-Name");
if (customEventClassNameHeader != null) {
Map<String, String> paramsMap = new HashMap<String, String>();
paramsMap.put("X-Custom-Event-Class-Name", customEventClassNameHeader.getValue());
Header customEventClassDataHeader =
response.getFirstHeader("X-Custom-Event-Class-Data");
if (customEventClassDataHeader != null) {
paramsMap.put("X-Custom-Event-Class-Data", customEventClassDataHeader.getValue());
}
return new PerformCustomEventTaskResult(mAdView, paramsMap);
}
// Otherwise, use the (deprecated) legacy custom event system for older clients
Header oldCustomEventHeader = response.getFirstHeader("X-Customselector");
return new PerformLegacyCustomEventTaskResult(mAdView, oldCustomEventHeader);
}
// Handle mraid ad type.
else if (atHeader.getValue().equals("mraid")) {
Log.i("MoPub", "Loading mraid ad");
Map<String, String> paramsMap = new HashMap<String, String>();
paramsMap.put("X-Adtype", atHeader.getValue());
String data = httpEntityToString(entity);
paramsMap.put("X-Nativeparams", data);
return new LoadNativeAdTaskResult(mAdView, paramsMap);
}
// Handle native SDK ad type.
else if (!atHeader.getValue().equals("html")) {
Log.i("MoPub", "Loading native ad");
Map<String, String> paramsMap = new HashMap<String, String>();
paramsMap.put("X-Adtype", atHeader.getValue());
Header npHeader = response.getFirstHeader("X-Nativeparams");
paramsMap.put("X-Nativeparams", "{}");
if (npHeader != null) {
paramsMap.put("X-Nativeparams", npHeader.getValue());
}
Header ftHeader = response.getFirstHeader("X-Fulladtype");
if (ftHeader != null) {
paramsMap.put("X-Fulladtype", ftHeader.getValue());
}
return new LoadNativeAdTaskResult(mAdView, paramsMap);
}
// Handle HTML ad.
String data = httpEntityToString(entity);
return new LoadHtmlAdTaskResult(mAdView, data);
}
@Override
protected void onPostExecute(AdFetchResult result) {
if (!isMostCurrentTask()) {
Log.d("MoPub", "Ad response is stale.");
releaseResources();
return;
}
// If cleanup() has already been called on the AdView, don't proceed.
if (mAdView == null || mAdView.isDestroyed()) {
if (result != null) {
result.cleanup();
}
mAdFetcher.markTaskCompleted(mTaskId);
releaseResources();
return;
}
if (result == null) {
if (mException != null) {
Log.d("MoPub", "Exception caught while loading ad: " + mException);
}
MoPubErrorCode errorCode;
switch (mFetchStatus) {
case NOT_SET:
errorCode = MoPubErrorCode.UNSPECIFIED;
break;
case FETCH_CANCELLED:
errorCode = MoPubErrorCode.CANCELLED;
break;
case INVALID_SERVER_RESPONSE_BACKOFF:
case INVALID_SERVER_RESPONSE_NOBACKOFF:
errorCode = MoPubErrorCode.SERVER_ERROR;
break;
case CLEAR_AD_TYPE:
case AD_WARMING_UP:
errorCode = MoPubErrorCode.NO_FILL;
break;
default:
errorCode = MoPubErrorCode.UNSPECIFIED;
break;
}
mAdView.adDidFail(errorCode);
/*
* There are numerous reasons for the ad fetch to fail, but only in the specific
* case of actual server failure should we exponentially back off.
*
* Note: We place the exponential backoff after AdView's adDidFail because we only
* want to increase refresh times after the first failure refresh timer is
* scheduled, and not before.
*/
if (mFetchStatus == FetchStatus.INVALID_SERVER_RESPONSE_BACKOFF) {
exponentialBackoff();
mFetchStatus = FetchStatus.NOT_SET;
}
} else {
result.execute();
result.cleanup();
}
mAdFetcher.markTaskCompleted(mTaskId);
releaseResources();
}
@Override
protected void onCancelled() {
if (!isMostCurrentTask()) {
Log.d("MoPub", "Ad response is stale.");
releaseResources();
return;
}
Log.d("MoPub", "Ad loading was cancelled.");
if (mException != null) {
Log.d("MoPub", "Exception caught while loading ad: " + mException);
}
mAdFetcher.markTaskCompleted(mTaskId);
releaseResources();
}
private String httpEntityToString(HttpEntity entity)
throws IOException {
InputStream inputStream = entity.getContent();
int numberBytesRead = 0;
StringBuffer out = new StringBuffer();
byte[] bytes = new byte[4096];
while (numberBytesRead != -1) {
out.append(new String(bytes, 0, numberBytesRead));
numberBytesRead = inputStream.read(bytes);
}
inputStream.close();
return out.toString();
}
/* This helper function is called when a 4XX or 5XX error is received during an ad fetch.
* It exponentially increases the parent AdView's refreshTime up to a specified cap.
*/
private void exponentialBackoff() {
if (mAdView == null) {
return;
}
int refreshTimeMilliseconds = mAdView.getRefreshTimeMilliseconds();
refreshTimeMilliseconds = (int) (refreshTimeMilliseconds * EXPONENTIAL_BACKOFF_FACTOR);
if (refreshTimeMilliseconds > MAXIMUM_REFRESH_TIME_MILLISECONDS) {
refreshTimeMilliseconds = MAXIMUM_REFRESH_TIME_MILLISECONDS;
}
mAdView.setRefreshTimeMilliseconds(refreshTimeMilliseconds);
}
private void releaseResources() {
mAdFetcher = null;
mException = null;
mFetchStatus = FetchStatus.NOT_SET;
}
private DefaultHttpClient getDefaultHttpClient() {
HttpParams httpParameters = new BasicHttpParams();
int timeoutMilliseconds = mAdFetcher.getTimeout();
if (timeoutMilliseconds > 0) {
// Set timeouts to wait for connection establishment / receiving data.
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutMilliseconds);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutMilliseconds);
}
// Set the buffer size to avoid OutOfMemoryError exceptions on certain HTC devices.
// http://stackoverflow.com/questions/5358014/android-httpclient-oom-on-4g-lte-htc-thunderbolt
HttpConnectionParams.setSocketBufferSize(httpParameters, 8192);
return new DefaultHttpClient(httpParameters);
}
private void shutdownHttpClient() {
if (mHttpClient != null) {
ClientConnectionManager manager = mHttpClient.getConnectionManager();
if (manager != null) {
manager.shutdown();
}
mHttpClient = null;
}
}
private boolean isMostCurrentTask() {
return mTaskId >= mAdFetcher.mLastCompletedTaskId;
}
}
private static abstract class AdFetchResult {
WeakReference<AdView> mWeakAdView;
public AdFetchResult(AdView adView) {
mWeakAdView = new WeakReference<AdView>(adView);
}
abstract void execute();
/* The AsyncTask thread pool often appears to keep references to these
* objects, preventing GC. This method should be used to release
* resources to mitigate the GC issue.
*/
abstract void cleanup();
}
/*
* This is the old way of performing Custom Events, and is now deprecated. This will still be
* invoked on old clients when X-Adtype is "custom" and the new X-Custom-Event-Class-Name header
* is not specified (legacy custom events parse the X-Customselector header instead).
*/
@Deprecated
private static class PerformLegacyCustomEventTaskResult extends AdFetchResult {
protected Header mHeader;
public PerformLegacyCustomEventTaskResult(AdView adView, Header header) {
super(adView);
mHeader = header;
}
public void execute() {
AdView adView = mWeakAdView.get();
if (adView == null || adView.isDestroyed()) {
return;
}
adView.setIsLoading(false);
MoPubView mpv = adView.mMoPubView;
if (mHeader == null) {
Log.i("MoPub", "Couldn't call custom method because the server did not specify one.");
mpv.loadFailUrl(MoPubErrorCode.ADAPTER_NOT_FOUND);
return;
}
String methodName = mHeader.getValue();
Log.i("MoPub", "Trying to call method named " + methodName);
Class<? extends Activity> c;
Method method;
Activity userActivity = mpv.getActivity();
try {
c = userActivity.getClass();
method = c.getMethod(methodName, MoPubView.class);
method.invoke(userActivity, mpv);
} catch (NoSuchMethodException e) {
Log.d("MoPub", "Couldn't perform custom method named " + methodName +
"(MoPubView view) because your activity class has no such method");
mpv.loadFailUrl(MoPubErrorCode.ADAPTER_NOT_FOUND);
return;
} catch (Exception e) {
Log.d("MoPub", "Couldn't perform custom method named " + methodName);
mpv.loadFailUrl(MoPubErrorCode.ADAPTER_NOT_FOUND);
return;
}
}
public void cleanup() {
mHeader = null;
}
}
/*
* This is the new way of performing Custom Events. This will be invoked on new clients when
* X-Adtype is "custom" and the X-Custom-Event-Class-Name header is specified.
*/
private static class PerformCustomEventTaskResult extends AdFetchResult {
protected Map<String,String> mParamsMap;
public PerformCustomEventTaskResult(AdView adView, Map<String,String> paramsMap) {
super(adView);
mParamsMap = paramsMap;
}
public void execute() {
AdView adView = mWeakAdView.get();
if (adView == null || adView.isDestroyed()) {
return;
}
adView.setIsLoading(false);
MoPubView moPubView = adView.mMoPubView;
if (mParamsMap == null) {
Log.i("MoPub", "Couldn't invoke custom event because the server did not specify one.");
moPubView.loadFailUrl(MoPubErrorCode.ADAPTER_NOT_FOUND);
return;
}
moPubView.loadCustomEvent(mParamsMap);
}
public void cleanup() {
mParamsMap = null;
}
}
private static class LoadNativeAdTaskResult extends AdFetchResult {
protected Map<String, String> mParamsMap;
private LoadNativeAdTaskResult(AdView adView, Map<String, String> paramsMap) {
super(adView);
mParamsMap = paramsMap;
}
public void execute() {
AdView adView = mWeakAdView.get();
if (adView == null || adView.isDestroyed()) {
return;
}
adView.setIsLoading(false);
MoPubView mpv = adView.mMoPubView;
mpv.loadNativeSDK(mParamsMap);
}
public void cleanup() {
mParamsMap = null;
}
}
private static class LoadHtmlAdTaskResult extends AdFetchResult {
protected String mData;
private LoadHtmlAdTaskResult(AdView adView, String data) {
super(adView);
mData = data;
}
public void execute() {
AdView adView = mWeakAdView.get();
if (adView == null || adView.isDestroyed()) {
return;
}
if (mData == null) {
return;
}
adView.setResponseString(mData);
adView.loadDataWithBaseURL("http://" + adView.getServerHostname() + "/", mData,
"text/html", "utf-8", null);
}
public void cleanup() {
mData = null;
}
}
}