Использование службы для продолжения таймера вне жизненного цикла приложения

Редактировать: Спасибо за ответы. В конце концов я нашел хорошее решение (которое я опубликовал ниже), которое использует службу переднего плана и широковещательные приемники для тех, кто заинтересован.

Исходный вопрос:

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

  1. Продолжайте таймер, даже если приложение закрывается
  2. Отправьте уведомление и разбудите телефон (если он спит), когда таймер достигнет своей продолжительности.

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

Для справки здесь мой класс таймера

public class MyTimer implements Runnable {
MainActivity activity;
Handler handler;
TextView timerView;
long current_time,duration;

public MyTimer(MainActivity activity){
    this.activity = activity;
    this.handler = new Handler();
    this.current_time = 0L;
    timerView = (TextView) activity.findViewById(R.id.timerValue);
}

public MyTimer startTimer(int duration){
    this.duration = duration;
    handler.postDelayed(this,1000);
    return this;
}
public MyTimer resetTimer(){
    timerView.setText("0:00");
    handler.removeCallbacks(this);
    return this;
}

@Override
public void run() {
    if(current_time == duration){
        Toast.makeText(activity,"Timer is done",Toast.LENGTH_SHORT).show();
        resetTimer();
        return;
    }
    current_time += 1000;
    int secs = (int) (current_time / 1000);
    int minutes = secs / 60;

    timerView.setText(Integer.toString(minutes) + ":" + String.format("%02d", secs%60));
    handler.postDelayed(this, 1000);
}
}

timerView и две кнопки для запуска/остановки

простой вид таймера

Я также думал о том, чтобы просто сохранить таймер в базе данных во время onStop/onDestroy и использовать системное время и его разницу между сохраненным временем, чтобы просто обновить таймер таким образом. Но это не решит проблему выдачи уведомления и/или пробуждения телефона.


person Fahad Hassan    schedule 13.08.2016    source источник
comment
вы можете сделать это с сервисом. или используйте alarmmanager и установите оставшееся время для alarmmanager, чтобы уведомить пользователя об уничтожении активности.   -  person ugur    schedule 14.08.2016


Ответы (2)


Примеры, которые вы найдете, не слишком сложны — для достижения желаемого вам понадобится:

  1. Связанный Service, который будет отслеживать прошедшее время и регистрировать тревогу с помощью AlarmManager
  2. Фрагмент/деятельность, которая может связывать вышеупомянутые Service и выполнять такие методы, как resetTimer(), startTimer(), getElapsedTime(). Вам нужно выполнить запрос к getElapsedTime() с помощью Handler, но тайм-аут в 1 секунду слишком велик (я бы использовал 0,1 секунды или что-то подобное).

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

public void startTimer(long duration) {
   mStartTime = System.currentTimeMillis();
   mDuration = duration;
   // register alarm with AlarmManager here
}

public long getElapsedTime() {
   return System.currentTimeMillis() - mStartTime;
}
person Vasiliy    schedule 14.08.2016

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

Итак, в резюме

  1. В классе обслуживания

    • Broadcasts the timer to main activity in which the MainActivity will receive it using a broadcastreceiver and updates the UI
    • Класс обслуживания использует собственный широковещательный приемник, чтобы проверить, включен или выключен экран телефона, и обновляет таймер, когда он выходит из спящего режима.
  2. В классе основной деятельности

    • Receive the broadcast sent from the timer service and update the UI
    • Другая логистика, например, когда регистрировать/отменять регистрацию широковещательного приемника и отправлять действия в службу для остановки/запуска.

Класс обслуживания:

//Timer service which uses a handler to monitor tick rate. Also uses a broadcast receiver
//to update the timer if the device was in sleep mode.
public class TimerService extends Service{
    Intent intent;
    public static final String TAG = TimerService.class.getSimpleName();
    private final Handler handler = new Handler();
    long currentTime, duration;
    long timeSinceLastOn, elapsedTimeSinceOff;

    @Override
    public void onCreate() {
        super.onCreate();
        currentTime = duration = elapsedTimeSinceOff = 0L;
        timeSinceLastOn = SystemClock.elapsedRealtime();
        intent = new Intent(Constants.ACTION.BROADCAST_ACTION);

        /**Starting Timer here**/
        handler.removeCallbacks(timerThread);
        handler.postDelayed(timerThread,0);
        /**********************/

        /**Broadcast receiver to check if the screen is on **/
        IntentFilter screenStateFilter = new IntentFilter();
        screenStateFilter.addAction(Intent.ACTION_SCREEN_ON);
        screenStateFilter.addAction(Intent.ACTION_SCREEN_OFF);
        registerReceiver(broadcastReceiver, screenStateFilter);
        /***************************************************/

    }

