diff --git a/README.md b/README.md new file mode 100644 index 0000000..e29a30c --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +Ad2iction Android Adapter for MoPub +================================= + +Adapter version 1.0.0 - Updated 2017-02-21 +------------------------------------------ + +This version of the adapter works with MoPub Android SDK 4.12+ and Ad2iction Android SDK 3.3.0. +Otherwise, please upgrade to the newer versions of both SDKs. + +### Mediate Ad2iction Ads through MoPub + +To integrate Ad2iction as the Custom Native Network in the MoPub ad serving flow, you need the +Custom Event Class code incorporated into your application in addition to the Flurry SDK. +Three quick steps are necessary: + +1. Integrate the Ad2iction SDK and Ad2iction adapter for MoPub code into your app +2. Configure Ad2iction's Ad unit(s) +3. Configure MoPub to mediate Ad2iction + +#### Integrate the Ad2iction SDK and Ad2iction adapter for MoPub code into your app + +1. If your application is not yet using Ad2iction, please contact Ad2iction. + +2. Add the Ad2iction Android SDK. + +3. Add the Google Play Services SDK to your project. This is required for Android Advertising ID +support. See http://developer.android.com/google/playservices/setup.html. + +4. Add the Ad2iction MoPub adapter classes (found in the [com.mopub.nativeads](src/com/mopub/nativeads) package) to your project. Place the following classes in the com.mopub.nativeads package: + * [`Ad2ictionCustomEventNative`](src/com/mopub/nativeads/Ad2ictionCustomEventNative.java) + * [`Ad2ictionNativeAdRenderer`](src/com/mopub/nativeads/Ad2ictionNativeAdRenderer.java) + +5. The steps to integrate Flurry Native Ads via MoPub are similar to those described [here](https://github.com/mopub/mopub-android-sdk/wiki/Native-Ads-Integration): + * Create an XML layout for your native ads + * Define where ads should be placed within your feed + * Create a MoPubAdAdapter to wrap your existing `Adapter` subclass and begin loading ads. + + And you should register the `Ad2ictionNativeAdRenderer` as a custom ad renderer. + + ```java + ViewBinder viewBinder = new ViewBinder.Builder(R.layout.native_ad_list_item) + // Set up your regular ViewBinder + .build(); + + // Register the Ad2ictionNativeAdRenderer to handle static native ads + final Ad2ictionNativeAdRenderer ad2ictionRenderer = new Ad2ictionNativeAdRenderer(viewBinder); + mAdAdapter = new MoPubAdAdapter(getActivity(), adapter); + mAdAdapter.registerAdRenderer(ad2ictionRenderer); + + //...register other native ad renderers as required + ``` + +6. Add permissions in AndroidManifest: +```xml + + + + + +``` + +7. Add Activity declaration in AndroidManifest: +```xml + + + + +``` + +8. If you plan to run [ProGuard](http://developer.android.com/tools/help/proguard.html) on your app +before release, you will need to add the following to your ProGuard configuration file. + +``` +# Ad2iction Proguard Config +# NOTE: You should also include the Android Proguard config found with the build tools: +# $ANDROID_HOME/tools/proguard/proguard‐android.txt + +# Add this line if you use Admob Adapter +‐libraryjars \libs\ad2iction‐mediation‐adapteradmob.jar + +‐libraryjars \libs\ad2iction‐sdk.jar + +# Keep public classes and methods. +‐keepclassmembers class com.ad2iction.** { public *;} +‐keep public class com.ad2iction.** +‐keep public class android.webkit.JavascriptInterface {} + +‐keep class * extends com.ad2iction.mobileads.CustomEventBanner {} +‐keep class * extends com.ad2iction.mobileads.CustomEventInterstitial {} +‐keep class * extends com.ad2iction.nativeads.CustomEventNative {} + +‐keep class com.google.android.gms.common.GooglePlayServicesUtil {*;} +‐keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {*;} +‐keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {*;} + ``` + +#### Configure Ad2iction Ad unit(s) + +For each MoPub ad unit that you would like to mediate Ad2iction through, please contact Ad2iction. + +#### Configure MoPub to mediate Ad2iction + +Ad2iction's custom events are implemented in accordance with [instructions provided by MoPub](https://github.com/mopub/mopub-android-sdk/wiki/Custom-Events). + +After you incorporate the Ad2iction files into your project, you need to +configure Ad2iction as a Custom Network. Please follow instructions provided by MoPub +(for [native](https://dev.twitter.com/mopub/ui-setup/network-setup-custom-native)) with any of the Ad2iction custom events class noted below: + +* [`com.mopub.nativeads.Ad2ictionCustomEventNative`](src/com/mopub/nativeads/Ad2ictionCustomEventNative.java) + for Ad2iction native ads + +**NOTE:** An important step to get this integration working is to configure the custom network (as described above) with the +Ad2iction API key and send them as server extras (in the "Custom Event Class Data" field on the MoPub UI). + +The custom event class data should be in the following JSON format: + +```json +{"response_body_key":"YOUR_API_KEY"} +``` + +You can also configure the custom event data as a line item in your MoPub order. + +![Screenshot showing ad unit/line item config on MoPub's dashboard](imgs/mopub_line_item_config.png) + +Changelog +--------- +#### Version 1.0.0 - 2017-02-21 +* Introduced Ad2iction Native Ad support in the adapter +* Supports MoPub SDK 4.12+ diff --git a/imgs/mopub_line_item_config.png b/imgs/mopub_line_item_config.png new file mode 100644 index 0000000..f4b7d39 Binary files /dev/null and b/imgs/mopub_line_item_config.png differ diff --git a/src/com/mopub/nativeads/Ad2ictionCustomEventNative.java b/src/com/mopub/nativeads/Ad2ictionCustomEventNative.java new file mode 100644 index 0000000..56fe664 --- /dev/null +++ b/src/com/mopub/nativeads/Ad2ictionCustomEventNative.java @@ -0,0 +1,142 @@ +package com.mopub.nativeads; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import com.ad2iction.nativeads.Ad2ictionNative; +import com.ad2iction.nativeads.NativeResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.mopub.nativeads.NativeImageHelper.preCacheImages; + +public class Ad2ictionCustomEventNative extends CustomEventNative { + + private static final String LOG_TAG = Ad2ictionCustomEventNative.class.getSimpleName(); + + protected void loadNativeAd(@NonNull Context context, + @NonNull CustomEventNativeListener customEventNativeListener, + @NonNull Map localExtras, + @NonNull Map serverExtras) { + try { + Ad2ictionForwardingNativeAd ad2ictionForwardingNativeAd = + new Ad2ictionForwardingNativeAd(context, serverExtras.get("response_body_key"), + customEventNativeListener); + ad2ictionForwardingNativeAd.loadAd(); + } catch (IllegalArgumentException e) { + customEventNativeListener.onNativeAdFailed(NativeErrorCode.UNSPECIFIED); + Log.i(LOG_TAG, + "Failed Native AdFetch: Missing required server extras [response_body_key]."); + } + } + + public static class Ad2ictionForwardingNativeAd extends BaseNativeAd + implements Ad2ictionNative.Ad2ictionNativeNetworkListener, + Ad2ictionNative.Ad2ictionNativeEventListener { + + private final Context mContext; + private final String mKey; + private final Ad2ictionNative mAd; + private final CustomEventNativeListener mCustomEventNativeListener; + private NativeResponse mResponse; + + private Ad2ictionForwardingNativeAd(Context context, String key, + CustomEventNativeListener customEventNativeListener) + throws IllegalArgumentException { + mContext = context.getApplicationContext(); + mKey = key; + if (TextUtils.isEmpty(mKey)) { + throw new IllegalArgumentException("Key cannot be null"); + } + mAd = new Ad2ictionNative(context, key, "native", this); + mCustomEventNativeListener = customEventNativeListener; + } + + private void loadAd() { + mAd.setNativeEventListener(this); + mAd.makeRequest(); + } + + @Override + public void onNativeLoad(NativeResponse nativeResponse) { + mResponse = nativeResponse; + + final List imageUrls = new ArrayList<>(2); + final String mainImageUrl = nativeResponse.getMainImageUrl(); + if (mainImageUrl != null) { + imageUrls.add(mainImageUrl); + Log.d(LOG_TAG, "Ad2iction Native Ad main image found."); + } + + final String iconUrl = nativeResponse.getIconImageUrl(); + if (iconUrl != null) { + imageUrls.add(iconUrl); + Log.d(LOG_TAG, "Ad2iction Native Ad icon image found."); + } + + preCacheImages(mContext, imageUrls, new NativeImageHelper.ImageListener() { + + public void onImagesCached() { + mCustomEventNativeListener.onNativeAdLoaded(Ad2ictionForwardingNativeAd.this); + } + + public void onImagesFailedToCache(NativeErrorCode errorCode) { + mCustomEventNativeListener.onNativeAdFailed(errorCode); + } + }); + } + + @Override + public void onNativeFail(com.ad2iction.nativeads.NativeErrorCode nativeErrorCode) { + if (nativeErrorCode == null) { + mCustomEventNativeListener.onNativeAdFailed(NativeErrorCode.UNSPECIFIED); + } else if (nativeErrorCode == com.ad2iction.nativeads.NativeErrorCode.NETWORK_NO_FILL) { + mCustomEventNativeListener.onNativeAdFailed(NativeErrorCode.NETWORK_NO_FILL); + } else if (nativeErrorCode == + com.ad2iction.nativeads.NativeErrorCode.NETWORK_INVALID_STATE) { + mCustomEventNativeListener.onNativeAdFailed(NativeErrorCode.NETWORK_INVALID_STATE); + } else { + mCustomEventNativeListener.onNativeAdFailed(NativeErrorCode.UNSPECIFIED); + } + } + + @Override + public void onNativeImpression(View view) { + Log.d(LOG_TAG, "onNativeImpression: Ad2iction native ad impression logged"); + notifyAdImpressed(); + } + + @Override + public void onNativeClick(View view) { + Log.d(LOG_TAG, "onNativeClick: Ad2iction native ad clicked"); + notifyAdClicked(); + } + + @Override + public void prepare(@NonNull View view) { + mResponse.prepare(view); + Log.d(LOG_TAG, "prepare(" + mResponse.toString() + " " + view.toString() + ")"); + } + + @Override + public void clear(@NonNull View view) { + mResponse.clear(view); + Log.d(LOG_TAG, "clear(" + mResponse.toString() + ")"); + } + + @Override + public void destroy() { + mResponse.destroy(); + Log.d(LOG_TAG, "destroy(" + mResponse.toString() + ") started."); + } + + NativeResponse getNativeResponse() { + return mResponse; + } + } +} \ No newline at end of file diff --git a/src/com/mopub/nativeads/Ad2ictionNativeAdRenderer.java b/src/com/mopub/nativeads/Ad2ictionNativeAdRenderer.java new file mode 100644 index 0000000..e83ff99 --- /dev/null +++ b/src/com/mopub/nativeads/Ad2ictionNativeAdRenderer.java @@ -0,0 +1,145 @@ +package com.mopub.nativeads; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.ad2iction.common.VisibleForTesting; +import com.ad2iction.nativeads.NativeResponse; +import com.mopub.common.Preconditions; + +import java.util.Iterator; +import java.util.WeakHashMap; + +public class Ad2ictionNativeAdRenderer + implements MoPubAdRenderer { + + private static final String LOG_TAG = Ad2ictionNativeAdRenderer.class.getSimpleName(); + + @NonNull private final ViewBinder mViewBinder; + @NonNull private final WeakHashMap mViewHolderMap; + + public Ad2ictionNativeAdRenderer(@NonNull final ViewBinder viewBinder) { + mViewBinder = viewBinder; + mViewHolderMap = new WeakHashMap<>(); + } + + @Override + @NonNull + public View createAdView(@NonNull final Context context, final ViewGroup parent) { + return LayoutInflater.from(context).inflate(this.mViewBinder.layoutId, parent, false); + } + + @Override + public void renderAdView(@NonNull final View view, @NonNull + final Ad2ictionCustomEventNative.Ad2ictionForwardingNativeAd ad2ictionForwardingNativeAd) { + Ad2ictionNativeViewHolder nativeViewHolder = mViewHolderMap.get(view); + if (nativeViewHolder == null) { + nativeViewHolder = Ad2ictionNativeViewHolder.fromViewBinder(view, this.mViewBinder); + mViewHolderMap.put(view, nativeViewHolder); + } + nativeViewHolder.update(ad2ictionForwardingNativeAd.getNativeResponse()); + nativeViewHolder + .updateExtras(view, ad2ictionForwardingNativeAd.getNativeResponse(), mViewBinder); + setViewVisibility(nativeViewHolder, View.VISIBLE); + } + + private void setViewVisibility(@NonNull final Ad2ictionNativeViewHolder viewHolder, + final int visibility) { + if (viewHolder.staticNativeViewHolder.mainView != null) { + viewHolder.staticNativeViewHolder.mainView.setVisibility(visibility); + } + } + + @Override + public boolean supports(@NonNull final BaseNativeAd nativeAd) { + Preconditions.checkNotNull(nativeAd); + return nativeAd instanceof Ad2ictionCustomEventNative.Ad2ictionForwardingNativeAd; + } + + private static class Ad2ictionNativeViewHolder { + + private final StaticNativeViewHolder staticNativeViewHolder; + @Nullable TextView titleView; + @Nullable TextView textView; + @Nullable TextView callToActionView; + @Nullable ImageView mainImageView; + @Nullable ImageView iconImageView; + + @VisibleForTesting + + private Ad2ictionNativeViewHolder(final StaticNativeViewHolder staticNativeViewHolder) { + this.staticNativeViewHolder = staticNativeViewHolder; + } + + static Ad2ictionNativeViewHolder fromViewBinder(final View view, + final ViewBinder viewBinder) { + StaticNativeViewHolder staticNativeViewHolder = + StaticNativeViewHolder.fromViewBinder(view, viewBinder); + Ad2ictionNativeViewHolder nativeViewHolder = + new Ad2ictionNativeViewHolder(staticNativeViewHolder); + + nativeViewHolder.titleView = staticNativeViewHolder.titleView; + nativeViewHolder.textView = staticNativeViewHolder.textView; + nativeViewHolder.callToActionView = staticNativeViewHolder.callToActionView; + nativeViewHolder.mainImageView = staticNativeViewHolder.mainImageView; + nativeViewHolder.iconImageView = staticNativeViewHolder.iconImageView; + + return nativeViewHolder; + } + + void update(@NonNull NativeResponse nativeResponse) { + this.addTextView(this.titleView, nativeResponse.getTitle()); + this.addTextView(this.textView, nativeResponse.getText()); + this.addTextView(this.callToActionView, nativeResponse.getCallToAction()); + nativeResponse.loadMainImage(this.mainImageView); + nativeResponse.loadIconImage(this.iconImageView); + } + + void updateExtras(@NonNull View outerView, @NonNull NativeResponse nativeResponse, + @NonNull ViewBinder viewBinder) { + Iterator i$ = viewBinder.extras.keySet().iterator(); + + while (i$.hasNext()) { + String key = (String) i$.next(); + int resourceId = viewBinder.extras.get(key); + View view = outerView.findViewById(resourceId); + Object content = nativeResponse.getExtra(key); + if (view instanceof ImageView) { + ((ImageView) view).setImageDrawable(null); + nativeResponse.loadExtrasImage(key, (ImageView) view); + } else if (view instanceof TextView) { + ((TextView) view).setText(null); + if (content instanceof String) { + this.addTextView((TextView) view, (String) content); + } + } else { + Log.d(LOG_TAG, "View bound to " + key + + " should be an instance of TextView or ImageView."); + } + } + + } + + private void addTextView(@Nullable TextView textView, @Nullable String contents) { + if (textView == null) { + Log.d(LOG_TAG, "Attempted to add text (" + contents + ") to null TextView."); + } else { + textView.setText(null); + if (contents == null) { + Log.d(LOG_TAG, "Attempted to set TextView contents to null."); + } else { + textView.setText(contents); + } + + } + } + } + +}