Intent Service not working in doze mode

2019-06-05 06:28发布

问题:

One of my peer developer has written an intent service that makes an API call and then sleeps for 2 mins. After waking up, it sends again.

Below is the code:

public class GpsTrackingService extends IntentService {

....

@Override
    protected void onHandleIntent(Intent intent) {
      do{
        try{
          //make API call here

           //then go to sleep for 2 mins
          TimeUnit.SECONDS.sleep(120);
       
        } catch(InterruptedException ex){
            ex.printStackTrace();
        }
      } while (preferences.shouldSendGps()); //till the user can send gps.

    }

....

}


Manifest

<service android:name=".commons.GpsTrackingService" />

This is working fine when the phone is active. However, whenever the phone goes into doze mode it fails to wake.

Will using alarm manager with WAKE permission solve this?

I have just got the code base and need to fix this within today. It'll be great if someone can help.

回答1:

As the documentation says:

In Doze mode, the system attempts to conserve battery by restricting apps' access to network and CPU-intensive services. It also prevents apps from accessing the network and defers their jobs, syncs, and standard alarms.

Periodically, the system exits Doze for a brief time to let apps complete their deferred activities. During this maintenance window, the system runs all pending syncs, jobs, and alarms, and lets apps access the network.

In few words, while in Doze mode the system suspends network accesses, ignores Wake Locks, stops acquiring data from sensors, defers AlarmManager jobs to the next Doze maintenance window (which are progressively less frequently called), also WiFi scans, JobScheduler jobs and Sync adapters do not run.

Neither setAndAllowWhileIdle() nor setExactAndAllowWhileIdle() can fire alarms more than once per 9 (?) minutes, per app.

And it seems that the Foreground Services are also involved into this "Doze Drama", at least in MarshMellow (M).

To survive in this situation, tons of applications need to be at least rewiewed. Can you imagine a simple mp3 player which stops playing music when the device enters in Doze Mode?

Doze mode starts automatically, when the device is unplugged from the power supply and left on the table for about 1 hour or so, or even earlier when the user clicks the power button to power down the screen, but I think this could depend by the device manufacturer too.

I tried a lot of countermeasures, some of them really hilarious.

At the end of my tests I reached a possible solution:

One possible (and maybe the only) way to have your app running even when the host device is in Doze mode, is basically to have a ForegroundService (even a fake one, doing no jobs at all) running in another process with an acquired partial WakeLock.

What you need to do is basically the following (you could create a simple project to test it):

  • 1 - In your new project, create a new class which extends Application (myApp), or use the main activity of the new project.
  • 2 - In myApp onCreate() start a Service (myAntiDozeService)
  • 3 - In myAntiDozeService onStartCommand(), create the Notification needed to start the service as a foreground service, start the service with startForeground(id, notification) and acquire the partial WakeLock.

REMEMBER! This will work, but it is just a starting point, because you have to be careful with the "Side Effects" this approach will generate:

  • 1 - Battery drain: The CPU will work for your app forever if you don't use some strategy and leave the WakeLock always active.

  • 2 - One notification will be always shown, even in the lockscreen, and this notification cannot be removed by simply swiping it out, it will be always there until you'll stop the foreground service.

OK, let's do it.

myApp.java

    public class myApp extends Application {
    private static final String STARTFOREGROUND_ACTION = "STARTFOREGROUND_ACTION";
    private static final String STOPFOREGROUND_ACTION = "STOPFOREGROUND_ACTION";

        @Override
        public void onCreate() {

            super.onCreate();            

            // start foreground service
            startForeService();
    }

    private void stopForeService() {
        Intent service = new Intent(this, myAntiDozeService.class);
        service.setAction(STOPFOREGROUND_ACTION);
        stopService(service);
    }

    private void startForeService(){
        Intent service = new Intent(this, myAntiDozeService.class);
        service.setAction(STARTFOREGROUND_ACTION);
        startService(service);
    }

    @Override
    public void onTerminate() {
        stopForeService();
        super.onTerminate();
    }
}

