Nitzan Daloomy
Nitzan Daloomy

Reputation: 316

Android: Notification Shows in Delay And Stops Updating When App Closed for 30 Seconds (on OnePlus 8T)

Google has its clock app, which includes its stopwatch. I'm currently trying to create in my app a (count-up) timer, or you can call it a stopwatch, that will be able to run in the background, and when it runs in the background I want it to also show a notification, that displays the time it counts and a "Stop" button (all of this happens in google clock app (see here)). For the timer in my app, I'm using a Handler that posts a Runnable, which is posting itself. I'm writing my app in Java.

the code defining the 'timer' (Handler and Runnable):

Handler timerHandler = new Handler();
Runnable timerRunnable = new Runnable() {
    @Override
    public void run() {
        long millis = System.currentTimeMillis() - startTime;
        seconds = (millis / 1000) + PrefUtil.getTimerSecondsPassed();
        timerHandler.postDelayed(this, 500);
    }
};

my onPause function:

@Override
public void onPause() {
    super.onPause();

    if (timerState == TimerState.Running) {
        timerHandler.removeCallbacks(timerRunnable);
        //TODO: start background timer, show notification
    }

    PrefUtil.setTimerSecondsPassed(seconds);
    PrefUtil.setTimerState(timerState);
}

How can I implement the background service and the notification in my app?

Edit

