Доброго времени суток, Хабрахабр!

Меня зовут Александр, я работаю тренером по питанию, а в свободное время, по вечерам — инди разработчик под ОС Android. Сегодня хочу с вами поделиться опытом реализации альтернативного платному способу отключения рекламы в приложении — отключение рекламы за просмотр рекламы (AdMob Rewarded Video Ads). Интересно? Тогда добро пожаловать под кат.


Как все было ?


В далеком 2013 году я решил заняться разработкой приложений под Android, начал читать тематические книги, статьи, смотрел видео уроки и т.д. Написал первое недоприложение и приуныл, т.к. хотелось сделать что-то полезное, нужное обществу, а идей не было. В 2014 меня знакомый попросил разработать для него мобильный справочник по синтаксису платформы Arduino (там С язык). С огромным желанием я взялся за этот проект и реализовал первую версию для Android 3.0+. Через время решено было усовершенствовать ее, и так появилась вторая версия (для Android 4.0+). Обе они бесплатные с баннером от AdMob внизу и платным его отключением. Все было хорошо, пока мне не стали писать, что ~150-170 рублей РФ дороговато для отключения рекламы навсегда в их любимом справочнике. На что я ответил «бартерным» решением вопроса — пользователь может отключить баннер внизу на время за просмотр видео рекламы от AdMob.
[вернуться к содержанию]

Реализация, часть 1: Принцип работы (словами)


При запуске приложения пользователю будет показан Dialog, с предложением отключить рекламу, если, конечно, он ранее ее не отключил или отсутствует подключение к сети Интернет. В случае положительного ответа, приложение показывает фрагмент с кнопками, с помощью которых и можно выполнить отключение рекламы в приложении удобным способом.
[вернуться к содержанию]

Реализация, часть 2: Внешний вид


Диалог с предложением отключить рекламу
Диалог с предложением отключить рекламу

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

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

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

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

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

[вернуться к содержанию]

Реализация, часть 3: Принцип работы (java код)


Код главной Activity

public class ActivityMain extends AppCompatActivity  {
    private static boolean internet = CheckURLConnection.isNetworkAvailable();
    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

        // 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);
        NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
        return activeNetworkInfo != null;
    }
}


Код класса 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 недели. Не все пользователи обновились. Но точно есть те, кто пользуется таким способом отключения баннера внизу.

