Доброго времени суток, Хабрахабр!
Меня зовут Александр, я разработчик под ОС Android. Сегодня хочу с вами поделиться опытом реализации альтернативного платному способу отключения рекламы в приложении — отключение рекламы за просмотр рекламы (AdMob Rewarded Video Ads). Интересно? Тогда добро пожаловать под кат.
В далеком 2013 году я решил заняться разработкой приложений под Android, начал читать тематические книги, статьи, смотрел видео уроки и т.д. Написал первое недоприложение и приуныл, т.к. хотелось сделать что-то полезное, нужное обществу, а идей не было. В 2014 меня знакомый попросил разработать для него мобильный справочник по синтаксису платформы Arduino (там С язык). С огромным желанием я взялся за этот проект и реализовал первую версию для Android 3.0+. Через время решено было усовершенствовать ее, и так появилась вторая версия (для Android 4.0+). Обе они бесплатные с баннером от AdMob внизу и платным его отключением. Все было хорошо, пока мне не стали писать, что ~150-170 рублей РФ дороговато для отключения рекламы навсегда в их любимом справочнике. На что я ответил «бартерным» решением вопроса — пользователь может отключить баннер внизу на время за просмотр видео рекламы от AdMob.
[вернуться к содержанию]
При запуске приложения пользователю будет показан Dialog, с предложением отключить рекламу, если, конечно, он ранее ее не отключил или отсутствует подключение к сети Интернет. В случае положительного ответа, приложение показывает фрагмент с кнопками, с помощью которых и можно выполнить отключение рекламы в приложении удобным способом.
[вернуться к содержанию]
[вернуться к содержанию]
Код главной Activity
Код класса CheckURLConnection
Код класса PreferencesManager
Класс ConstantHolder — класс, в котором я храню константы, чтобы не импортировать их отовсюду, а только из одного места брать (аналог класса R)
И самое интересное — класс-фрагмент отключения рекламы
Класс работы с баннером AdMob. Ничего сложного, публикую для ознакомления. Хотя его же и на StackOverFlow ни раз выкладывал
[вернуться к содержанию]
Вот и все. Суть статьи — поделиться с обществом своей идеей и ее реализацией. А также получить приглашение на Хабрахабр, если кому-то понравится то, чем я поделился. Буду рад и благодарен за ваше мнение в вопросах доработки кода и/или идеи. Если понадобится дополнительное пояснение — пишите, я в кратчайшие сроки внесу правки в статью или дам ответ в комментариях.
Ссылку на приложение по понятным причинам не публикую в открытом доступе. Этика есть этика! Кому нужно — дам в лс.
Статистика AdMob пока еще сырая, с момента внедрения данной альтернативы прошло всего ~2 недели. Не все пользователи обновились. Но точно есть те, кто пользуется таким способом отключения баннера внизу.
UPDATE: Ссылка на статью с результатами ИТОГ 3-х месяцев: Альтернатива платному отключению рекламы в бесплатном приложении Android
Благодарю всех, кто дочитал статью до конца!
Меня зовут Александр, я разработчик под ОС Android. Сегодня хочу с вами поделиться опытом реализации альтернативного платному способу отключения рекламы в приложении — отключение рекламы за просмотр рекламы (AdMob Rewarded Video Ads). Интересно? Тогда добро пожаловать под кат.
Содержание для удобства навигации
Как все было ?
В далеком 2013 году я решил заняться разработкой приложений под Android, начал читать тематические книги, статьи, смотрел видео уроки и т.д. Написал первое недоприложение и приуныл, т.к. хотелось сделать что-то полезное, нужное обществу, а идей не было. В 2014 меня знакомый попросил разработать для него мобильный справочник по синтаксису платформы Arduino (там С язык). С огромным желанием я взялся за этот проект и реализовал первую версию для Android 3.0+. Через время решено было усовершенствовать ее, и так появилась вторая версия (для Android 4.0+). Обе они бесплатные с баннером от AdMob внизу и платным его отключением. Все было хорошо, пока мне не стали писать, что ~150-170 рублей РФ дороговато для отключения рекламы навсегда в их любимом справочнике. На что я ответил «бартерным» решением вопроса — пользователь может отключить баннер внизу на время за просмотр видео рекламы от AdMob.
[вернуться к содержанию]
Реализация, часть 1: Принцип работы (словами)
При запуске приложения пользователю будет показан Dialog, с предложением отключить рекламу, если, конечно, он ранее ее не отключил или отсутствует подключение к сети Интернет. В случае положительного ответа, приложение показывает фрагмент с кнопками, с помощью которых и можно выполнить отключение рекламы в приложении удобным способом.
[вернуться к содержанию]
Реализация, часть 2: Внешний вид
Диалог с предложением отключить рекламу