myAntiDozeService.java

public class myAntiDozeService extends Service {

    private static final String TAG = myAntiDozeService.class.getName();
    private static boolean is_service_running = false;
    private Context mContext;
    private PowerManager.WakeLock mWakeLock;
    private static final int NOTIFICATION_ID = 12345678;
    private static final String STARTFOREGROUND_ACTION = "STARTFOREGROUND_ACTION";
    private static final String STOPFOREGROUND_ACTION = "STOPFOREGROUND_ACTION";

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplicationContext();

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (!is_service_running && STARTFOREGROUND_ACTION.equals(intent.getAction())) {
            Log.i(TAG, "Received Start Foreground Intent ");
            showNotification();
            is_service_running = true;
            acquireWakeLock();

        } else if (is_service_running && STOPFOREGROUND_ACTION.equals(intent.getAction())) {
            Log.i(TAG, "Received Stop Foreground Intent");
            is_service_running = false; 
            stopForeground(true);
            stopSelf();
        }

        return START_STICKY;
    }

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

    private void showNotification(){

        Intent notificationIntent = new Intent(mContext, ActivityMain.class);
        notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, notificationIntent, 0);

        Notification notification = new NotificationCompat.Builder(mContext)
                .setContentTitle("myApp")
                .setTicker("myApp")
                .setContentText("Application is running")
                .setSmallIcon(R.drawable.ic_launcher)
                .setContentIntent(pendingIntent)
                .build();

        // starts this service as foreground
        startForeground(NOTIFICATION_ID, notification);
    }

    public void acquireWakeLock() {
        final PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        releaseWakeLock();
        //Acquire new wake lock
        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG+"PARTIAL_WAKE_LOCK");
        mWakeLock.acquire();
    }

    public void releaseWakeLock() {
        if (mWakeLock != null && mWakeLock.isHeld()) {
            mWakeLock.release();
            mWakeLock = null;
        }
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

AndroidManifest.xml changes.


In the AndroidManifest.xml add this permission:

<uses-permission android:name="android.permission.WAKE_LOCK" />

Don't forget to add the name of your app in the <application> tag:

<application
        ....
        android:name=".myApp"
        ....

And finally add your foreground service running into another process:

   <service
        android:name=".myAntiDozeService"
        android:process=":MyAntiDozeProcessName">
    </service>

A couple of notes.

  • In the previous example, the notification created, when clicked, opens the ActivityMain activity of your test project.

    Intent notificationIntent = new Intent(mContext, ActivityMain.class);

    but you can use another kind of intent too.

  • To test it, you have to add some job to be performed into your ActivityMain.java, for example some repeating alarm (which was normally stopped when the device falls in Doze Mode), or a ripetitive network access, or a timed tone played, or.... whatever you want.
  • Remember that the job performed by the main activity has to run forever because to test this AntiDoze you need to wait at least 1 hour to be sure the device enters in Doze Mode.
  • To enter in Doze mode, the device has to be quiet and unplugged, so you can't test it while you are debugging. Debug your app first, check that everything is running then stop it, unplug, restart the app again and leave the device alone and quiet on your desk.

  • The adb commands suggested by the documentation to simulate Doze and StandBy modes could and could not give you the right results (it depends, I suppose, by the device manufacturer, drivers, bla bla). Please make your tests in the REAL behaviour.

In my first test, I used an AlarmManager and a tone generator to play a tone every 10 minutes just to understand that my app was still active. And it is still running from about 18 hours, breaking my ears with a loud tone exactly every 10 minutes. :-)

Happy coding!



回答2:

One of my peer developer has written an intent service that makes an API call and then sleeps for 2 mins. After waking up, it sends again.

Only have a service running while it is actively delivering value to the user. Sitting around for two minutes, watching the clock tick, is not actively delivering value to the user.

Will using alarm manager with WAKE permission solve this?

That depends on what you mean by "solve this". You can use AlarmManager to request to get control every two minutes so that you can do work. While the device is in Doze mode, you will not actually get control every two minutes, but once per maintenance window.