7

I wrote a foreground service to make sure my app can continue running when put into the background. The app needs to run in the background because after its timer elapses, it sounds a tone and vibrates to alert the user. However, when the Power or Home button is pressed, the app's timer stops running after about 15 minutes unless the phone is plugged into power. The phone was fully charged when I tested this.

Incidentally, I also set the app to NOT be optimized for battery life after reading on various sites that that would ensure that the app would continue running. From everything I read, I'm doing everything right, yet I still can't get this to work. I'm running Android 11 on a Pixel 2. I know that Google limited foreground processing for later versions of Android, but setting the app to not optimize for battery life is supposed to get around the problem, isn't it? To be safe, when the app starts, it asks the user to approve background operation:

     PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE);
     if (!pm.isIgnoringBatteryOptimizations(APPLICATION_ID)) {
         // Ask user to allow app to not optimize battery life. This will keep
         // the app running when the user puts it in the background by pressing
         // the Power or Home button.
         Intent intent = new Intent();
         intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
         intent.setData(Uri.parse("package:" + APPLICATION_ID));
         startActivity(intent);
     }

so the user sees the following when the app is run and while optimized for battery:

enter image description here

I start the foreground service as follows:

    private void startForegroundMonitoring() {
    broadcastIntent = new Intent(context, BroadcastService.class);
    broadcastIntent.putExtra(ALLOWEDTIME, allowed_time);
    broadcastIntent.putExtra(BEEP, beep.isChecked());
    broadcastIntent.putExtra(VIBRATE, vibrate.isChecked());
    broadcastIntent.putExtra(NOTIFY, notify_monitor.isChecked());
    broadcastIntent.putExtra(CURFEW, curfew_config.isChecked());
    broadcastIntent.putExtra(CURFEWSTARTTIME, curfew_start_time);
    broadcastIntent.putExtra(CURFEWENDTIME, curfew_end_time);
    startService(broadcastIntent);
}

UPDATE: Here is some code that demonstrates the problem:

Main activity:

package com.testapp.showbug;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;

import static com.testapp.showbug.BuildConfig.APPLICATION_ID;

public class MainActivity extends AppCompatActivity {

    private Context context;

    private Intent broadcastIntent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        context = getApplicationContext();

        PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE);
        if (!pm.isIgnoringBatteryOptimizations(APPLICATION_ID)) {
            // Ask user to allow app to not optimize battery life. This will keep
            // the app running when the user puts it in the background by pressing
            // the Power or Home button.
            Intent intent = new Intent();
        
            intent.setAction(
            Settings.
                ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
            intent.setData(Uri.parse("package:" + 
                APPLICATION_ID));
            startActivity(intent);
        }

    broadcastIntent = new Intent(context, 
      BroadcastService.class);
    startService(broadcastIntent);
    }
    public void onDestroy() {
        super.onDestroy();

        stopService(broadcastIntent);
    }
}

BroadcastService:

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.CountDownTimer;
import android.os.IBinder;
import android.widget.Toast;

import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import static android.content.pm.ServiceInfo.
  FOREGROUND_SERVICE_TYPE_LOCATION;

public class BroadcastService extends Service {
    private static final int ONE_MINUTE = 60000;

    private int allowed_time = 30, tickCounter;

    private CountDownTimer countDown;

    private NotificationManagerCompat notificationManager;

    private NotificationCompat.Builder notification;

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

        // Clear all notifications sent earlier.
        notificationManager = 
          NotificationManagerCompat.from(this);
        notificationManager.cancelAll();

