Несколько лет назад я решил, что вставлять одну рекламную сеть в мобильное приложение недостаточно эффективно и засунул несколько сетей, а этой зимой решил переписать и выложить эту разработку на github. Так и родился OpenAdAdapter.

OpenAdAdapter — это библиотека для мобильных игр (Android и iOS, лицензия Apache 2.0). Я решил делать адаптер для игр, а не для всех приложений, чтобы API был проще. Под игрой я понимаю приложение, у которого на весь экран один GL канвас, и баннер расположен сверху или снизу. Когда баннер есть, канву надо чуть подвинуть. То есть разработчик просто говорит: покажи баннер снизу, без того, чтобы вникать как сеть Х засунуть в лайот. Многие игры разрабатываются с помощью SDK и движков типа Marmalade или Unity. Там добраться до нативной платформы и изучить все нюансы реализации колбеков, это отдельное джитсу. Кстати, как раз поэтому в OpenAdAdapter нет колбеков.

Предполагается, что API OpenAdAdapter можно вызывать из любого потока. (Я так задумывал, но опасаюсь зарекаться). Все методы статичные.

На данный момент поддерживаются следующие сети:

Android

— AdColony
— Admob
— AerServ
— Chartboost
— Heyzap
— InMobi

iOS

— AdColony
— Admob
— AerServ
— Chartboost
— Heyzap
— InMobi
— iAd

Создать адаптер для новой сети несложно. Я изначально хотел поддержать российскую WapStart так, как они мне раз платили невероятный $1 за клик целых два дня за московский трафик, (обычно 5 центов), потом штрафанули на 30% и отдали деньги, но добавление их усложнило бы все на андроиде. Дело в том, что все сети дают jar файл для интеграции, а WapStart давал проект на еклипс так еще с файлами ресурсов. Вот поэтому я их не реализовал пока.

Настройки загружаются из JSON файла из интернета.

		OpenAdAdapter.initFromUrl(
						this,
						"https://raw.githubusercontent.com/sample-data/oad1/master/android-redirect.json");



    [OpenAdAdapter startWithUrl:@"https://raw.githubusercontent.com/sample-data/oad1/master/ios-redir.json"];


(хостить статические файлы на гитхабе может нарушать правила github)

Это очень важно. Вы можете поменять стратегию или перестать показывать рекламу какой-то сети, или какая-то сеть вас может забанить.

OpenAdAdapter умеет показать баннер сверху или снизу, спрятать баннер, показать объявление на весь экран (fullscreen/interstitial), видео(video) и видео за вознаграждение (rewarded). Что и как показывать описано в json файле.

iOS


Контролер для айфона
#import "ViewController.h"
#import "OpenAdAdapter.h"


@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *btnInit;
@property (weak, nonatomic) IBOutlet UILabel *label1;
@end

@implementation ViewController
{
    bool btick;
    NSString * rewardText;
}
- (IBAction)clickChkReward:(id)sender {
    OADReward * reward = [OpenAdAdapter reward];
    if(reward != nil){
        self->rewardText = [NSString stringWithFormat:@"%@ %f %@", [reward network], [reward amount], [reward currency]];
    }else{
        self->rewardText = @"";
    }
}
- (IBAction)clickInit:(id)sender {
    if(!self->btick){
        self->btick = true;
        [self performSelector:@selector(tick) withObject:nil afterDelay:1.0];
    }
    self.label1.text = @"Initializing 1";
    [OpenAdAdapter startWithUrl:@"https://raw.githubusercontent.com/sample-data/oad1/master/ios-redir.json"];
    //    [OpenAdAdapter startWithUrl:@"https://raw.githubusercontent.com/sample-data/oad1/master/ios-no-heyzap.json"];
    //[OpenAdAdapter startWithUrl:@"https://raw.githubusercontent.com/sample-data/oad1/master/ios-heyzap.json"];
    self.label1.text = @"Initializing 2";
}
-(void)tick{
    [self performSelector:@selector(tick) withObject:nil afterDelay:1.0];
    NSString * s1 = [NSString stringWithFormat:@"bh %g %g %@", [OpenAdAdapter bannerHeightPts], [OpenAdAdapter bannerHeightPixels], self->rewardText];
    self.label1.text = s1;
}
- (IBAction)clickBanner:(id)sender {
    [OpenAdAdapter showTopBanner:self];
}
- (IBAction)clickBottomBanner:(id)sender {
    [OpenAdAdapter showBottomBanner:self];
}
- (IBAction)clickHideBanner:(id)sender {
    [OpenAdAdapter hideBanner];
}
- (IBAction)clickFullscreen:(id)sender {
    [OpenAdAdapter showFullscreen:self];
}
- (IBAction)clickVideo:(id)sender {
    [OpenAdAdapter showVideo:self];
}
- (IBAction)clickRewarded:(id)sender {
    [OpenAdAdapter showRewarded:self];
}
- (IBAction)clickTest:(id)sender {
    
//    [TestX1 test1];
    
    [OpenAdAdapter test1];
}
- (IBAction)clickTest2:(id)sender {
    //[OpenAdAdapter test2:self];
    [OpenAdAdapter showTopBanner:self];
}
- (IBAction)clickTest3:(id)sender {
    //[OpenAdAdapter test3:self];
    [OpenAdAdapter showBottomBanner:self];
}


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end


