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:
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:
- 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.
- Wait 28 minutes.
- Put the app back into the foreground and wait for the next tick.
- 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.
from My Android 11 app stops running when the user puts it into the background unless the phone is connected to power
No comments:
Post a Comment