Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0dd0cac6 authored by James Lemieux's avatar James Lemieux
Browse files

Promote TimerService to the foreground while expired timers exist

Bug: 26471891

This makes the clock app unlikely to be killed in memory pressure
situations while expired timers are ringing.

Change-Id: I89f141a835e3de67a58671d6c5b381de52be5c18
parent dd3f9e7c
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.deskclock.data;

import android.app.Service;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
@@ -318,11 +319,12 @@ public final class DataModel {
    }

    /**
     * @param service used to start foreground notifications for expired timers
     * @param timer the timer to be expired
     */
    public void expireTimer(Timer timer) {
    public void expireTimer(Service service, Timer timer) {
        enforceMainLooper();
        mTimerModel.updateTimer(timer.expire());
        mTimerModel.expireTimer(service, timer);
    }

    /**
+36 −10
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.deskclock.data;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -40,6 +41,7 @@ import android.util.ArraySet;

import com.android.deskclock.AlarmAlertWakeLock;
import com.android.deskclock.HandleDeskClockApiCalls;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
import com.android.deskclock.events.Events;
@@ -109,6 +111,13 @@ final class TimerModel {
    /** A mutable copy of the expired timers. */
    private List<Timer> mExpiredTimers;

    /**
     * The service that keeps this application in the foreground while a heads-up timer
     * notification is displayed. Marking the service as foreground prevents the operating system
     * from killing this application while expired timers are actively firing.
     */
    private Service mService;

    TimerModel(Context context, SettingsModel settingsModel, NotificationModel notificationModel) {
        mContext = context;
        mSettingsModel = settingsModel;
@@ -206,6 +215,24 @@ final class TimerModel {
        return timer;
    }

    /**
     * @param service used to start foreground notifications related to expired timers
     * @param timer the timer to be expired
     */
    void expireTimer(Service service, Timer timer) {
        if (mService == null) {
            // If this is the first expired timer, retain the service that will be used to start
            // the heads-up notification in the foreground.
            mService = service;
        } else if (mService != service) {
            // If this is not the first expired timer, the service should match the one given when
            // the first timer expired.
            LogUtils.wtf("Expected TimerServices to be identical");
        }

        updateTimer(timer.expire());
    }

    /**
     * @param timer an updated timer to store
     */
@@ -712,17 +739,14 @@ final class TimerModel {
     * displayed whether the application is open or not.
     */
    private void updateHeadsUpNotification() {
        // Filter the timers to just include expired ones.
        final List<Timer> expired = new ArrayList<>();
        for (Timer timer : getMutableTimers()) {
            if (timer.isExpired()) {
                expired.add(timer);
            }
        }
        final List<Timer> expired = getExpiredTimers();

        // If no expired timers exist, cancel the notification.
        // If no expired timers exist, stop the service (which cancels the foreground notification).
        if (expired.isEmpty()) {
            mNotificationManager.cancel(mNotificationModel.getExpiredTimerNotificationId());
            if (mService != null) {
                mService.stopSelf();
                mService = null;
            }
            return;
        }

@@ -793,7 +817,9 @@ final class TimerModel {
        // Update the notification.
        final Notification notification = builder.build();
        final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
        mNotificationManager.notify(notificationId, notification);
        if (mService != null) {
            mService.startForeground(notificationId, notification);
        }
    }

    /**
+58 −54
Original line number Diff line number Diff line
@@ -27,14 +27,16 @@ import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Timer;
import com.android.deskclock.events.Events;

import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;

/**
 * This service exists solely to allow {@link android.app.AlarmManager} and timer notifications
 * <p>This service exists solely to allow {@link android.app.AlarmManager} and timer notifications
 * to alter the state of timers without disturbing the notification shade. If an activity were used
 * instead (even one that is not displayed) the notification manager implicitly closes the
 * notification shade which clashes with the use case of starting/pausing/resetting timers without
 * disturbing the notification shade.
 * disturbing the notification shade.</p>
 *
 * <p>The service has a second benefit. It is used to start heads-up notifications for expired
 * timers in the foreground. This keeps the entire application in the foreground and thus prevents
 * the operating system from killing it while expired timers are firing.</p>
 */
public final class TimerService extends Service {

@@ -51,33 +53,28 @@ public final class TimerService extends Service {
        final int timerId = timer == null ? -1 : timer.getId();
        return new Intent(context, TimerService.class)
                .setAction(ACTION_TIMER_EXPIRED)
                .addFlags(FLAG_RECEIVER_FOREGROUND)
                .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timerId);
    }

    public static Intent createResetExpiredTimersIntent(Context context) {
        return new Intent(context, TimerService.class)
                .setAction(ACTION_RESET_EXPIRED_TIMERS)
                .addFlags(FLAG_RECEIVER_FOREGROUND);
                .setAction(ACTION_RESET_EXPIRED_TIMERS);
    }

    public static Intent createResetUnexpiredTimersIntent(Context context) {
        return new Intent(context, TimerService.class)
                .setAction(ACTION_RESET_UNEXPIRED_TIMERS)
                .addFlags(FLAG_RECEIVER_FOREGROUND);
                .setAction(ACTION_RESET_UNEXPIRED_TIMERS);
    }

    public static Intent createAddMinuteTimerIntent(Context context, int timerId) {
        return new Intent(context, TimerService.class)
                .setAction(HandleDeskClockApiCalls.ACTION_ADD_MINUTE_TIMER)
                .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timerId)
                .addFlags(FLAG_RECEIVER_FOREGROUND);
                .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timerId);
    }

    public static Intent createUpdateNotificationIntent(Context context) {
        return new Intent(context, TimerService.class)
                .setAction(ACTION_UPDATE_NOTIFICATION)
                .addFlags(FLAG_RECEIVER_FOREGROUND);
                .setAction(ACTION_UPDATE_NOTIFICATION);
    }

    @Override
@@ -87,6 +84,7 @@ public final class TimerService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        try {
            switch (intent.getAction()) {
                case ACTION_UPDATE_NOTIFICATION: {
                    DataModel.getDataModel().updateTimerNotification();
@@ -129,10 +127,16 @@ public final class TimerService extends Service {
                    DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_notification);
                    break;
                case ACTION_TIMER_EXPIRED:
                DataModel.getDataModel().expireTimer(timer);
                    DataModel.getDataModel().expireTimer(this, timer);
                    Events.sendTimerEvent(R.string.action_fire, R.string.label_intent);
                    break;
            }
        } finally {
            // This service is foreground when expired timers exist and stopped when none exist.
            if (DataModel.getDataModel().getExpiredTimers().isEmpty()) {
                stopSelf();
            }
        }

        return START_NOT_STICKY;
    }