    @Override
    /**Depending on action issued by MainActivity either puts service in
     *foreground with duration or destroys the service**/
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(intent != null) {
            if (intent.getAction().equals(Constants.ACTION.STARTFOREGROUND_ACTION)) {
                if (intent.hasExtra(Constants.TIMER.DURATION))
                    duration = intent.getLongExtra(Constants.TIMER.DURATION, 0);
                startForeground(Constants.NOTIFICATION_ID.FOREGROUND_SERVICE, createTimerNotification());
            } else if (intent.getAction().equals(Constants.ACTION.STOPFOREGROUND_ACTION)) {
                stopForeground(true);
                stopSelf();
            }
        }
        return START_STICKY;
    }

    /**Thread the handler uses to push to message queue. This creates a timer effect.**/
    private Runnable timerThread = new Runnable() {
        @Override
        public void run() {
            if(currentTime == duration){
                stopSelf();
                return;
            }
            currentTime += 1000;
            sendTimerInfo();
            handler.postDelayed(this,1000);
        }
    };

    /**Broadcasts the timer in which the MainActivity will receive it and update the UI**/
    private void sendTimerInfo(){
        Log.d(TAG, "timer running: tick is " + currentTime);
        intent.putExtra(Constants.TIMER.CURRENT_TIME, currentTime);
        sendBroadcast(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"timer service finished");
        unregisterReceiver(broadcastReceiver);
        handler.removeCallbacks(timerThread);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /******************** Broadcast Receiver To Check if Screen is on**************************************/
    private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            handler.removeCallbacks(timerThread);
            /**If the screen is back on then update the timer and start it again**/
            if(intent.getAction().equals(Intent.ACTION_SCREEN_ON)){
                Log.d(TAG,"Screen is turned on");
                elapsedTimeSinceOff = SystemClock.elapsedRealtime() - timeSinceLastOn;
                Log.d(TAG," screen was off and updating current time by"+elapsedTimeSinceOff);
                currentTime += elapsedTimeSinceOff;
                handler.postDelayed(timerThread,0);
            }
            /**Turns off the timer when the screen is off**/
            else if(intent.getAction().equals(Intent.ACTION_SCREEN_OFF)){
                Log.d(TAG,"Screen is turned off");
                timeSinceLastOn = SystemClock.elapsedRealtime();
            }
        }
    };

    /**Since this is foreground service it must have a notification**/
    private Notification createTimerNotification() {
        Intent notificationIntent = new Intent(this, MainActivity.class);
        notificationIntent.setAction(Constants.ACTION.MAIN_ACTION);
        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                notificationIntent,0);

        Bitmap icon = BitmapFactory.decodeResource(getResources(),
                R.mipmap.ic_launcher);

        Notification notification = new NotificationCompat.Builder(this)
                .setContentTitle("Service Timer")
                .setTicker("Count up timer")
                .setContentText("timer")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(Bitmap.createScaledBitmap(icon, 128, 128, false))
                .setContentIntent(pendingIntent)
                .setOngoing(true)
                .build();
        return notification;
    }
}

Основная деятельность:

public class MainActivity extends Activity {

    TextView timerView;
    Intent timerService;
    //Example duration of 3minutes
    long currentTime, duration = 180000;

