Reputation: 316
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?
I've managed to succeed in creating a foreground service that runs my timer, but I have two problems:
Here's my Service
s 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);
}
}
When I run the app on google pixel emulator, I don't face any of the issues listed
Upvotes: 2
Views: 3490
Reputation: 1
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).
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.
->
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
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
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
Reputation: 3765
There are 2 issues. I will try to solve both of them.
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.
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
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