среда, 13 ноября 2013 г.

"Вечный Service" в Android

Когда мы делаем приложения, мы руководствуемся самыми гумаными соображениями. Мы хотим облегчить жизнь нашим клиентам, помочь им, защитить, вооружить против любых проблем этого ужасного мира. Но клиенты почему-то не хотят покоряться нашему мудрому руководству. Закрывают наше приложения, останавливают его, находят в списке "запущенных процессов" через настройки и опять-таки останавливают. Ну, как дети малые, ей-богу ;) Если вы в своей всеобъемлющей мудрости хотите спасти своих клиентов от их самих, вам наверняка прийдётся защититься от их жалких попыток упавлять своим смартфоном. Как же это сделать? Перенести логику в Service, чтобы закрытие приложения не останавливало его работу? Недостаточно. Сделать в нём вечный цикл, вызвать startService() в onDestroy() - тоже. В конце концов эти неразумные существа могут перезагрузить смартфон и мы утратим над ними власть не сможем помогать им. Можно, конечно ловить событие загрузки (и ещё стопицот других системных событий) и поднимать наш сервис. Однако система на то и система, чтобы жить своими, непредсказуемыми событиями, а значит никакой гарантии, что эти события произойдут когда нам нужно, увы, нет. Как же нам быть? Ответ очевиден:



AlarmManager

Этот механизм позволяет нам не ждать милостей от природы событий от системы, а "заказывать" ей свои собственные события. Мы говорим системе "разбудить" наше приложение через Х секунд и делаем то что нам нужно в течении этих Х секунд а потом завершаем работу. Если пользователь или система убили нас раньше, их радость будет недолгой. Наше приложение возродится как феникс и продолжит свою работу. Такая схема позволяет нам гарантированно и независимо от прихотей платформы оставаться в памяти всегда. Вот как это выглядит:

public class MyActivity extends Activity implements View.OnClickListener{
 
    private ToggleButton mTb;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout root = new LinearLayout(this);
        root.setGravity(Gravity.CENTER);
        mTb = new ToggleButton(this);
        mTb.setOnClickListener(this);
        mTb.setTextOff("Disabled");
        mTb.setTextOn("Enabled");
        root.addView(mTb,
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
 
        setContentView(root, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT));
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        mTb.setChecked(EternalService.isRunning(this));
    }
 
    @Override
    public void onClick(View view) {
        if (EternalService.isRunning(this)) {
            EternalService.Alarm.cancelAlarm(this);
            stopService(new Intent(this, EternalService.class));
            mTb.setChecked(false);
        } else {
            EternalService.Alarm.setAlarm(this);
            startService(new Intent(this, EternalService.class));
            mTb.setChecked(true);
        }
    }
}

Это пока только Activity c кнопкой-переключателем. Сам сервис:

public class EternalService extends Service {
 
    private static final String TAG = EternalService.class.getName();
 
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.v(TAG, "Service started!");
        // TODO: тут делаем всё что нам хочется...
        return super.onStartCommand(intent, flags, startId);
    }
 
    public static boolean isRunning(Context ctx) {
        ActivityManager manager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (EternalService.class.getName().equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.v(TAG, "Service stopped!");
    }
 
    public static class Alarm extends BroadcastReceiver {
 
        public static final String ALARM_EVENT = "net.multipi.ALARM";
        public static final int ALARM_INTERVAL_SEC = 5;
 
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.v(TAG, "Alarm received: "+intent.getAction());
            if (!isRunning(context)) {
                context.startService(new Intent(context, EternalService.class));
            }
            else {
                Log.v(TAG, "don't start service: already running...");
            }
        }
 
        public static void setAlarm(Context context) {
            AlarmManager am=(AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
            PendingIntent pi = PendingIntent.getBroadcast(context, 0, new Intent(ALARM_EVENT), 0);
            am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 1000 * ALARM_INTERVAL_SEC, pi);
        }
 
        public static void cancelAlarm(Context context) {
            PendingIntent sender = PendingIntent.getBroadcast(context, 0, new Intent(ALARM_EVENT), 0);
            AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            alarmManager.cancel(sender);
        }
    }
}

И не забываем добавить в AndroidManifest (в тег application) строки:

<receiver android:name=".EternalService$Alarm" android:exported="true">
   <intent-filter>
       <action android:name="net.multipi.ALARM" />
   </intent-filter>
</receiver>
<service android:name=".EternalService" />

Но, шутки в сторону

Во-первых, лучший способ помогать своим пользователям - не создавать им новых проблем. Если вы всё-таки решили висеть в памяти их смартфона, то хотя бы не держите открытыми сетевые соединения и (не дай бог) не слушайте спутники GPS. Посадите клиенту батарею и потеряете клиента.
Во-вторых, сервис всё-таки не вечный. Приложение, которое "убили" в настройках перестаёт получать Broadcast-ы а значит уже не оживёт, пока клиент не запустит его снова.
И да, не ставьте слишком короткий интервал вызова вашего сервиса. В большинстве случаев ваш сервис будет ещё жив и вы его проигнорируете, а для системы такие вызовы совсем не "бесплатны".

5 комментариев:

  1. И как с помощью этого сервиса слушать хотя бы LocationManager.NETWORK_PROVIDER

    ОтветитьУдалить
    Ответы
    1. и как после вашего комментария понять хотя бы, что вы не зря занялись программированием.

      Удалить
  2. шедеврально.

    ОтветитьУдалить
  3. Отличный способ
    я для своих целей вызывал сразу сервис поместив этот код в onStartCommand

    Intent service_intent = new Intent(getApplicationContext(), getClass());
    service_intent.setAction(ACTION);

    AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
    PendingIntent pendingIntent = PendingIntent.getService(this, 1, service_intent, PendingIntent.FLAG_ONE_SHOT);
    alarmManager.cancel(pendingIntent);

    if(Build.VERSION.SDK_INT >= 19) {
    alarmManager.setExact(AlarmManager.RTC, System.currentTimeMillis() + delay, pendingIntent);
    } else{
    alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + delay, pendingIntent);
    }

    метод setRepeating как то не совсем точно отрабатывает интервал, было даже что иногда вообще не сработал!
    как по мне, точнее будет каждый раз новую задачу - alarmManager.setExact

    ОтветитьУдалить