Отличие линковки приложений под iOS (от Android) заставило сделать не одну либу, а одну + адаптер для каждой сети.
То есть, если вам нужен Chartboost, то в проект надо добавить

— libOADAdapterChartboost.a — адаптер из OpenAdAdapter
— Chartboost.framework — оригинальный фреймворк от Chartboost

github.com/OpenAdAdapter/OAD-iOS-bin/tree/master/chartboost

— libOpenAdAdapter.a — и сам OpenAdAdapter

Самое простое это кинуть все сети сразу.

github.com/OpenAdAdapter/OAD-iOS-bin

Добавить необходимые фреймворки:

    AdSupport.framework
    StoreKit.framework
    MessageUI.framework
    libxml2.2.dylib
    libz.dylib
    libsqlite3.0.dylib
    CoreTelephony.framework
    EventKit.framework
    EventKitUI.framework
    Security.framework
    Social.framework
    WebKit.framework


и Other Linker Flags: -ObjC

Android


Активити из примера
package com.example.testoad01;

import com.openadadapter.OpenAdAdapter;
import com.openadadapter.Reward;

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends ActionBarActivity {

	Runnable tick = new Runnable(){

		@Override
		public void run() {
			try{
				label1.setText("bh " + OpenAdAdapter.getBannerHeightInPoints() + " " +OpenAdAdapter.getBannerHeightInPixels());
			}
			finally{
				handler.postDelayed(tick, 1000);

			}
		}};
		Handler handler = new Handler(Looper.getMainLooper());
	boolean ticking;
	private TextView label1;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TestX.test();
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		OpenAdAdapter.onCreate(this);
		label1 = (TextView)findViewById(R.id.textView1);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle action bar item clicks here. The action bar will
		// automatically handle clicks on the Home/Up button, so long
		// as you specify a parent activity in AndroidManifest.xml.
		int id = item.getItemId();
		if (id == R.id.action_settings) {
			return true;
		}
		return super.onOptionsItemSelected(item);
	}

	public void butBS(View v) {
		Toast.makeText(getApplicationContext(), "Bottom Banner Show",
				Toast.LENGTH_LONG).show();
		OpenAdAdapter.showBottomBanner(null);
	}

	public void butBSTop(View v) {
		Toast.makeText(getApplicationContext(), "Top Banner Show",
				Toast.LENGTH_LONG).show();
		OpenAdAdapter.showTopBanner(null);
	}

	public void butBH(View v) {
		Toast.makeText(getApplicationContext(), "Banner Hide",
				Toast.LENGTH_LONG).show();
		OpenAdAdapter.hideBanner();
	}

	public void butF(View v) {
		Toast.makeText(getApplicationContext(), "Banner Fullscreen",
				Toast.LENGTH_LONG).show();
		OpenAdAdapter.showFullscreen(null);
	}

	public void butVideo(View v) {
		Toast.makeText(getApplicationContext(), "Banner Video",
				Toast.LENGTH_LONG).show();
		OpenAdAdapter.showVideo(null);
	}

	public void butR(View v) {
		Toast.makeText(getApplicationContext(), "Rewarded Video",
				Toast.LENGTH_LONG).show();
		OpenAdAdapter.showRewarded(null);
	}

	public void butIU(View v) {
		Toast.makeText(getApplicationContext(), "Init from URL",
				Toast.LENGTH_LONG).show();

		// OpenAdAdapter.initFromUrl(this,
		// "https://raw.githubusercontent.com/sample-data/oad1/master/data1.json");
		OpenAdAdapter
				.initFromUrl(
						this,
						"https://raw.githubusercontent.com/sample-data/oad1/master/android-redirect.json");
		if(!ticking){
			handler.postDelayed(tick, 1000);
			ticking = true;
		}

	}

	public void butIF(View v) {
		Toast.makeText(getApplicationContext(), "Init from File",
				Toast.LENGTH_LONG).show();

		// OpenAdAdapter.preinit();

	}

	public void butV(View v) {
		Toast.makeText(getApplicationContext(), "verify", Toast.LENGTH_LONG)
				.show();

		OpenAdAdapter.verify();

	}

	public void butSF(View v) {
		Toast.makeText(getApplicationContext(), "Show Fullscreen",
				Toast.LENGTH_LONG).show();

		OpenAdAdapter.showMyFullscreen(this);

	}

	public void clickFetchReward(View v) {
		Reward reward = OpenAdAdapter.fetchReward();

		if (reward == null) {
			Toast.makeText(getApplicationContext(), "No reward",
					Toast.LENGTH_LONG).show();
		} else {
			Toast.makeText(
					getApplicationContext(),
					"Reward " + reward.getNetwork() + " " + reward.getAmount()
							+ " " + reward.getCurrency(), Toast.LENGTH_LONG)
					.show();
		}

	}

	@Override
	public void onStart() {
		super.onStart();
		OpenAdAdapter.onStart(this);
	}

	@Override
	public void onResume() {
		super.onResume();
		OpenAdAdapter.onResume(this);
	}

	@Override
	public void onPause() {
		super.onPause();
		OpenAdAdapter.onPause(this);
	}

	@Override
	public void onStop() {
		super.onStop();
		OpenAdAdapter.onStop(this);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		OpenAdAdapter.onDestroy(this);
	}

	@Override
	public void onBackPressed() {
		if (OpenAdAdapter.onBackPressed(this))
			return;
		super.onBackPressed();
	}


}


