Привет, Хабр. Данная статья адресована к постигающим искусство Android-разработки, как и я.

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

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

Задача была решена следующим образом.

Напишем тестовое приложение.Создайте проект, с пустой активностью.

Форма:

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="ru.alerttest.MainActivity">

    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Автоматический  запуск"
        android:id="@+id/checkBox"
        android:checked="true"
        android:layout_below="@+id/button"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Ручной запуск"
        android:id="@+id/button"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Очень важная опция"
        android:id="@+id/checkBox2"
        android:checked="true"
        android:layout_below="@+id/checkBox"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
</RelativeLayout>


Создадим новый IntentService, с именем UniversalService.

В MainActivity внесем изменения в onCreate:

onCreate
    CheckBox checkBox, checkBox1;
    SharedPreferences sPref;
    SharedPreferences.Editor editor;
 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkBox = (CheckBox)findViewById(R.id.checkBox);

        sPref = getSharedPreferences("AlertTest",MODE_PRIVATE);
        editor = sPref.edit();
        editor.putBoolean("chbAutomatic", checkBox.isChecked());
        editor.putBoolean("success", checkBox.isChecked());
        editor.commit();

     checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                editor.putBoolean("chbAutomatic", checkBox.isChecked());
                editor.commit();
                setServiceAlarm(MainActivity.this);
            }
        });

        checkBox1 = (CheckBox)findViewById(R.id.checkBox2);

        checkBox1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                editor.putBoolean("success", checkBox1.isChecked());
                editor.commit();
                setServiceAlarm(MainActivity.this);
            }
        });

        Button button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("AlertTest", "Загрузка в ручном режиме");
                Intent intent = new Intent(MainActivity.this, UniversalService.class);
                startService(intent);
            }
        });
    }


И добавим метод:

public void setServiceAlarm(Context context){

        SharedPreferences settings = getSharedPreferences("AlertTest", MODE_PRIVATE);
        Boolean automaticSynchronize = settings.getBoolean("chbAutomatic", false);
        Intent intent = new Intent(context, UniversalService.class);
        PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        Integer period = 5;
        if(automaticSynchronize){
            alarmManager.setRepeating(AlarmManager.RTC, System.currentTimeMillis(), period*1000, pendingIntent);
            Log.d("AlertTest", "Загрузка в автоматическом режиме, период: "+period*1000);
        } else {
            Log.d("AlertTest", "Загрузка отменена");
            alarmManager.cancel(pendingIntent);
            pendingIntent.cancel();
        }
    }

В данном коде (в методе onCreate), мы сохраняем в Общие Настройки, с именем «AlertTest», состояние чекбоксов, и (в методе setServiceAlarm) запускаем таймер с периодичностью 5 секунд, на выполнение службы UniversalService.

Также, мы хотим получать ответ от службы, об успешности выполнения операции.

Для этого добавим в MainActivity класс:

 public class MyBroadRec extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Boolean result = intent.getBooleanExtra(UniversalService.EXTRA_KEY_OUT, false);
            Intent intentRec = new Intent(MainActivity.this, UniversalService.class);
            if(!result){
                Log.d("AlertTest", "Новая попытка");
                startService(intentRec);
            }
        }
    }

И в конец onCreate добавим:

     MyBroadRec myBroadRec = new MyBroadRec();
        IntentFilter intentFilter = new IntentFilter(UniversalService.ACTION_MYINTENTSERVICE);
        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
        registerReceiver(myBroadRec, intentFilter);
        setServiceAlarm(MainActivity.this);

А в UniversalService поменяем код на следующий:

UniversalService
package ru.alerttest;

import android.app.IntentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

public class UniversalService extends IntentService {

    public static final String EXTRA_KEY_OUT = "EXTRA_OUT";
    public static final String ACTION_MYINTENTSERVICE = "ru.timgor.alerttest.RESPONSE";