Благодарю всех, кто дочитал статью до конца!

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


  1. TrllServ
    24.02.2018 15:53

    Правильно ли я понимаю, что это сделано чтоб можно смотреть рекламу с «наземного» инета, что б потом не тратить трафик с мобильного?

    PS:
    Хорошая идея. Было б вообще хорошо, если развить далее: «посмотреть» на пару дней или неделю вперед. Но думаю, тут уже можно столкнуться с ограничением со стороны рекламодателя.


    1. web_alex Автор
      24.02.2018 16:12

      Правильно ли я понимаю, что это сделано чтоб можно смотреть рекламу с «наземного» инета, что б потом не тратить трафик с мобильного?
      Я просто пошел людям навстречу. Есть те, кто не хочет платить за отключение рекламы, но она им мешает. Выход всегда есть! Поэтому я дал возможность избавиться на время от баннера внизу путем просмотра видео рекламы. Один раз посмотрел — нет баннера 1 час. Посмотрел больше — иной временной диапазон без рекламы внизу. И людям приятно, и мне лишняя копеечка за просмотры.

      В играх разработчики дают своим пользователям золото, монеты или дополнительные жизни, если те посмотрят 1-2 ролика. Я же даю то, что нужно моим пользователям. «И волки сыты, и овцы целы!»

      PS:
      Хорошая идея. Было б вообще хорошо, если развить далее: «посмотреть» на пару дней или неделю вперед. Но думаю, тут уже можно столкнуться с ограничением со стороны рекламодателя.
      Я хотел так сделать, но столкнулся с проблемой — рекламы для текущего региона, где пользователь находится, сейчас может не быть в нужном количестве. Поэтому решено было не развивать механизм хранения уже выполненных просмотров, а запустить в тестовом режиме то, что я описал в статье.

      Благодарю Вас за конструктивный комментарий!


  1. petrovichtim
    24.02.2018 17:37

    Спасибо за статью, интересна экономика этого решения. Стоит ли оно того?


    1. web_alex Автор
      24.02.2018 18:30

      Большую роль в данном решении играет возможность отключать рекламу бесплатно. Как я говорил в статье, что пользователи не хотят платить и жалуются в отзывах к приложению. Сопровождают свои комментарии заниженными оценками. То есть само приложение их устраивает на 1`000`000%, но реклама (маленький баннер внизу) + еще и разработчик жлоб, хочет нажиться, просит денег за отключение. Поэтому 3 или 2 звезды вообще. Сначала я писал аргументы и доводы в ответ, старался как-то дать понять, что реклама внизу и платное ее отключение — нормально! Но потом понял, что это бессмысленно. Так я и пришел к тому, что дал пользователю возможность самому принимать решение: платно или бесплатно он будет избавляться от баннера внизу.

      Если говорить о цифрах, то имеем следующее (период 01.02 — 24.02.2018, сегодня):
      bottom banner:
      * 49`047 показов
      * 217 кликов
      * 0.44% CTR
      * 0.05$ за клик
      * сумма: 10.68$

      rewarded video ads:
      * 3`807 показов
      * 7 кликов
      * 0.18 CTR
      * 0.27$ за клики
      * сумма: 1.92$

      Видно, что сам баннер внизу ГОРАЗДО прибыльный, оно то и понятно. Он расположен всегда внизу экрана, а до видео рекламы нужно еще и добраться по пунктам меню или через положительный ответ на вопрос в DialogFragment, который я показываю пользователю при старте приложения. Код, кстати, приложил к статье.

      Еще немного цифр для Вас:
      активных установок: 18`428 из общего числа установок — 62`004
      средняя оценка: 4.66 по результатам 1`452 оцениваний

      Спасибо за статью, ...
      Пожалуйста. Рад тому, что Вам она понравилась.


      1. schetilin
        24.02.2018 19:26

        Не про Ваше приложение, а про рекламу.

        То есть само приложение их устраивает на 1`000`000%, но реклама (маленький баннер внизу) ...

        Есть еще один неприятный момент. Этот маленький баннер внизу экрана содает жуткие тормоза. Что виновато? Может криво блок рекламы вставлен, может слабый телефон. Но во многих приложениях, при подключении к интернету (без интернета не показывает :)), наблюдаются фризы при смене баннера.


        1. web_alex Автор
          24.02.2018 20:46

          Никогда не замечал фризов рекламы.


        1. KodyWiremane
          25.02.2018 11:53

          Может, синхронный запрос какой-нибудь, или проверка в цикле. У нас же 5g уже везде, новые баннеры грузятся мгновенно, можно не париться с оптимизацией. А могли и ядра в фоновых процессах завязнуть. Появился интернет, все проснулись — и в сеть.


          1. web_alex Автор
            25.02.2018 12:39

            Появился интернет, все проснулись — и в сеть.
            Да, бывает и так. Весь смартфон подвисает в момент тотальной синхронизации данных на смартфоне и различных сервисов.


            1. schetilin
              25.02.2018 19:37

              Нет, микрофриз именно во время смены баннера на новый. Интернет 3g, да и то достаточно поганенький. (По ощущениям — ждет загрузки в потоке приложения.)


              1. web_alex Автор
                25.02.2018 20:44

                Может быть и так.


          1. Noserdan
            27.02.2018 20:13

            О чем вы? У нас и LTE до сих пор не везде, а в иных местах и ешечка тормозит


            1. web_alex Автор
              27.02.2018 20:15

              LTE ??? В Украине только стал нормального работать 3G. И то не везде еще есть покрытие, есть соты с EDGE.


      1. TrllServ
        24.02.2018 19:58

        Когда реклама сразу на экране, сверху или снизу, она мешает не только эстетически, но и вполне себе фукционально — например случайные тапы могут увести по ссылке рекламной. Если это во время игры приводит к потере контроля. А дальше зависит от типа игры, в интерактивных играх это всегда поражение, и вполне ожидаемо после 2-3 раз на эмоциях пользователь проставляет 2-3 звезды.

        Собсно, это я к тому, что когда реклама на экране, больше переходов может быть не от желания её просмотреть, а от мисскликовмисстапов.


        1. web_alex Автор
          24.02.2018 20:48

          Поэтому Google/AdMob установили привила, что нельзя ставить контролы около баннеров. И нужно явно отделять баннер от UI. Я, к примеру, добавил отступ в 8dp (в коде есть).


          1. TrllServ
            24.02.2018 23:18

            А если это «всевдошутер» где тапаешь в любом месте экрана, и если ты ещё не наловчился, запросто можно тыкнуть в рекламу? тоже самое верно для экранов 4-4.5, как у меня, потому что лопата на 5.5-6 уже не очень комфортна в виде мобилки.
            Для тех целей где нужен экран больше у меня 7" планшетка имеется :)
            Мисстапы случаются у многих, особенно поначалу (сужу исключительно по своему опыту и пробному приложению).


            1. web_alex Автор
              24.02.2018 23:36

              Я с Вами согласен на 1оо% на счет мисстапов! Главное, чтобы они случались не по вине неправильной расстановки контролов на экране.

              Хочу еще добавить к Вашему комментарию, что нормальный разработчик не будет вставлять баннер в шутере или иной динамичной игре. Для них существуют межстраничные (Interstitial Ads) и видео (Rewarded Video Ads) рекламы, которые показываются пользователю после проигрыша или в режиме паузы. Показывать баннер в динамичной игре — хардкор какой-то. Мистапов будет ооочень много и «мигания» объявлений будут отвлекать внимание игрока.

              А мое решение в таких играх просто избавит от межстраничных или видео реклам после проигрыша. Хотя не каждый захочет его применять, т.к. выгодней в играх за просмотр видео рекламы давать монеты/жизни/золото/ключи/и другие плюшки.

              // я так это вижу, другие разработчики могут считать по-другому))


    1. web_alex Автор
      24.02.2018 18:44

      Стоит ли оно того?
      Мое решение хорошо подойдет для приложений с бОльшей аудиторией. В моем же случае, если говорить только о доп. доходе с рекламы, не стоит оно того. Пока не стоит. Посмотрим, что будет через 1-2 месяца, когда пользователи обновятся и привыкнут. Когда разрабатывал такой вид отключения, моя цель была — убавить негатив в комментариях и сохранить рейтинг приложения в Google Play.


  1. exformat
    24.02.2018 20:43

    Спасибо за статью и за идею!)


    1. web_alex Автор
      24.02.2018 20:44

      Пожалуйста. Пользуйтесь. Если реализуете/придумаете какие-то форки, то обязательно дайте сообществу знать! :-) Интересно.


  1. Kirill_NN
    24.02.2018 23:36

    Интересный метод. Отпишись пожалуйста после месяцев трех-четырех о результатах


    1. web_alex Автор
      24.02.2018 23:38

      Да, конечно. Мне самому хочется узнать и расшарить эти данные.


      1. web_alex Автор
        25.02.2018 11:51

        Что именно не так?


  1. a-tk
    25.02.2018 11:26

    Статическая переменная для проверки наличия интернета… Что-то здесь не так.


    1. web_alex Автор
      25.02.2018 12:40

      Что именно не так?


      1. anegin
        27.02.2018 01:58

        Статическая переменная internet будет инициализирована только один раз при создании класса активити. Какой в ней смысл?


        1. web_alex Автор
          27.02.2018 02:04

          Во время создания ActivityMain получаем информацию о наличии доступа в сеть Интернет. Если есть, то делаем по коду, иначе пропускаем все.
          В классе ActivityMain есть еще несколько мест, где нужно считать значение из internet. Поэтому переменная является глобальной.

          anegin, или Вы хотили сказать, что модификатор static лишний?


          1. anegin
            27.02.2018 14:38

            К тому моменту, когда нужно будет прочитать значение из переменной internet, ее значение может быть уже не актуальным, т.е. сети уже может не быть.
            В нужных местах достаточно вызывать CheckURLConnection.isNetworkAvailable(), чтобы узнавать о наличии сети по факту в нужный момент.
            А так получается, что наличие сети проверяется один раз при создании класса активити и зачем-то сохраняется в переменной, к тому же еще private static.

            Static-поля и методы нужны для доступа к ним без создания экземпляра класса. Например, в static-методах этого класса или inner-static-классах — это если поле private. Или, например, как константы для доступа из других классов — это если поле не private.


  1. bogotoff
    27.02.2018 01:38

    На словах выглядит интересно, однако у меня есть одно подозрение.
    Если пользователь посмотрит рекламу 5 раз подряд, и он не кликнет по ссылке, то его ценность сильно уменьшится и этому пользователю будут показываться дешевая реклама. Т.е. Вы получите меньше денег за рекламу, чем если бы вы растянули эти 5 показов на больший временной отрезок.


    1. web_alex Автор
      27.02.2018 01:58

      Дельное замечание! Можете дать ссылку на подтверждение Ваших слов?

      В моем решении есть два зайца, и обоих хочется поймать! Первый, самый ценный/важный, — заяц-хранитель рейтинга приложения. Он выполняет функции успокаивания пользователей в вопросах отключения рекламы. Платить не хочешь — смотри рекламу.

      Второй заяц — не менее важная персона, а именно: доход с рекламы и ее платном отключении. И у этого зайца появляются какие-то прихрамывания — Ваше замечание касательно удешевления рекламы.
      Но стоит помнить, что платят то за клики по баннеру, а не просмотры.
      Поэтому я Вас прошу предоставить подтверждение/факты к Вашему дельному замечанию.
      Если Ваше утверждение верно на 1оо%, то придется переписывать код.


      1. bogotoff
        27.02.2018 03:19

        Но стоит помнить, что платят то за клики по баннеру, а не просмотры.

        Я говорю про видео рекламу, и тап по нему после просмотра, а не про баннер, который висит все время в меню.

        Можете дать ссылку на подтверждение Ваших слов?

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

        Даже если я прав, возможно не стоит переписывать код. Здесь говорится, что дешевая реклама в некоторых случаях выгоднее. Стоит изучить аналитику в admob.


        1. web_alex Автор
          27.02.2018 12:33

          Хорошая мини статья! Благодарю Вас за то, что поделились!


  1. HueyOne
    27.02.2018 12:14

    Когда уже запилят адблок для мобилы...


    1. web_alex Автор
      27.02.2018 12:22

      Вы же понимаете, что для кого-то реклама — источник дохода?! И блокируя ее показ, Вы лишаете человека/компанию заработка. Это равносильно пиратству. Кто-то потратил часть своей жизни на создание, к примеру, цифрового продукта, и хочет, чтобы за его труд потребители заплатили. А пираты берут и крадут у него эту возможность.

      Чтобы проникнуться этой мыслью, нужно сначала самому что-то создать для общества.

      Каюсь! Я тоже раньше не уважал чужой труд! Сейчас же все наоборот!
      Кстати, чтобы урегулировать «баталии» между правообладателями и пирами, придумали Open Source проекты. Они бесплатны в распространении, понятны в принципах работы. Но для их существования нужны денежные вливания, то есть донат/пожертвования. Как Вы относитесь к Open Source и донату?


      1. TrllServ
        27.02.2018 15:16

        Игроки которые не покупают дополнительный контент в free to play, видимо тоже «равносильны пиратам»?
        А может не нужно везде пихать это избитое слово?
        Украсть «возможность» — не возможно.

        Есть и третья возможность, которой часто пользуются, те кто вышел из пиратсва (а это между делом, большая часть СНГ) — бесплатно для частного пользования, платно для коммерческого.

        Чем больше будет такого софта, тем спокойнее и легче будет жить в цифровую эпоху.


        1. web_alex Автор
          27.02.2018 20:09

          Игроки которые не покупают дополнительный контент в free to play, видимо тоже «равносильны пиратам»?
          Нет, они не пираты.

          А может не нужно везде пихать это избитое слово?
          Украсть «возможность» — не возможно.
          Никогда ранее не слышал такого.

          Есть и третья возможность, которой часто пользуются, те кто вышел из пиратсва (а это между делом, большая часть СНГ) — бесплатно для частного пользования, платно для коммерческого.
          И как узнать о типе использования? Ведь можно на дому заниматься коммерцией.


    1. leshakk
      27.02.2018 12:30

      Есть AdAway, практически полное уничтожение рекламы в андроиде. Причём никакой нагрузки на систему: домены, с которых качается реклама, блокируются на уровне DNS ( прописываются в /etc/hosts )
      Списки доменов обновляются по сети, аналогично адблоку.
      Недостаток: нужен рут


      1. web_alex Автор
        27.02.2018 20:13

        И как тогда монетизировать свой труд, если отовсюду народ старается что-то украсть, заблокировать и т.д.?

        Как бы Вы, leshakk, монетизировали свое приложение, зная, что можно заблокировать рекламу, взломать покупку полной версии и т.д.?


        1. leshakk
          27.02.2018 20:50

          Уж точно не рекламой.
          Как пользователь скажу, что если мне ОЧЕНЬ понравилась прога,
          я лучше заплачу за неё.

          Вот пара примеров:

          1.Утилита для диагностики вариаторов Nissan по OBDII.
          Написана энтузиастом с автомобильного форума, писалась по сути «для себя».
          Первые версии были бесплатны. Потом автор сдела программу платной,
          но предыдущие версии оставил в свободном доступе на сайте.
          По большому счёту, функционал бесплатной версии вполне достаточен,
          дальнейшие отличаются только большим удобством, но я купил,
          чтобы поблагодарить автора, учитывая, что стоимость программы в 5-10 раз ниже суммы,
          которую просят за «компьютерную диагностику» в сервисе.

          2. Программа учёта расхода топлива. Тоже написана энтузиастом.
          Есть как платная, так и бесплатная версии.
          В платной добавлена возможность синхронизации данных с облаком.
          Стоимость символическая. Купил.

          И что характерно, даже в бесплатных версиях этих программ не было никакой рекламы.
          Чтож, их авторы уважают своих пользователей, потому и сами достойны уважения.