Так как некоторые сети хотят получать уведомление onStart, onPause, onResume и т.д., то и OpenAdAdapter на андроиде нужно вызывать при этих событиях.

Так же нужно не забыть про кипу активити, которых нужно указать в AndroidManifest.xml

И все jar файлы кинуть в папку lib (ненужные можно выкинуть): github.com/OpenAdAdapter/OAD-Android-bin/tree/master/lib

Неочевидное — вероятное


Важный момент: нежелательно, чтобы баннер перекрывал интерфейс программы. Как раз эта проблема и подтолкнула меня на создание проекта. Мое приложение определяло есть или нет баннер, исходя из того, что сообщает делегат/колбек/лиснер.

— AdShown — экран сжался
— AdFailed — экран разжался

То есть, я не делал скриншот экрана и не распознавал образ баннера на нем, но оказалось, что самая прекрасная сеть рекламы работает не так, как я предполагал. Она может сообщить AdFailed, а когда приложение расширит канву на весь экран — забанить приложение.

Решением проблемы с перекрытием контента стали следующие функции

int pt = OpenAdAdapter.getBannerHeightInPoints();
int px = OpenAdAdapter.getBannerHeightInPixels();

int pt = [OpenAdAdapter bannerHeightPts];
int px = [OpenAdAdapter bannerHeightPixels];

Если значение 0 — баннера нет, значение больше 0 — баннер сверху, значение меньше 0 — баннер снизу. Вызывать можно каждый кадр или раз в секунду и соответсвенно изменять габариты GL канваса.

Аналогичным способом без колбеков я решил реализовать проверку получения вознаграждения за просмотр Rewarded видео.

Конфигурационный файл JSON


raw.githubusercontent.com/sample-data/oad1/master/data1.json:

{
	"debug":{"verify":true},
	"urls":["https://ohohoho.appspot.com/track", {"url":"https://xman545476.appspot.com/track", "priority": 10}],
	"commands":["save", {"cmd":"settings", "settings":{"reportLocation":true, "advertisingId":true, "userId": true}}],
	"strategy":{
		"banner": {"list":["admob", "inmobi", "wapstart", "aerserv"], "strategy":"random"},
		"fullscreen2": {
				"list":[
					"aerserv", 
					"inmobi"], 
				"strategy":"random"},
		"fullscreen": {
				"list":[
					"aerserv", 
					"heyzap", 
					"adcolony",
					{"name":"heyzap","type":"rewarded", "preload": "always"}, 
					"admob", 
					{"name":"chartboost","type":"video", "preload": "low"}, 
					"inmobi"], 
				"strategy":"random"},
		"video": {"list":["chartboost", "heyzap"],"strategy":"round-robin"},
		"rewarded": {"list":["adcolony", "chartboost", "heyzap"], 
				"strategy":"random"}
	},
	"networks":[
		{
			"name":"admob",
			"bannerId":"ca-app-pub-8607147313123654/8458359243",
			"fullscreenId":"ca-app-pub-8607147313123654/9935092440"
		},
		{
			"name":"inmobi",
			"propId":"d4783f2efd4147499e40cc3540f2d221",
			"bannerId":"d4783f2efd4147499e40cc3540f2d221",
			"fullscreenId":"d4783f2efd4147499e40cc3540f2d221",
			"bannerId1":"1428178968194889",
			"fullscreenId1":"1428178928625995"
		},
		{
			"name":"chartboost",
			"id":"5519c460c909a67c4e1e58a4",
			"sig":"e34adc93d56dfcff2a3a34d5f0dcd74204cbaf05",
			"video": "true",
			"rewarded": "true"
		},
		{
			"name":"heyzap",
			"id":"e0c44b21d39c921a55f31ea836a70b65",
			"video": "true",
			"rewarded": "true"
		},
		{
			"name":"adcolony",
			"id":"app398cb71e4cae463f94",
			"videoId":"vzc71a58270b924c95a0",
			"rewardedId":"vz6669f90f9dbe4e4a89"
		},
		{
			"name":"aerserv",
			"fullscreenId":"1000741",
			"banner320":"1000834",
			"banner728":"1000835"
		},
		{
			"name":"home",
			"bannerId":"",
			"halfId":"",
			"fullscreenId":""
		},
		{
			"name":"direct",
			"bannerId":"",
			"halfId":"",
			"fullscreenId":""
		}
	]
}


