I am creating a countdown app which shows the time remaining to a specified date and I decided to implement a foreground service to show the time remaining in the notification panel. However, I came across an issue when "bug testing" the app.
Currently, I have a start service button and stop service button which seems to be working fine. However, it only works when I remain in the same launch instance of the app.
Example of remaining in the same launch instance:
1) Start service (countdown in notification starts) > Stop Service (countdown in notification disappears)
2) Start service (countdown in notification starts) > Press Home Button > Launch App Again > Stop Service (countdown in notification disappears)
When i try to start service and stop service in different launch instance of the app, a problem occurs.
Example of different launch instance:
Start service (countdown in notification starts) > Press Home Button > Kill App In App Drawer > Launch App Again > Stop service (countdown in notification is supposed to disappear but doesn't)
The countdown still remains when i click the stop service button when i kill and relaunch the app (the only way to kill it is to clear storage/uninstall app). I figured it may be 2 services conflicting to change 1 variable,etc... but even after hours of trying to find the error and researching on forums, I still cant seem to find the reason why it happens.
Any help or leads would be much appreciated. Really sorry for the terrible formatting.
MainActivity.java
public class MainActivity extends AppCompatActivity implements DatePickerDialog.OnDateSetListener {
private Handler mHandler = new Handler();
private TextView dateText;
private CountDownTimer countDownTimer;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// if first start
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
boolean firstStart = prefs.getBoolean("firstStart", true);
if(firstStart)
{
showStartDialog();
}
// set dateText to date_text
dateText = findViewById(R.id.date_text);
// show date picker when click on show_dialog button
findViewById(R.id.show_dialog).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePickerDialog();
}
});
startTimer();
}
private void showStartDialog()
{
new AlertDialog.Builder(this)
.setTitle("One Time Dialog")
.setMessage("This should only be shown once")
.setPositiveButton("ok", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
showDatePickerDialog();
}
})
.create().show();
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("firstStart", false);
editor.apply();
}
private void showDatePickerDialog()
{
DatePickerDialog datePickerDialog = new DatePickerDialog(
this,
this,
Calendar.getInstance().get(Calendar.YEAR),
Calendar.getInstance().get(Calendar.MONTH),
Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
);
datePickerDialog.show();
}
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth)
{
Date endDate = new Date((year-1900),month,dayOfMonth);
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong("endDate", endDate.getTime());
editor.apply();
startTimer();
}
private void startTimer()
{
long difference = getRemainDays();
if(countDownTimer !=null)
{
countDownTimer.cancel();
countDownTimer = null;
}
countDownTimer = new CountDownTimer(difference,1000) // 1 second
{
@Override
public void onTick(long millisUntilFinished)
{
int days = (int)(millisUntilFinished/(1000*60*60*24));
int hours = (int)((millisUntilFinished/(1000*60*60))%24);
int mins = (int)((millisUntilFinished/(1000*60))%60);
int sec = (int)((millisUntilFinished/(1000))%60);
dateText.setText(String.format("%02d Days %d Hours %d Mins %d Sec",days,hours,mins,sec));
}
@Override
public void onFinish()
{
// Done
dateText.setText("Done");
}
}.start();
}
private long getRemainDays()
{
Date currentDate = new Date();
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
long endDate = prefs.getLong("endDate", currentDate.getTime());
return endDate - currentDate.getTime();
}
public void startService(View v){
String input = dateText.getText().toString();
Intent serviceIntent = new Intent(this, ExampleService.class);
serviceIntent.putExtra("inputExtra", input);
ContextCompat.startForegroundService(this,serviceIntent);
mNotificationRunnable.run();
}
public void stopService(View v){
Intent serviceIntent = new Intent(this,ExampleService.class);
stopService(serviceIntent);
mHandler.removeCallbacks(mNotificationRunnable);
}
private void updateNotification() {
String input = dateText.getText().toString();
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, 0);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Example Service")
.setContentText(input)
.setSmallIcon(R.drawable.ic_android)
.setContentIntent(pendingIntent)
.build();
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(1, notification);
}
private Runnable mNotificationRunnable = new Runnable()
{
@Override
public void run() {
updateNotification();
mHandler.postDelayed(this,1000);
}
};
}
ExampleService.java
public class ExampleService extends Service {
@Override
public void onCreate()
{
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String input = intent.getStringExtra("inputExtra");
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, 0);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Example Service")
.setContentText(input)
.setSmallIcon(R.drawable.ic_android)
.setContentIntent(pendingIntent)
.build();
startForeground(1,notification);
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
New MainActivity.java
private TextView dateText;
private CountDownTimer countDownTimer;
private NotificationSingleton notificationSingleton;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// if first start
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
boolean firstStart = prefs.getBoolean("firstStart", true);
if(firstStart)
{
showStartDialog();
}
// set dateText to date_text
dateText = findViewById(R.id.date_text);
// show date picker when click on show_dialog button
findViewById(R.id.show_dialog).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePickerDialog();
}
});
notificationSingleton = NotificationSingleton.getInstance();
startTimer();
}
private void showStartDialog()
{
new AlertDialog.Builder(this)
.setTitle("One Time Dialog")
.setMessage("This should only be shown once")
.setPositiveButton("ok", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
showDatePickerDialog();
}
})
.create().show();
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("firstStart", false);
editor.apply();
}
private void showDatePickerDialog()
{
DatePickerDialog datePickerDialog = new DatePickerDialog(
this,
this,
Calendar.getInstance().get(Calendar.YEAR),
Calendar.getInstance().get(Calendar.MONTH),
Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
);
datePickerDialog.show();
}
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth)
{
Date endDate = new Date((year-1900),month,dayOfMonth);
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong("endDate", endDate.getTime());
editor.apply();
startTimer();
}
private void startTimer()
{
long difference = getRemainDays();
if(countDownTimer !=null)
{
countDownTimer.cancel();
countDownTimer = null;
}
countDownTimer = new CountDownTimer(difference,1000) // 1 second
{
@Override
public void onTick(long millisUntilFinished)
{
int days = (int)(millisUntilFinished/(1000*60*60*24));
int hours = (int)((millisUntilFinished/(1000*60*60))%24);
int mins = (int)((millisUntilFinished/(1000*60))%60);
int sec = (int)((millisUntilFinished/(1000))%60);
dateText.setText(String.format("%02d Days %d Hours %d Mins %d Sec",days,hours,mins,sec));
}
@Override
public void onFinish()
{
// Done
dateText.setText("Done");
}
}.start();
}
private long getRemainDays()
{
Date currentDate = new Date();
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
long endDate = prefs.getLong("endDate", currentDate.getTime());
return endDate - currentDate.getTime();
}
public void startService(View v){
String input = dateText.getText().toString();
Intent serviceIntent = new Intent(this, ExampleService.class);
serviceIntent.putExtra("inputExtra", input);
ContextCompat.startForegroundService(this,serviceIntent);
notificationSingleton.mNotificationRunnable.run();
}
public void stopService(View v){
Intent serviceIntent = new Intent(this,ExampleService.class);
stopService(serviceIntent);
notificationSingleton.stopService();
}
public TextView getDateText()
{
return dateText;
}
NotificationSingleton.java
public Handler mHandler = new Handler();
private static NotificationSingleton instance;
private NotificationSingleton()
{
//private to prevent any else from instantiating
}
public static synchronized NotificationSingleton getInstance()
{
if (instance == null){
instance = new NotificationSingleton();
}
return instance;
}
public void stopService()
{
mHandler.removeCallbacks(mNotificationRunnable);
}
public Runnable mNotificationRunnable = new Runnable()
{
@Override
public void run() {
updateNotification();
mHandler.postDelayed(this,1000);
}
};
private void updateNotification() {
MainActivity mainActivity = new MainActivity();
String input = mainActivity.getDateText().getText().toString();
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, 0);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Example Service")
.setContentText(input)
.setSmallIcon(R.drawable.ic_android)
.setContentIntent(pendingIntent)
.build();
NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mNotificationManager.notify(1, notification);
}
It's because the first mNotificationRunnable
is never stopped, when you kill the activity and re-enter, you are removing a new one in stopService()
. You can also cause this bug just by rotating the screen.
I'd suggest moving this runnable into some singleton class, where you can manipulate it even with a new activity instance.