    @Override
    protected void onStart() {
        super.onStart();
        timerService = new Intent(this, TimerService.class);
        //Register broadcast if service is already running
        if(isMyServiceRunning(TimerService.class)){
            registerReceiver(broadcastReceiver, new IntentFilter(Constants.ACTION.BROADCAST_ACTION));
        }
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        Button startButton, stopButton;
        timerView = (TextView) findViewById(R.id.timerValue);
        startButton = (Button) findViewById(R.id.startButton);
        stopButton = (Button) findViewById(R.id.stopButton);

        //Button to Start the service when pushed
        startButton.setOnClickListener(new View.OnClickListener() {

            public void onClick(View view) {
                if(!isMyServiceRunning(TimerService.class)) {
                    timerService.setAction(Constants.ACTION.STARTFOREGROUND_ACTION);
                    timerService.putExtra(Constants.TIMER.DURATION,duration);
                    startService(timerService);
                    registerReceiver(broadcastReceiver, new IntentFilter(Constants.ACTION.BROADCAST_ACTION));
                }
            }
        });

        //Button to stop the service when pushed
        stopButton.setOnClickListener(new View.OnClickListener() {

            public void onClick(View view) {
                if(isMyServiceRunning(TimerService.class)) {
                    timerView.setText("0:00");
                    timerService.setAction(Constants.ACTION.STOPFOREGROUND_ACTION);
                    startService(timerService);
                    unregisterReceiver(broadcastReceiver);
                }
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        if(!isMyServiceRunning(TimerService.class)) {
            //Resets timer if no service is running
            timerView.setText("0:00");
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(isMyServiceRunning(TimerService.class)) {
            unregisterReceiver(broadcastReceiver);
            Log.d(MainActivity.class.getSimpleName(), "unregistered broadcast");
        }
    }

    /******************** Broadcast Receiver **************************************/

    //Receives the broadcast sent out by the service and updates the UI accordingly.
    private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if(!updateUI(intent)){
                if(!updateUI(timerService)){
                    timerService.setAction(Constants.ACTION.STOPFOREGROUND_ACTION);
                    startService(timerService);
                    showTimerCompleteNotification();
                }
            }
        }
    };

    //Receives the timer from the service and updates the UI
    public boolean updateUI(Intent intent){
        if(!intent.hasExtra(Constants.TIMER.CURRENT_TIME)) return false;

        this.currentTime = intent.getLongExtra(Constants.TIMER.CURRENT_TIME, 0L);

        if(this.currentTime == duration){
            timerView.setText("0:00");
            Toast.makeText(this,"Timer done",Toast.LENGTH_SHORT).show();
            return false;
        }

        int secs = (int) (currentTime / 1000);
        int minutes = secs / 60;

        timerView.setText(Integer.toString(minutes) + ":" + String.format("%02d", secs%60));
        return true;
    }
    /******************************************************************************************/


    /************* Helper Methods ****************************/
    private void showTimerCompleteNotification() {
        Intent resultIntent = new Intent(this, MainActivity.class);
        PendingIntent resultPendingIntent =
                PendingIntent.getActivity(
                        this,
                        0,
                        resultIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT
                );
        NotificationCompat.Builder mBuilder =
                new NotificationCompat.Builder(this)
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setContentTitle("Timer Done!")
                        .setContentText("Congrats")
                        .setContentIntent(resultPendingIntent)
                        .setColor(Color.BLACK)
                        .setLights(Color.BLUE, 500, 500)
                        .setDefaults(NotificationCompat.DEFAULT_VIBRATE)
                        .setDefaults(NotificationCompat.DEFAULT_SOUND)
                        .setStyle(new NotificationCompat.InboxStyle());

        // Gets an instance of the NotificationManager service
        final NotificationManager mNotifyMgr =
                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        // Builds the notification and issues it.
        mNotifyMgr.notify(Constants.NOTIFICATION_ID.FOREGROUND_SERVICE, mBuilder.build());

        //Cancel the notification after a little while
        Handler h = new Handler();
        long delayInMilliseconds = 5000;

        h.postDelayed(new Runnable() {
            public void run() {
                mNotifyMgr.cancel(Constants.NOTIFICATION_ID.FOREGROUND_SERVICE);
            }
        }, delayInMilliseconds);
    }

    private boolean isMyServiceRunning(Class<?> serviceClass) {
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (serviceClass.getName().equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

}

Класс констант:

package com.example.admin.servicetimer.service;

public class Constants {


    public interface ACTION {
        public static String MAIN_ACTION = "com.fahadhd.foregroundservice.action.main";
        public static final String STARTFOREGROUND_ACTION = "com.fahadhd.foregroundservice.action.startforeground";
        public static final String STOPFOREGROUND_ACTION = "com.fahadhd.foregroundservice.action.stopforeground";
        public static final String BROADCAST_ACTION = "com.fahadhd.foregroundservice.action.broadcast";
    }
    public interface TIMER {
        public static final String CURRENT_TIME = "com.fahadhd.foregroundservice.timer.current_time";
        public static final String DURATION = "com.fahadhd.foregroundservice.timer.duration";
    }

    public interface NOTIFICATION_ID {
        public static int FOREGROUND_SERVICE = 1;
    }
}
person Fahad Hassan    schedule 15.08.2016