    public UniversalService() {
        super("UniversalService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d("AlertTest", "Начало загрузки");
        if(!verify()){
            Intent responseIntent = new Intent();
            responseIntent.setAction(ACTION_MYINTENTSERVICE);
            responseIntent.addCategory(Intent.CATEGORY_DEFAULT);
            responseIntent.putExtra(EXTRA_KEY_OUT, false);
            Log.d("AlertTest", "Загрузка не произошла");
            sendBroadcast(responseIntent);
        } else{
            Log.d("AlertTest", "Загрузка прошла успешно");
        }
    }

    public boolean verify(){
        SharedPreferences settings = getSharedPreferences("AlertTest", MODE_PRIVATE);
        Boolean success = settings.getBoolean("success", false);
        return success;
    }
}


Как видите, метод verify, эмулирует нашу задачу. В том случае, если она выполнена неуспешно — создается Intent, запускающий MyBroadRec, который опять стартует нашу службу. Настроить мы можем через чекбокс «Очень важная опция». Если мы переведем чекбокс «Автоматический запуск» в неактивное состояние, то работа таймера будет прекращена. Как только задача будет выполнена — переходим в штатный режим.

Но у нас еще осталась задача автозапуска нашего приложения. Для этого создаем класс UniversalReceiver наследующий от BroadcastReceiver:

package ru.alerttest;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;

public class UniversalReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("AlertTest", "Произошла смена статуса");
        Intent intentNew = new Intent(context, MainActivity.class);
        intentNew.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intentNew);
    }
}

И в манифесте добавляем следующие строки:

        <receiver android:name=".UniversalReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            </intent-filter>
        </receiver>

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

P.S.: Классы полностью:

MainActivity
package ru.alerttest;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;

public class MainActivity extends AppCompatActivity {
    CheckBox checkBox, checkBox1;
    SharedPreferences sPref;
    SharedPreferences.Editor editor;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkBox = (CheckBox)findViewById(R.id.checkBox);

        sPref = getSharedPreferences("AlertTest",MODE_PRIVATE);
        editor = sPref.edit();
        editor.putBoolean("chbAutomatic", checkBox.isChecked());
        editor.putBoolean("success", checkBox.isChecked());
        editor.commit();

     checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                editor.putBoolean("chbAutomatic", checkBox.isChecked());
                editor.commit();
                setServiceAlarm(MainActivity.this);
            }
        });

        checkBox1 = (CheckBox)findViewById(R.id.checkBox2);

        checkBox1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                editor.putBoolean("success", checkBox1.isChecked());
                editor.commit();
                setServiceAlarm(MainActivity.this);
            }
        });

        Button button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("AlertTest", "Загрузка в ручном режиме");
                Intent intent = new Intent(MainActivity.this, UniversalService.class);
                startService(intent);
            }
        });

        MyBroadRec myBroadRec = new MyBroadRec();
        IntentFilter intentFilter = new IntentFilter(UniversalService.ACTION_MYINTENTSERVICE);
        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
        registerReceiver(myBroadRec, intentFilter);
        setServiceAlarm(MainActivity.this);
    }

    public class MyBroadRec extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Boolean result = intent.getBooleanExtra(UniversalService.EXTRA_KEY_OUT, false);
            Intent intentRec = new Intent(MainActivity.this, UniversalService.class);
            if(!result){
                Log.d("AlertTest", "Новая попытка");
                startService(intentRec);
            }
        }
    }



    public void setServiceAlarm(Context context){

        SharedPreferences settings = getSharedPreferences("AlertTest", MODE_PRIVATE);
        Boolean automaticSynchronize = settings.getBoolean("chbAutomatic", false);
        Intent intent = new Intent(context, UniversalService.class);
        PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        Integer period = 5;
        if(automaticSynchronize){
            alarmManager.setRepeating(AlarmManager.RTC, System.currentTimeMillis(), period*1000, pendingIntent);
            Log.d("AlertTest", "Загрузка в автоматическом режиме, период: "+period*1000);
        } else {
            Log.d("AlertTest", "Загрузка отменена");
            alarmManager.cancel(pendingIntent);
            pendingIntent.cancel();
        }
    }
}


UniversalService
package ru.alerttest;

import android.app.IntentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

public class UniversalService extends IntentService {

    public static final String EXTRA_KEY_OUT = "EXTRA_OUT";
    public static final String ACTION_MYINTENTSERVICE = "ru.timgor.alerttest.RESPONSE";