I've managed to succeed in creating a foreground service that runs my timer, but I have two problems:

  1. When I run the app after something like 5 minutes, the notification shows up in a 10-second delay.
  2. the notification stops updating after around 30 seconds from the time it starts/resumes (The timer keeps running in the background, but the notification won't keep updating with the timer).

Here's my Services code:

public class TimerService extends Service {

    Long startTime = 0L, seconds = 0L;
    boolean notificationJustStarted = true;
    Handler timerHandler = new Handler();
    Runnable timerRunnable;
    NotificationCompat.Builder timerNotificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
    public static final String TIMER_BROADCAST_ID = "TimerBroadcast";
    Intent timerBroadcastIntent = new Intent(TIMER_BROADCAST_ID);

    @Override
    public void onCreate() {
        Log.d(TAG, "onCreate: started service");
        startForeground(1, new NotificationCompat.Builder(this, CHANNEL_ID).setSmallIcon(R.drawable.timer).setContentTitle("Goal In Progress").build());
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String goalName = intent.getStringExtra(PublicMethods.getAppContext().getString(R.string.timer_notification_service_current_goal_extra_name));
        startTime = System.currentTimeMillis();
        notificationJustStarted = true;
        timerRunnable = new Runnable() {
            @Override
            public void run() {
                long millis = System.currentTimeMillis() - startTime;
                seconds = (millis / 1000) + PrefUtil.getTimerSecondsPassed();
                updateNotification(goalName, seconds);
                timerHandler.postDelayed(this, 500);
            }
        };
        timerHandler.postDelayed(timerRunnable, 0);

        return START_STICKY;
    }

    public void updateNotification(String goalName, Long seconds) {
        try {
            if (notificationJustStarted) {
                Intent notificationIntent = new Intent(this, MainActivity.class);
                PendingIntent pendingIntent = PendingIntent.getActivity(this,
                        0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
                timerNotificationBuilder.setContentTitle("Goal In Progress")
                        .setOngoing(true)
                        .setSmallIcon(R.drawable.timer)
                        .setContentIntent(pendingIntent)
                        .setOnlyAlertOnce(true)
                        .setOngoing(true)
                        .setPriority(NotificationCompat.PRIORITY_MAX);
                notificationJustStarted = false;
            }

            timerNotificationBuilder.setContentText(goalName + " is in progress\nthis session's length: " + seconds);

            startForeground(1, timerNotificationBuilder.build());
        } catch (Exception e) {
            Log.d(TAG, "updateNotification: Couldn't display a notification, due to:");
            e.printStackTrace();
        }
    }

    @Override
    public void onDestroy() {
        timerHandler.removeCallbacks(timerRunnable);
        PrefUtil.setTimerSecondsPassed(seconds);
        super.onDestroy();
    }

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

And here is how I start it in my fragment:

private void startTimerService() {
        Intent serviceIntent = new Intent(getContext(), TimerService.class);
        serviceIntent.putExtra(getString(R.string.timer_notification_service_current_goal_extra_name), "*Current Goal Name Here*");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Objects.requireNonNull(getContext()).startForegroundService(serviceIntent);
        }
}

UPDATE

When I run the app on google pixel emulator, I don't face any of the issues listed

Upvotes: 2

Views: 3490

Answers (4)

I've managed to succeed in creating a foreground service that runs my timer, but I have two problems:

When I run the app after something like 5 minutes, the notification shows up in a 10-second delay. the notification stops updating after around 30 seconds from the time it starts/resumes (The timer keeps running in the background, but the notification won't keep updating with the timer).

https://developer.android.com/about/versions/12/behavior-changes-all#foreground-service-notification-delay

To provide a streamlined experience for short-running foreground services, devices that run Android 12 or higher can delay the display of foreground service notifications by 10 seconds, with a few exceptions. This change gives short-lived tasks a chance to complete before their notifications appear.

->

https://developer.android.com/develop/background-work/services/foreground-services#notification-immediate

Services that show a notification immediately If a foreground service has at least one of the following characteristics, the system shows the associated notification immediately after the service starts, even on devices that run Android 12 or higher:

  • The service is associated with a notification that includes action buttons.
  • The service has a foregroundServiceType of mediaPlayback, mediaProjection, or phoneCall.
  • The service provides a use case related to phone calls, navigation, or media playback, as defined in the notification's category
    attribute.
  • The service has opted out of the behavior change by passing FOREGROUND_SERVICE_IMMEDIATE into setForegroundServiceBehavior() when setting up the notification.

On Android 13 (API level 33) or higher, if the user denies the notification permission, they still see notices related to foreground services in the Task Manager but don't see them in the notification drawer.

Upvotes: 0

Turosik
Turosik

Reputation: 34

Solution would work much better, even with enabled battery restrictions, if you will replace recursive postDelayed with scheduleAtFixedRate in your TimerService inside onStartCommand function. Something like this:

        TimerTask timerTaskNotify = new TimerTask() {
            @Override
            public void run() {
                // add a second to the counter
                seconds++;

                //update the notification with that second
                updateNotification(goalName, seconds);

                //Print the seconds
                Log.d("timerCount", seconds + "");

                //Save the seconds passed to shared preferences
                PrefUtil.setTimerSecondsPassed(TimerService.this,seconds);
            }
        };
        Timer timerNotify = new Timer();
        timerNotify.scheduleAtFixedRate(timerTaskNotify, 0, 1000);

P.S. I can update your git repository if you will grant permission)

Upvotes: 1

Nitzan Daloomy
Nitzan Daloomy

Reputation: 316

I've found the reason why my notification stops updating after 30 seconds! Apparently,(according to this thread) on some devices running Android versions higher than 9 there are background restrictions.

These restrictions are the ones stopping my notifications from updating after 30 seconds from the moment the app gets closed, or in other words - from the moment they're becoming background activities (even though they are called through startForeground()).

There is no way around this setting. You cannot programmatically disable it. Your only option is to programmatically check if it's enabled using ActivityManager.isBackgroundRestricted() and display a pop-up informing your users on how to disable this setting

Says the user from the accepted answer in the thread.

And so, the issue of the notification not updating as expected is solved. The issue of the delay to show the first notification though remains unsolved, and there's another issue - every time the notification gets updated, the whole notification panel freezes for a second fraction.

Upvotes: 0

Sambhav Khandelwal
Sambhav Khandelwal

Reputation: 3765

There are 2 issues. I will try to solve both of them.

First issue


When I run the app after something like 5 minutes, the notification shows up in a 10-second delay.

For this, you need to update the notification with its code. Now, because it takes time to show up, show it in the activity where you start the service and then, pass the notification id to the service using its constructor. Using that id, update it in the service.

Let's hope that solves the first issue.

Second issue


the notification stops updating after around 30 seconds from the time it starts/resumes (The timer keeps running in the background, but the notification won't keep updating with the timer).

To solve that, you can clear the previous notification after 10 seconds by it's id. Then you can make a new random key for the notification( I'd prefer new Random().nextInt()) and then show it. But then you or anyone would say that there is so much sound when a notification comes. Just disable it this way when creating a channel:

notificationChannel.setSound(null, null);

NOTE: You might want to reinstall your app for it to work

If that seems complicated, see this:

Runnable running -> When 10 seconds done from previous notification display -> Clear the notification -> Make a new notification id -> show notification with that id -> Repeat

EDIT


This is the working code for me:

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

import java.util.concurrent.TimeUnit;

public class TimerService extends Service {

    Long startTime = 0L, seconds = 0L;
    boolean notificationJustStarted = true;
    Handler timerHandler = new Handler();
    Runnable timerRunnable;
    private final String CHANNEL_ID = "Channel_id";
    NotificationManager mNotificationManager;

    NotificationCompat.Builder timerNotificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle(CHANNEL_ID);

    @SuppressLint("InlinedApi")
    @Override
    public void onCreate() {
        super.onCreate();
        Toast.makeText(this, "created", Toast.LENGTH_SHORT).show();
        String TAG = "Timer Service";
        Log.d(TAG, "onCreate: started service");
        startForeground(1, new NotificationCompat.Builder(TimerService.this, createChannel()).setContentTitle("Goal In Progress").setPriority(NotificationManager.IMPORTANCE_MAX).build());
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String goalName = "Sample Goal";
        Toast.makeText(this, "started", Toast.LENGTH_SHORT).show();
        startTime = System.currentTimeMillis();
        notificationJustStarted = true;
        timerRunnable = new Runnable() {
            @Override
            public void run() {
                long millis = System.currentTimeMillis() - startTime;
                seconds = (millis / 1000) + PrefUtil.getTimerSecondsPassed(TimerService.this);
                updateNotification(goalName, seconds);
                Log.d("timerCount", seconds + "");
                timerHandler.postDelayed(this, 1000);
            }
        };
        timerHandler.postDelayed(timerRunnable, 0);

        return Service.START_STICKY;
    }

    @SuppressLint("NewApi")
    public void updateNotification(String goalName, long seconds) {
        if (notificationJustStarted) {
            Intent notificationIntent = new Intent(this, MainActivity.class);
            @SuppressLint("InlinedApi") PendingIntent pendingIntent = PendingIntent.getActivity(this,
                    0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
            timerNotificationBuilder.setContentTitle("Goal In Progress")
                    .setOngoing(true)
                    .setContentIntent(pendingIntent)
                    .setOnlyAlertOnce(true)
                    .setOngoing(true)
                    .setPriority(NotificationCompat.PRIORITY_MAX)
                    .setSmallIcon(R.drawable.ic_launcher_foreground);
            notificationJustStarted = false;
        }

        long minutes = TimeUnit.SECONDS.toMinutes(seconds);
        String time = minutes + ":" + (seconds - TimeUnit.MINUTES.toSeconds(minutes));

        timerNotificationBuilder.setContentText(goalName + " is in progress\nthis session's length: " + time);

        mNotificationManager.notify(1, timerNotificationBuilder.build());

        startForeground(1, timerNotificationBuilder.build());
    }

    @Override
    public void onDestroy() {
        timerHandler.removeCallbacks(timerRunnable);
        PrefUtil.setTimerSecondsPassed(this, seconds);
        super.onDestroy();
    }

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

    @NonNull
    @TargetApi(26)
    private synchronized String createChannel() {
        mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);

        String name = "STOPWATCH";
        int importance = NotificationManager.IMPORTANCE_LOW;

        NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance);

        mChannel.setName("Notifications");

        if (mNotificationManager != null) {
            mNotificationManager.createNotificationChannel(mChannel);
        } else {
            stopSelf();
        }

        return CHANNEL_ID;
    }
}

You can also view my repo on this here. It is a complete stop watch app

Upvotes: 0

Related Questions