Экран отключения рекламы 

1 видео реклама просмотрена
5 видео реклам просмотрено

Реклама отключена на 1 час

Реклама отключена на 1 день

[вернуться к содержанию]
Реализация, часть 3: Принцип работы (java код)
Код главной Activity
public class ActivityMain extends AppCompatActivity { private boolean internet; private boolean isAdsDisabled; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // get SharedPreferences prefManager = new PreferencesManager(this); isAdsDisabled = prefManager.isAdsDisabled(); // true - disable | false - enabled // ... здесь код создания Вашего UI internet = CheckURLConnection.isNetworkAvailable(); // true - disable | false - enabled if (internet && !isAdsDisabled && isTimeUp()) { DialogFragment disableAds = new DisableAdsDialog(); if (!disableAds.isResumed()) { disableAds.show(getSupportFragmentManager(), ConstantHolder.DIALOG_DISABLE_ADS); } } private boolean isTimeUp() { return System.currentTimeMillis() > prefManager.getEstimatedAdsTime(); } }
Код класса CheckURLConnection
public class CheckURLConnection { public static boolean isNetworkAvailable() { ConnectivityManager connectivityManager = (ConnectivityManager) MyAppClass.getContext().getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivityManager != null) { NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); } else { return false; } } }
Код класса PreferencesManager
public class PreferencesManager { private Context mContext; private static SharedPreferences mSPref; private SharedPreferences.Editor mSPEditor; public PreferencesManager(Context context) { this.mContext = context; mSPref = mContext.getSharedPreferences(ConstantHolder.APP_PREF, Context.MODE_PRIVATE); } // получаем значение состояния рекламы из SharedPreferences public boolean isAdsDisabled() { return mSPref.getBoolean(ConstantHolder.APP_PREF_DISABLE_ADS, false); } // получаем дату в миллисекундах, когда нужно включить рекламу public long getEstimatedAdsTime() { return mSPref.getLong(ConstantHolder.APP_DISABLE_ADS_PERIOD, 0); } }
Класс ConstantHolder — класс, в котором я храню константы, чтобы не импортировать их отовсюду, а только из одного места брать (аналог класса R)
public class ConstantHolder { //Preferences Constants public static final String APP_PREF = "app_pref"; public static final String APP_PREF_DISABLE_ADS = "disableAds"; // Реклама public static final String APP_DISABLE_ADS_PERIOD = "disableAdsPeriod"; // Период отключения рекламы }
И самое интересное — класс-фрагмент отключения рекламы
java код целого класса
public class SettingsAdsFrag extends Fragment implements View.OnClickListener { private static final String VIEWED_ZERO_VIDEO_ADS = "0"; private static final int VIEWED_ADS_NUMBER_PER_HOUR = 1; private static final int VIEWED_ADS_NUMBER_PER_DAY = 5; //5 private static final long DISABLE_ADS_PERIOD_1_HOUR = 60 * 60 * 1000; private static final long DISABLE_ADS_PERIOD_24_HOURS = 24 * 60 * 60 * 1000; private Context mContext; private PreferencesManager prefManager; private RewardedVideoAd mRewardedVideoAd; private AdRequest mAdRequest; private boolean internet; private boolean readyToPurchase; private boolean bDisableAds; private String ready; private String notReady; private static int adsViewedCounter = 0; private Button btnReadyToViewing; private Button btnDisableAdsPerHour; private Button btnDisableAdsPerDay; private TextView tvViewedAds; private TextView tvEstimatedDate; private ToggleButton tbDisableAds; private BillingProcessor bp; @Override public void onAttach(Context context) { super.onAttach(context); this.mContext = context; // инициализируем свой класс менеджер хранения настроек prefManager = new PreferencesManager(context); // получаем текущее состояние интернет соединения internet = CheckURLConnection.isNetworkAvailable(); // присваиваем "не готово" биллингу readyToPurchase = false; // сохраняем в глобальные переменные значения "НЕ ГОТОВО" и "СМОТРЕТЬ" из ресурсов. Сделал так, чтобы по несколько раз к ресурсам не обращаться ready = context.getString(R.string.txt_cat_ads_ready_for_viewing); notReady = context.getString(R.string.txt_cat_ads_not_ready_for_viewing); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // отключаю меню у Toolbar setHasOptionsMenu(false); // инициализирую биллинг с помощью библиотеки от Anjlab - In-App-Billing-v3 bp = new BillingProcessor(getActivity(), InAppBillingResources.getRsaKey(), // мой RSA ключ InAppBillingResources.getMerchantId(), // мой ID продавца из Google Play Developer Console bpHandler // и сам хэндлер ); // получаю состояние рекламы из файла настроек bDisableAds = prefManager.isAdsDisabled(); // true - disable | false - enabled } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // здесь код вывода заголовков в Toolbar, опустил его, т.к. не по теме статьи // [START AdMob Rewarded Video Ads - инициализация] mRewardedVideoAd = MobileAds.getRewardedVideoAdInstance(getActivity()); mRewardedVideoAd.setRewardedVideoAdListener(rewardedVideoAdListener); // такую хитрость я применяю везде, чтобы повторно Google меня не забанил на AdMob (еще 1 год писать апелляции я не хочу!) if (BuildConfig.DEBUG) { mAdRequest = new AdRequest.Builder() .addTestDevice(DeviceHash.getHtcDeviceHash()) .build(); } else { mAdRequest = new AdRequest.Builder() .build(); } // загружаю видео рекламу loadRewardedVideoAd(); // [END AdMob Rewarded Video Ads] // создаем View экрана Настройки - Отключение рекламы View settAdsView = inflater.inflate(R.layout.frag_sett_ads_screen, container, false); // [START ToggleButton Disable Ads] tbDisableAds = (ToggleButton) settAdsView.findViewById(R.id.tb_disable_ads); // если биллинг инициалирован и отключение рекламы куплено, то сохраняем это в SharedPrefereces и устанавливаем "Отключено" на кнопке-переключателе // да-да-да, я еще раз делаю запрос в Google на предмет покупки. А вдруг юзер руками подправил файл настроек ? if (readyToPurchase) { if (bp.isPurchased(InAppBillingResources.getSKU_DisableAds())) { setAdsDisable(); tbDisableAds.setChecked(false); } } else { // в противном случае читаю то, что записано было // true - disable | false - enabled tbDisableAds.setChecked(!bDisableAds); } // устанавливаю слушатель нажатия по кнопке tbDisableAds.setOnClickListener(this); // [END ToggleButton Disable Ads] // далее идет элементарная инициализация полей и установка значений для каждой из них. Ничего сложного // [START TextView Rewarded Video Ads Disabling Guide] TextView tvRewardedGuide = (TextView) settAdsView.findViewById(R.id.tv_rewarded_video_disabling_guide); tvRewardedGuide.setText(String.format(getActivity().getString(R.string.txt_cat_ads_disable_tmp_text), VIEWED_ADS_NUMBER_PER_HOUR, VIEWED_ADS_NUMBER_PER_DAY)); // [END TextView Rewarded Video Ads Disabling Guide] // [START TextView Viewed Ads] tvViewedAds = (TextView) settAdsView.findViewById(R.id.tv_viewed_ads); // [END TextView Viewed Ads] // [START Button Ready for Viewing] btnReadyToViewing = (Button) settAdsView.findViewById(R.id.btn_ready_to_viewing); btnReadyToViewing.setText(notReady); btnReadyToViewing.setEnabled(false); btnReadyToViewing.setOnClickListener(this); // [END Button Ready for Viewing] // [START Button Disable Ads Per Hour] btnDisableAdsPerHour = (Button) settAdsView.findViewById(R.id.btn_disable_ads_per_hour); btnDisableAdsPerHour.setEnabled(false); btnDisableAdsPerHour.setOnClickListener(this); // [END Button Disable Ads Per Hour] // [START Button Disable Ads Per Day] btnDisableAdsPerDay = (Button) settAdsView.findViewById(R.id.btn_disable_ads_per_day); btnDisableAdsPerDay.setEnabled(false); btnDisableAdsPerDay.setOnClickListener(this); // [END Button Disable Ads Per Day] // [START TextView Last Viewing Time] tvEstimatedDate = (TextView) settAdsView.findViewById(R.id.tv_estimated_date); // [END TextView Last Viewing Time] // обновляю значения текстовых полей и текста кнопок updateTextView(); updateButtons(); return settAdsView; } @Override public void onResume() { // Rewarded Video Ad - необходимо для поддержания жизненного цикла видео рекламы if (mRewardedVideoAd != null) { mRewardedVideoAd.resume(getActivity()); } super.onResume(); // updateUI - обновляю экран updateTextView(); updateButtons(); } @Override public void onPause() { // Rewarded Video Ad - необходимо для поддержания жизненного цикла видео рекламы if (mRewardedVideoAd != null) { mRewardedVideoAd.pause(getActivity()); } super.onPause(); } @Override public void onDestroy() { // Rewarded Video Ad - необходимо для поддержания жизненного цикла видео рекламы if (mRewardedVideoAd != null) { mRewardedVideoAd.destroy(getActivity()); } super.onDestroy(); } // обработчик нажатий по кнопкам @Override public void onClick(View v) { switch (v.getId()) { case R.id.tb_disable_ads: // disable ads ? setText(..OFF) : setText(..ON) // if state ON (disableAds - false) // true - ads disabled; false - ads enabled if (!bDisableAds && readyToPurchase) { // если реклама не отключена и биллинг готов, выполняем покупку "Отключить рекламу навсегда платно" bp.purchase(getActivity(), InAppBillingResources.getSKU_DisableAds()); } break; case R.id.btn_ready_to_viewing: // если видео реклама загружена, то запускаем ее просмотр if (mRewardedVideoAd.isLoaded()) { mRewardedVideoAd.show(); } break; case R.id.btn_disable_ads_per_hour: // отключаем рекламу 1 час disableAdsPerPeriod(DISABLE_ADS_PERIOD_1_HOUR); adsViewedCounter--; updateTextView(); updateButtons(); break; case R.id.btn_disable_ads_per_day: // отключаем рекламу 1 день disableAdsPerPeriod(DISABLE_ADS_PERIOD_24_HOURS); clearAdsCounter(); updateTextView(); updateButtons(); break; default: break; } // true - ads disabled; // false - ads enabled if (bDisableAds) { tbDisableAds.setChecked(false); showSnackbar(); } } // ========================================================== // [START R E W A R D E D V I D E O A D] private RewardedVideoAdListener rewardedVideoAdListener = new RewardedVideoAdListener() { @Override public void onRewardedVideoAdLoaded() { // когда видео реклама будет полностью загружена, влючаем кнопку просмотра btnReadyToViewing.setText(ready); btnReadyToViewing.setEnabled(true); } @Override public void onRewardedVideoAdOpened() { } @Override public void onRewardedVideoStarted() { // устанавливаем НЕ ГОТОВО на кнопку и выключаем ее btnReadyToViewing.setText(notReady); btnReadyToViewing.setEnabled(false); } @Override public void onRewardedVideoAdClosed() { // загружаем новую видео рекламу loadRewardedVideoAd(); } @Override public void onRewarded(RewardItem rewardItem) { // если счетчик рекламы меньше количества просмотров для отключения на день, то инкрементируем его if (adsViewedCounter < VIEWED_ADS_NUMBER_PER_DAY) { adsViewedCounter++; } // обновляем поля экрана updateTextView(); updateButtons(); } @Override public void onRewardedVideoAdLeftApplication() { } @Override public void onRewardedVideoAdFailedToLoad(int i) { // загружаем новую рекламу loadRewardedVideoAd(); } }; private void loadRewardedVideoAd() { // если есть доступ в сеть Интернет, грузим видео рекламу if (internet) { mRewardedVideoAd.loadAd(mContext.getString(R.string.admob_rewarded_video_id), mAdRequest); } } // [END R E W A R D E D V I D E O A D] // ========================================================== // обновляем текстовые поля согласно условий и значения счетчика просмотров // показываем дату возобновления рекламы в приложении, если ее отключили временно private void updateTextView() { // true - disable | false - enabled if (bDisableAds) { tvViewedAds.setText(String.valueOf(adsViewedCounter)); tvEstimatedDate.setText(""); } else { // [START U P D A T E T E X T V I E W : tvViewedAds] if (adsViewedCounter > 0 && adsViewedCounter <= VIEWED_ADS_NUMBER_PER_DAY) { String strViewedAdsCount = adsViewedCounter + " / " + VIEWED_ADS_NUMBER_PER_DAY; tvViewedAds.setText(strViewedAdsCount); } else { tvViewedAds.setText(VIEWED_ZERO_VIDEO_ADS); } // [END U P D A T E T E X T V I E W : tvViewedAds] // [START U P D A T E T E X T V I E W : tvEstimatedDate] long estimatedDate = prefManager.getEstimatedAdsTime(); long currentDate = System.currentTimeMillis(); if (estimatedDate != 0 && estimatedDate > currentDate) { String strEstimatedDate = convertTime(estimatedDate); String strEstimatedDateFinal = "<b>" + mContext.getString(R.string.txt_tv_header_estimated_time).toUpperCase() + "</b>" + "<br>" + strEstimatedDate; tvEstimatedDate.setText(Html.fromHtml(strEstimatedDateFinal)); } // [END U P D A T E T E X T V I E W : tvEstimatedDate] } } // обновляем кнопки private void updateButtons() { // 0 if (adsViewedCounter == 0) { btnDisableAdsPerHour.setEnabled(false); btnDisableAdsPerDay.setEnabled(false); } // 1 - 4 if (adsViewedCounter > 0 && adsViewedCounter < VIEWED_ADS_NUMBER_PER_DAY) { btnDisableAdsPerHour.setEnabled(true); } // 5 if (adsViewedCounter == VIEWED_ADS_NUMBER_PER_DAY) { btnDisableAdsPerHour.setEnabled(true); btnDisableAdsPerDay.setEnabled(true); } } // метод отключения рекламы на период private void disableAdsPerPeriod(long disablePeriod) { // текущая дата в миллисекундах long currentDate = System.currentTimeMillis(); // дата возобновления рекламы в приложении long estimatedDate = currentDate + disablePeriod; // сохраняем дату в файл настроек prefManager.setEstimatedDate(estimatedDate); // отключаем баннер внизу экрана AdMobAds.disableBanner(getActivity(), true); } // обнуляем счетчик private void clearAdsCounter() { adsViewedCounter = 0; } // конвертер миллисекунд в дату и время согласно формату public String convertTime(long time) { Date date = new Date(time); Format format = new SimpleDateFormat("dd MMM yyyy @ HH:mm:ss"); return format.format(date); } // ========================================================== // [START IN APP BILLING] BillingProcessor.IBillingHandler bpHandler = new BillingProcessor.IBillingHandler() { @Override public void onProductPurchased(@NonNull String productId, @Nullable TransactionDetails details) { // Called when requested PRODUCT ID was successfully purchased // Вызывается, когда запрашиваемый PRODUCT ID был успешно куплен if (bp.isPurchased(productId)) { // сохраняем новое состояние рекламы "отключена" и устанавливаем "Выключено" для кнопки-переключателя setAdsDisable(); tbDisableAds.setChecked(false); // перезапускаем приложение restartDialog(); } else { // иначе устанавливаем "Включено" tbDisableAds.setChecked(true); } } @Override public void onPurchaseHistoryRestored() { //Вызывается, когда история покупки была восстановлена, // и список всех принадлежащих идентификаторы продуктов был загружен из Google Play } @Override public void onBillingError(int errorCode, @Nullable Throwable error) { // Вызывается, когда появляется ошибка. См. константы класса // для получения более подробной информации } @Override public void onBillingInitialized() { // Вызывается, когда bp был инициализирован и он готов приобрести readyToPurchase = true; } }; // [START IN APP BILLING] // ========================================================== // метод сохранения отключенного состояния рекламы private void setAdsDisable() { prefManager.setAdsDisabled(); } // диалог перезапуска приложения // [START restartDialog] private void restartDialog() { AlertDialog.Builder builder; View alertLayout = View.inflate(mContext, R.layout.dialog_restart, null); if (prefManager.getAppTheme() == 0) { builder = new AlertDialog.Builder(getActivity(), R.style.AppThemeDialogStyleLight); } else { builder = new AlertDialog.Builder(getActivity(), R.style.AppThemeDialogStyleDark); } builder.setTitle(getActivity().getString(R.string.msg_notification_Title)); builder.setView(alertLayout); builder.setPositiveButton(getActivity().getString(R.string.ans_restart), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { restartApp(); } }); builder.show(); } // [END restartDialog] // метод перезапуска приложения // [START restartApp] private void restartApp() { Intent i = getActivity().getPackageManager().getLaunchIntentForPackage(getActivity().getPackageName()); if (i != null) { i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); getActivity().startActivity(i); } } // [END restartApp] // Snackbar с уведомлением, что рекламу уже отключена. Если пользователь снова кликнет по кнопке отключения рекламы private void showSnackbar() { Snackbar.make(getActivity().getWindow().getDecorView().getRootView(), getActivity().getResources().getString(R.string.advertising_is_already_disabled), Snackbar.LENGTH_SHORT).show(); } }
Класс работы с баннером AdMob. Ничего сложного, публикую для ознакомления. Хотя его же и на StackOverFlow ни раз выкладывал
public class AdMobAds { public static void disableBanner(final Activity activity, boolean disableAds) { final View adsContainer = activity.findViewById(R.id.container); final AdView adView = (AdView) activity.findViewById(R.id.adView); if (disableAds) { adView.setVisibility(View.GONE); adsContainer.setPadding(0, 0, 0, 0); } else { AdRequest adRequest; if (BuildConfig.DEBUG) { adRequest = new AdRequest.Builder() .addTestDevice(DeviceHash.getHtcDeviceHash()) .build(); } else { adRequest = new AdRequest.Builder() .build(); } adView.loadAd(adRequest); adView.setAdListener(new AdListener() { @Override public void onAdFailedToLoad(int errorCode) { MyAppLogs.show("[bottom-banner] >> onAdFailedToLoad: реклама не загружена\terrorCode = " + errorCode + "."); super.onAdFailedToLoad(errorCode); } @Override public void onAdLoaded() { super.onAdLoaded(); MyAppLogs.show("[bottom-banner] >> onAdLoaded"); if (adView.getVisibility() == View.GONE) { adView.setVisibility(View.VISIBLE); } View adsContainer = activity.findViewById(R.id.container); adsContainer.setPadding(adsContainer.getPaddingLeft(), adsContainer.getPaddingTop(), adsContainer.getPaddingRight(), adView.getHeight() + 8); } }); } } }
[вернуться к содержанию]
Заключение
Вот и все. Суть статьи — поделиться с обществом своей идеей и ее реализацией. А также получить приглашение на Хабрахабр, если кому-то понравится то, чем я поделился. Буду рад и благодарен за ваше мнение в вопросах доработки кода и/или идеи. Если понадобится дополнительное пояснение — пишите, я в кратчайшие сроки внесу правки в статью или дам ответ в комментариях.
Ссылку на приложение по понятным причинам не публикую в открытом доступе. Этика есть этика! Кому нужно — дам в лс.
Статистика AdMob пока еще сырая, с момента внедрения данной альтернативы прошло всего ~2 недели. Не все пользователи обновились. Но точно есть те, кто пользуется таким способом отключения баннера внизу.
UPDATE: Ссылка на статью с результатами ИТОГ 3-х месяцев: Альтернатива платному отключению рекламы в бесплатном приложении Android
Благодарю всех, кто дочитал статью до конца!
