OpenAdAdapter — простое управление мобильной рекламой

от автора

Несколько лет назад я решил, что вставлять одну рекламную сеть в мобильное приложение недостаточно эффективно и засунул несколько сетей, а этой зимой решил переписать и выложить эту разработку на 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"]; 

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

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.

ссылка на оригинал статьи http://habrahabr.ru/post/262715/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *