2

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

Ryan M
  • 18,333
  • 31
  • 67
  • 74
Nitzan Daloomy
  • 166
  • 5
  • 24
  • Not sure whether this can be best fit in your usecase but you should try this: https://developer.android.com/guide/background#deferrable This is the era of AAC in Android and one should avoid direct use of Services itself. Instead find a way to use WorkManager and implement solution through it accordingly. – Jeel Vankhede Apr 11 '22 at 14:06
  • 1
    @JeelVankhede The timer is not deferrable though, so it doesn't match, or am I wrong? – Nitzan Daloomy Apr 11 '22 at 14:36
  • 1
    Please don't edit "Solved" or similar things into titles. Accepting an answer has the effect of marking a question solved. – Ryan M Apr 14 '22 at 18:18
  • @RyanM but now I understand my title wasn't quite accurate, and the problem wasn't understood before. updating it to |SOLVED: Allow background activities made it more accurate, didn't it? – Nitzan Daloomy Apr 14 '22 at 18:21
  • 2
    You can certainly update the title to clarify the problem, but the solution doesn't belong the title: that's what the answer is for. – Ryan M Apr 14 '22 at 18:22

3 Answers3

1

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)

Turosik
  • 34
  • 3
  • Hi Turosik (: my repository is public, here's a link to it: https://github.com/NiDaAppDev/Brainer If you could actually fork it and make a pull request it would be really great, if not - anyway probably soon I'll try it on my own if you won't precede me (: Thank you! – Nitzan Daloomy Mar 07 '23 at 00:01
  • I've tried it, and it actually does work much better! Thank you (: – Nitzan Daloomy Mar 07 '23 at 20:35
  • It didn't work on OnePlus 7. I am not sure how why a Timer would work while a Handler doesn't. – BPDev Jun 23 '23 at 02:59
0

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

Sambhav Khandelwal
  • 3,585
  • 2
  • 7
  • 38
  • 1
    Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/243869/discussion-on-answer-by-sambhav-k-android-notification-shows-in-delay-and-stop). – Ryan M Apr 13 '22 at 20:08
0

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
Nitzan Daloomy
  • 166
  • 5
  • 24
  • Oops. I missed your answer out here. Finally a reason found. But, even I was suspecting that your device is blocking that. Also, you wanted to make it like google stopwatch right. Here's a tip **Never try to copy google apps as they can modify their android device to make the app work. Our apps are restricted to do that. Also note that google apps do not use their own sdk which they provide to us. They instead make the own sdk which are not available to us publicly. Also, only some android apps code is given by google as they don't use the custom sdk. These apps are usually testing apps** – Sambhav Khandelwal Apr 14 '22 at 04:26
  • Hmmmm. I don't see that issue in my app when run on One plus 9R – Sambhav Khandelwal Apr 14 '22 at 16:46
  • And, that delay will be seen because we are performing a heavy task every second in our app. This is a issue the entire device will face. Not only the notification panel – Sambhav Khandelwal Apr 14 '22 at 16:47
  • So that's the thing, the device does not slow up or get stuck, and in the pixel 2 emulator it runs just fine... (If I understand you correctly you refer to the second issue, the one with the notification panel getting stuck) – Nitzan Daloomy Apr 14 '22 at 17:42
  • Yes. You are right. This works fine as Emulator does not have its own RAM. It uses the laptop's RAM. This makes it work fine. BTW. But, a android device does not have that much ram. This makes it slow. Also the device performance drops with time. Maybe your device is old for it to work slow – Sambhav Khandelwal Apr 15 '22 at 05:20
  • Actually I think you're wrong... My phone has 8 GB of RAM, while my pc has the same amount of RAM. Also, I run my emulator through android studio, and it gives the device it' specific specs, which means not all of the pcs RAM is used on the emulator, and my actual phone has more RAM than the emulator – Nitzan Daloomy Apr 15 '22 at 10:19
  • Might be. My phone has 8 GB ram and it gets stuck a bit. My PC has 16 gb Ram and it works fine – Sambhav Khandelwal Apr 15 '22 at 10:19