Первые три ключа debug, urls, commands — пока не используются. В ключе networks перечислены сети. Последние две (home, direct) тоже пока не используются. Я разрабатываю сервер, который будет более интеллектуальным чем JSON файл.

Ключ strategy самый сложный в этом конфиге. У него есть 4 подключа для 4х стратегий — banner, fullscreen, video, rewarded.
(fullscreen2 — не используется)

"banner": {"list":["admob", "inmobi", "wapstart", "aerserv"], "strategy":"random"}

Каждая стратегия имеет в свою очередь два ключа list и strategy.

— list — перечисляет сети;
— strategy — определяет стратегию: random, round-robin, failsafe;

random — показывает рекламу случайным образом;
round-robin — показывает рекламу по очереди;
failsafe — показывает всегда рекламу первую из списка, а если та не работает, то вторую.

Мною написано приложение на HTML для создания данного файла. Выложу его вскорости.

Как проект будет развиваться


HTML — приложение для редактирования конфига (уже написано);
Вебсайт — планирую сделать вебсайт, чтобы разработчик выбирал сети и получал ZIP файл (который будет содержать все и можно просто скопировать в проект одним движением руки) и инструкции, какие фреймворки добавить и что написать в AndroidManifest.xml;
Unity3D — написанно, все виды рекламы показывает на обеих платформах, но там не все так просто делается и описание реализации требует отдельной статьи, пока нет поддержки UnityAds;
Сервер — тут много идей, можно бесконечно развиваться;
Тюнинг — размеры банеров относительно экрана, умный и экономный прелоад сетей и т.п.;
Самопроверка — в проекте андроида можно заметить метод verify. В теории он должен проверять определены ли все нужные активити, пермишины, ресиверы и мета тег с версией google play services. И он это частично делает, но до конца этот функционал не реализован;
Статья на Мегамозг — с экономическим обоснованием использования OpenAdAdapter против Mopub, AdTapsy, Appodeal и других, обещающих невероятный CTR.

Комментарии (6)


  1. Suvitruf
    15.07.2015 19:16

    Хорошо бы документацию добавить. Да и комментарии в коде не помешали бы.


    1. pyra Автор
      15.07.2015 19:30

      Документация будет в ближайшее время. А пока есть два примера под iOS и Android


  1. petrovichtim
    16.07.2015 10:49

    Если не ошибаюсь, то тоже самое делают площадки медиаторы. Но они дают свое sdk а рекламу распределяют через свой сервис.Попутно ещё выбирая рекламу подороже


    1. pyra Автор
      16.07.2015 13:51

      Да, но на мой взгляд они немного дурят играя цифрами. Как «дурят»? Планирую показать в статье на мегамозге, если наскребу больше трех абзацев.
      + Они могут закрыть свой бизнес, приложения перестанут приносить прибыль вообще (так как приложения завязаны на их сервер), + они берут себе %, например AdTapsy берет 10% трафика


      1. petrovichtim
        16.07.2015 14:52

        Admob от гугла тоже же медиатор


        1. pyra Автор
          16.07.2015 15:19

          сейчас адаптеры к другим сетям есть, практически во всех сетях. Admob, AerServ, TapIt, HeyZap (We currently support AdColony/Opera, AdMob, Vungle, AppLovin, Facebook Audience Network, UnityAds, Apple iAd, and Chartboost. Heyzap's standard SDK is included by default… Convenient, right? We will also be bringing on new networks as time goes by)

          Дело в том, что мелкие сети имеют низкий филлрейт и не могут без медиации конкурировать и любой момент могут обанкротится, а большая как например, Admob может вас забанить, чтобы другим было неповадно.

          C OpenAdAdapter вы сами контролируете медиацию. Все что вам нужно это хостить статичный JSON файл в интернете. Если гугл запретит вам пожизненно пользовать сервисами гугла или HeyZap обанкротится — это не отразится на том, что вы выбираете, чью рекламу показывать.

          Так же добавить OpenAdAdapter в приложение в 6 сетями может быть проще чем просто один Адмоб или HeyZap, что я попробую показать на видео в скринкасте. Но на все надо время