    public UniversalService() {
        super("UniversalService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d("AlertTest", "Начало загрузки");
        if(!verify()){
            Intent responseIntent = new Intent();
            responseIntent.setAction(ACTION_MYINTENTSERVICE);
            responseIntent.addCategory(Intent.CATEGORY_DEFAULT);
            responseIntent.putExtra(EXTRA_KEY_OUT, false);
            Log.d("AlertTest", "Загрузка не произошла");
            sendBroadcast(responseIntent);
        } else{
            Log.d("AlertTest", "Загрузка прошла успешно");
        }
    }

    public boolean verify(){
        SharedPreferences settings = getSharedPreferences("AlertTest", MODE_PRIVATE);
        Boolean success = settings.getBoolean("success", false);
        return success;
    }
}


UniversalReceiver
package ru.alerttest;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;

public class UniversalReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("AlertTest", "Произошла смена статуса");
        Intent intentNew = new Intent(context, MainActivity.class);
        intentNew.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intentNew);
    }
}




P.P.S.: Внес изменения в BroadcastReceiver, на ошибки, которые указали мне Handy и BlackStream.
Сейчас разбираюсь с SyncAdapter.
Поделиться с друзьями
-->

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


  1. BlackStream
    07.09.2016 16:38
    +2

    public class UniversalReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("AlertTest", "Произошла смена статуса");
            MainActivity mainActivity = new MainActivity();
            mainActivity.setServiceAlarm(context);
        }
    }
    

    Вы же это не серьезно?

    Рекомендую обратить внимание на реактивный вариант, чтото типа:
    Observable
                    .interval(REFRESH_INTERVAL, TimeUnit.SECONDS)
                    .subscribeOn(Schedulers.newThread())
                    .takeWhile(new Func1<Long, Boolean>() {
                        @Override
                        public Boolean call(Long aLong) {
                            return someCondition();
                        }
                    })
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe...
    



    1. Snakecatcher
      07.09.2016 17:10

      Подробнее пожалуйста. :) Для котов, попонятнее :)


      1. BlackStream
        07.09.2016 17:55

        MainActivity mainActivity = new MainActivity();

        нельзя создавать Activity явно, да еще и для того что бы дергать паблик метод


      1. Handy
        07.09.2016 20:24

        Для начала, если Вам нужно создать и запустить новую активити, то нужно воспользоваться примерно следующим кодом:

        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        

        Ну и в принципе с onReceive() в BroadcastReceiver нужно быть очень осторожным, потому что у него есть жесткие ограничения, например, на время выполнения кода.


        1. Snakecatcher
          07.09.2016 21:02

          Точно, это я накосячил. Спасибо. Поправлю.


  1. ivazhnov
    07.09.2016 17:08

    Почему нельзя было воспользоваться SyncAdapter?


    1. Snakecatcher
      07.09.2016 17:09
      -1

      Пожалуйста примеры, ссылки.
      Я ж не волшебник, а только учусь. :) Всегда рад возможности улучшить код.


      1. ivazhnov
        07.09.2016 22:39

        Пожалуйста
        Sync Adapter делает почти все, что нужно из коробки. Правда, помимо всего прочего, придется хоть немного разобраться еще и с AccountManager и ContentProvider


        1. Snakecatcher
          07.09.2016 23:50

          Понял, вы имеете в виду, для отлова ситуации с отключением/подключением сети.
          Спасибо. Буду разбираться.


  1. Dimezis
    07.09.2016 19:05

    Ужас, кто это плюсует? Кто-то же может воспринять всерьез.


    1. Snakecatcher
      07.09.2016 21:01

      Хорошо. Было бы приятно выслушать критику специалиста.
      Что бы вы посоветовали?
      Какие участки кода, больше всего не нравятся?


      1. Dimezis
        08.09.2016 11:41
        +2

        Создание Activity вручную. Да еще и чтобы просто вызвать паблик метод. Вы хоть представляете на сколько тяжелый это компонент и на сколько бессмысленно это делать? Не говоря уже о том, что это ужасно с точки зрения архитектуры и вообще создавать Activity — ответственность ОС?

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

        Код стайл, форматирование, название переменных, классов.

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

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


        1. Snakecatcher
          08.09.2016 13:14
          -1

          Я поправил участок с вызовом Activity. Там накосячил. Сейчас через интент.
          Насчет кодстайла — это тестовое приложение, где никаким образом не заморачивался с названиями переменных и классов.
          Естественно, в полноценном проекте я не допускаю такого именования.

          С обертками не понял, что вы имели в виду. Можно поподробнее?

          С расходом батареи, спасибо ivazhnov и MetAmfetamin — разбираюсь с эффективным интструментом. Склоняюсь все-таки к GcmNetworkManager.


          1. Dimezis
            08.09.2016 14:07
            +1

            Я понимаю, что приложение тестовое, но вы же все-таки статью пишите, на которую кто-то будет равняться.
            Насчет оберток, я про разницу между boolean и Boolean, int и Integer и тд.


            1. Snakecatcher
              08.09.2016 14:58

              Спасибо, за уделенное время.
              Обязательно исправлюсь. :)


  1. Alex837
    07.09.2016 20:35
    +4

    Новички пишут статьи для новичков обеспечивая круговорот плохих практик написания приложений.

    Почти идеальное руководство по высаживанию батареи

    <action android:name=«android.net.conn.CONNECTIVITY_CHANGE» /> в манифесте будет стартовать приложение при каждой смене вышки и человека в дороге будет согревать теплый телефон.

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

    Ну а побудку телефона раз в 5 секунд оставим на совести автора, благо доз мод вырубит alarmManager и с какого-то апи оно вроде вообще не даст так часто запускать алярмы.


    1. Snakecatcher
      07.09.2016 20:59

      Спасибо за конструктивную критику.
      Я тот новичок, который хочет улучшить код, и уйти от плохих практик.
      Задача должна подцепиться к серверу, и выполнить закачку данных. Проблема в том, что сервер даёт доступ через раз, а то и через десять Поэтому, в предыдущей версии (не за моим авторством), пользователю приходилось самостоятельно нажимать на кнопку синхронизации. Это сильно раздражало пользователей, и я подумал, что лучше убрать с глаз долой синхронизацию.
      5 секунд это я для примера. Планируется около 5-10 минут.
      Подскажите, пожалуйста, а как тогда действовать в нижеприведённой ситуации?
      Имеем неусточивый интернет. Заполняем некую анкету. Если есть подключение к сети, то отправляем её на сервак. Если нет — то складируем в определённой папке.
      На данный момент, я с помощью: <action android:name=«android.net.conn.CONNECTIVITY_CHANGE» />, отлавливаю, что сеть появилась и отправляю данные.
      Но вы указали, что это неэффективно. Как быть?


      1. MetAmfetamin
        07.09.2016 21:53
        +1

        На мой взгляд идея правильная, просто неправильно выбраны средства для «синхронизации». Для решения проблемы с перезапуском загрузки я бы рассмотрел варианты:

        • простой — JobScheduler (Android 5+) или GcmNetworkManager (если нужна поддержка Android < 5). Проблема с GcmNetworkManager — это необходимость наличия play services на устройстве.
        • более сложный — SyncAdapter



        1. Snakecatcher
          08.09.2016 08:54

          Спасибо. Как раз, комментатор выше преддложил тоже SyncAdapter.
          Буду его изучать. :)


      1. Alex837
        08.09.2016 14:25

        Если установлен target api 24 то на андроид 7 приложении вообще не получит <action android:name=«android.net.conn.CONNECTIVITY_CHANGE» /> О такой штуке в манифесте лучше вообще забыть пока есть хоть какие-то другие способы. Правильное использование этого бродкаста подписываться перед передачей данных отписываться после.

        >Как быть?

        Читать developer.android.com. Там совсем плохого не советуют. В коментах уже предложили приемлемые решения с JobScheduler и SyncAdapter. К ним остается добавить, что при ошибках лучше делать таймаут и увеличивать интервал экспоненциально(exponential backoff) при каждой новой попытке (например в 2 раза до некоторого предела) и ограничить количество попыток после чего передать управление пользователю. Пользователь может находиться в условия плохой сети, и от того что приложение будет долбится бесконечно хорошего будет мало пока пользователь не найдет интернет получше. Т.е. если сервер никак нельзя починить я бы сделал пару попыток с небольшим интервалом, а потом интервал увеличивал.


        1. Snakecatcher
          08.09.2016 14:56

          Понял, спасибо.
          Проблема в том, что пользователью никак нельзя давать полный доступ к автоматической синхронизации. Желательно, чтобы все было «под капотом».
          Насчет сети, у меня стоит проверка на доступность сети вообще, как таковой. Если подключения нет, то — попытки подключиться к серверу прекращаются.
          Сделаю так: оставлю ограничение по количеству подключений (например 10 раз, с таймаутом 0,1 секунда). Если подключение инициировалось в автоматическом режиме, то никаких сообщений выводить не буду, просто буду спокойно ждать следующего тика таймера.
          Если же синхронизация была запущена вручную — то выведу сообщение, об ошибке, и её причине.


  1. sbnur
    07.09.2016 23:29

    Семь раз прочти — один раз напиши