        createNotificationChannel();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, 
        int startId) {
        if (intent == null) return START_STICKY;

        Intent notificationIntent = new Intent(this, 
          BroadcastService.class);
        PendingIntent pendingIntent =
            PendingIntent.getActivity(this, 0, 
            notificationIntent, 0);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notification = new 
              NotificationCompat.Builder(this, 
              getString(
                R.string.default_notification_channel_id))
              .setContentTitle( 
                getText(R.string.notification_title))
              .setContentText(
                  getText(R.string.notification_message))
              .setStyle(new NotificationCompat.BigTextStyle()                           
              .bigText(
                  getText(R.string.notification_message)))
              .setContentIntent(PendingIntent.getActivity(
                  this, 0, new Intent(), 0))
              .setSmallIcon(R.mipmap.ic_launcher_round)
              .setLocalOnly(true)
              .setContentIntent(pendingIntent);
        } else {
            notification = new 
                NotificationCompat.Builder(this, 
                  getString(
                    R.string.default_notification_channel_id))
                
            .setContentTitle(
                getText(R.string.notification_title))
            .setContentText(
                getText(R.string.notification_message))
            .setStyle(new NotificationCompat.BigTextStyle()                          
            .bigText(
                getText(R.string.notification_message)))
            .setContentIntent(PendingIntent.getActivity(
                this, 0, new Intent(), 0))
            .setSmallIcon(R.mipmap.ic_launcher_round)
            .setContentIntent(pendingIntent);
        }

    startForeground(FOREGROUND_SERVICE_TYPE_LOCATION, notification.build());

    tickCounter = -1;
    // Start countdown timer for allowed time.
    countDown = new CountDownTimer(allowed_time * ONE_MINUTE, ONE_MINUTE) {
        @Override
        public void onTick(long millisUntilFinished) {
            tickCounter++;
            Toast.makeText(getApplicationContext(), "tickCounter = " + tickCounter, Toast.LENGTH_LONG).show();
        }

        @Override
        public void onFinish() {
            Toast.makeText(getApplicationContext(), "tickCounter = " + allowed_time, Toast.LENGTH_LONG).show();
        }
    }.start();
    return START_STICKY;
}

private void createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        CharSequence name = getString(R.string.channel_name);
       String description = getString(R.string.channel_description);
       int importance = NotificationManager.IMPORTANCE_DEFAULT;
       NotificationChannel channel = new NotificationChannel(getString(R.string.default_notification_channel_id), name, importance);
       channel.setDescription(description);
        
       notificationManager.createNotificationChannel(channel);
       }
    }

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

The above code starts a foreground service which in turn starts a CountDownTimer that increments the number of ticks by one every minute and prints the result. After 30 minutes, it should show a count of 30 ticks. Instead, it stops early, usually after 15-16 ticks.

Here's how to run the code:

  1. Start the activity, disconnect the device from power (this is important and, obviously, requires a real device) and tap the Power button on the device.
  2. Wait 28 minutes.
  3. Put the app back into the foreground and wait for the next tick.
  4. Note that the next tick displays something less than 28.

Thanks for any help on this. It looks to me like a bug in the Android SDK, unlikely as that may seem. I don't see any other cause. By the way, I tested this code on a Pixel 2 and a Samsung Tab A, both running Android 11 (the only devices I own), so I don't know if the bug occurs on earlier versions of Android or different devices.

FractalBob
  • 3,225
  • 4
  • 29
  • 40
  • 1
    you didnt post your service but I assume you are calling `startForeground()` at some point? – tyczj Apr 12 '21 at 16:44
  • @tyczj Yes. I start it at the end of onCreate() and pass it all the variables it needs to run the timer. – FractalBob Apr 13 '21 at 05:54
  • @FractalBob Why are you stopping service on your Activity's `onDestroy`? – Aswin P Ashok Apr 15 '21 at 04:39
  • @AswinPAshok Cleanup. – FractalBob Apr 15 '21 at 04:41
  • @FractalBob isn't that why your app stops running? You press home button, OS kills your activity after a while, onDestroy gets called, then you stop your service? – Aswin P Ashok Apr 15 '21 at 04:45
  • @AswinPAshok No. onDestroy() does NOT get called by tapping the Home or Power button. In fact, the problem does not occur by tapping Home. – FractalBob Apr 15 '21 at 04:54
  • @FractalBob Did you check that `onDestroy` of your `Activity` doesn't get called right before the `Service` stops? – Anatolii Apr 15 '21 at 13:20
  • @Anatolii I didn't have to because onDestroy() only gets called when the Activity finishes, which it does not in this case. Besides, the Service never stopped; it just pauses several minutes after the Power button is tapped and resumes when the Home screen is displayed. If Home is pressed instead, the service continues running. – FractalBob Apr 15 '21 at 14:58
  • Check if the device manufacturer is known for aggressive app killing: https://dontkillmyapp.com – Robert Apr 15 '21 at 15:15
  • @Robert Thanks for the link, Robert. I checked and the manufacturer (Google) plays nice. As I mentioned earlier, I tested on the Pixel 2. – FractalBob Apr 15 '21 at 15:41

1 Answers1

6

I finally solved the problem, using wakelocks. Wakelocks ensure that the CPU continues to run after the Power button has been pressed. All I had to do was add the following code in BroadcastService.java:

In onCreate():

    PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE);
    wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PeriSecure:MyWakeLock");

In onStartCommand():

    wakeLock.acquire(allowed_time * ONE_MINUTE);

In onDestroy():

wakeLock.release();

That's all! The background Service runs as is should now.

FractalBob
  • 3,225
  • 4
  • 29
  • 40