Add more control over a service's start state.

One of the problems I have been noticing is background services
sitting around running and using resources.  Some times this is
due to the app developer doing this when they shouldn't, but there
are also a number of issues with the current Service interaction
model that make it very difficult (or impossible) to avoid
getting services stuck in the started state.  This is a
change/enhancement to the Service API to try to address this.

The main change is that Service.onStart() has been deprecated,
replaced with a new Service.onStartCommand() that allows the
service to better control how the system should manage it.  The
key part here is a new result code returned by the function, telling
the system what it should do with the service afterwards:

- START_STICKY is basically the same as the previous behavior,
  where we usually leave the service running.  The only difference
  is that it if it gets restarted because its process is killed,
  onStartCommand() will be called on the new service with a null
  Intent instead of not being called at all.

- START_NOT_STICKY says that, upon returning to the system, if
  its process is killed with no remaining start commands to
  deliver, then the service will be stopped instead of restarted.
  This makes a lot more sense for services that are intended to
  only run while executing commands sent to them.

- START_REDELIVER_INTENT is like START_NOT_STICKY, except if
  the service's process is killed before it calls stopSelf()
  for a given intent, that intent will be re-delivered to it
  until it completes (unless after 4 or more tries it still
  can't complete, at which point we give up).

Change-Id: I978f5ca420d70023d1b5e7f97de639d09381f8ad
diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java
index ee9fa36..a4b0685 100644
--- a/services/java/com/android/server/am/ActivityManagerService.java
+++ b/services/java/com/android/server/am/ActivityManagerService.java
@@ -42,6 +42,7 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.ResultInfo;
+import android.app.Service;
 import android.backup.IBackupManager;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -9385,7 +9386,9 @@
                 sr.app = null;
                 sr.executeNesting = 0;
                 mStoppingServices.remove(sr);
-                if (sr.bindings.size() > 0) {
+                
+                boolean hasClients = sr.bindings.size() > 0;
+                if (hasClients) {
                     Iterator<IntentBindRecord> bindings
                             = sr.bindings.values().iterator();
                     while (bindings.hasNext()) {
@@ -9406,7 +9409,20 @@
                 } else if (!allowRestart) {
                     bringDownServiceLocked(sr, true);
                 } else {
-                    scheduleServiceRestartLocked(sr);
+                    boolean canceled = scheduleServiceRestartLocked(sr, true);
+                    
+                    // Should the service remain running?  Note that in the
+                    // extreme case of so many attempts to deliver a command
+                    // that it failed, that we also will stop it here.
+                    if (sr.startRequested && (sr.stopIfKilled || canceled)) {
+                        if (sr.pendingStarts.size() == 0) {
+                            sr.startRequested = false;
+                            if (!hasClients) {
+                                // Whoops, no reason to restart!
+                                bringDownServiceLocked(sr, true);
+                            }
+                        }
+                    }
                 }
             }
 
@@ -9845,35 +9861,55 @@
 
     private final void sendServiceArgsLocked(ServiceRecord r,
             boolean oomAdjusted) {
-        final int N = r.startArgs.size();
+        final int N = r.pendingStarts.size();
         if (N == 0) {
             return;
         }
 
-        final int BASEID = r.lastStartId - N + 1;
         int i = 0;
         while (i < N) {
             try {
-                Intent args = r.startArgs.get(i);
+                ServiceRecord.StartItem si = r.pendingStarts.get(i);
                 if (DEBUG_SERVICE) Log.v(TAG, "Sending arguments to service: "
-                        + r.name + " " + r.intent + " args=" + args);
+                        + r.name + " " + r.intent + " args=" + si.intent);
+                if (si.intent == null && N > 0) {
+                    // If somehow we got a dummy start at the front, then
+                    // just drop it here.
+                    i++;
+                    continue;
+                }
                 bumpServiceExecutingLocked(r);
                 if (!oomAdjusted) {
                     oomAdjusted = true;
                     updateOomAdjLocked(r.app);
                 }
-                r.app.thread.scheduleServiceArgs(r, BASEID+i, args);
+                int flags = 0;
+                if (si.deliveryCount > 0) {
+                    flags |= Service.START_FLAG_RETRY;
+                }
+                if (si.doneExecutingCount > 0) {
+                    flags |= Service.START_FLAG_REDELIVERY;
+                }
+                r.app.thread.scheduleServiceArgs(r, si.id, flags, si.intent);
+                si.deliveredTime = SystemClock.uptimeMillis();
+                r.deliveredStarts.add(si);
+                si.deliveryCount++;
                 i++;
+            } catch (RemoteException e) {
+                // Remote process gone...  we'll let the normal cleanup take
+                // care of this.
+                break;
             } catch (Exception e) {
+                Log.w(TAG, "Unexpected exception", e);
                 break;
             }
         }
         if (i == N) {
-            r.startArgs.clear();
+            r.pendingStarts.clear();
         } else {
             while (i > 0) {
-                r.startArgs.remove(0);
                 i--;
+                r.pendingStarts.remove(i);
             }
         }
     }
@@ -9942,19 +9978,61 @@
         } finally {
             if (!created) {
                 app.services.remove(r);
-                scheduleServiceRestartLocked(r);
+                scheduleServiceRestartLocked(r, false);
             }
         }
 
         requestServiceBindingsLocked(r);
+        
+        // If the service is in the started state, and there are no
+        // pending arguments, then fake up one so its onStartCommand() will
+        // be called.
+        if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {
+            r.lastStartId++;
+            if (r.lastStartId < 1) {
+                r.lastStartId = 1;
+            }
+            r.pendingStarts.add(new ServiceRecord.StartItem(r.lastStartId, null));
+        }
+        
         sendServiceArgsLocked(r, true);
     }
 
-    private final void scheduleServiceRestartLocked(ServiceRecord r) {
+    private final boolean scheduleServiceRestartLocked(ServiceRecord r,
+            boolean allowCancel) {
+        boolean canceled = false;
+        
+        long minDuration = SERVICE_RESTART_DURATION;
+        long resetTime = minDuration*2*2*2;
+        
+        // Any delivered but not yet finished starts should be put back
+        // on the pending list.
+        final int N = r.deliveredStarts.size();
+        if (N > 0) {
+            for (int i=N-1; i>=0; i--) {
+                ServiceRecord.StartItem si = r.deliveredStarts.get(i);
+                if (si.intent == null) {
+                    // We'll generate this again if needed.
+                } else if (!allowCancel || (si.deliveryCount < ServiceRecord.MAX_DELIVERY_COUNT
+                        && si.doneExecutingCount < ServiceRecord.MAX_DONE_EXECUTING_COUNT)) {
+                    r.pendingStarts.add(0, si);
+                    long dur = SystemClock.uptimeMillis() - si.deliveredTime;
+                    dur *= 2;
+                    if (minDuration < dur) minDuration = dur;
+                    if (resetTime < dur) resetTime = dur;
+                } else {
+                    Log.w(TAG, "Canceling start item " + si.intent + " in service "
+                            + r.name);
+                    canceled = true;
+                }
+            }
+            r.deliveredStarts.clear();
+        }
+        
         r.totalRestartCount++;
         if (r.restartDelay == 0) {
             r.restartCount++;
-            r.restartDelay = SERVICE_RESTART_DURATION;
+            r.restartDelay = minDuration;
         } else {
             // If it has been a "reasonably long time" since the service
             // was started, then reset our restart duration back to
@@ -9962,17 +10040,21 @@
             // on a service that just occasionally gets killed (which is
             // a normal case, due to process being killed to reclaim memory).
             long now = SystemClock.uptimeMillis();
-            if (now > (r.restartTime+(SERVICE_RESTART_DURATION*2*2*2))) {
+            if (now > (r.restartTime+resetTime)) {
                 r.restartCount = 1;
-                r.restartDelay = SERVICE_RESTART_DURATION;
+                r.restartDelay = minDuration;
             } else {
-                r.restartDelay *= 2;
+                r.restartDelay *= 4;
+                if (r.restartDelay < minDuration) {
+                    r.restartDelay = minDuration;
+                }
             }
         }
         if (!mRestartingServices.contains(r)) {
             mRestartingServices.add(r);
         }
         r.cancelNotification();
+        
         mHandler.removeCallbacks(r.restarter);
         mHandler.postDelayed(r.restarter, r.restartDelay);
         r.nextRestartTime = SystemClock.uptimeMillis() + r.restartDelay;
@@ -9985,6 +10067,8 @@
         msg.what = SERVICE_ERROR_MSG;
         msg.obj = r;
         mHandler.sendMessage(msg);
+        
+        return canceled;
     }
 
     final void performServiceRestartLocked(ServiceRecord r) {
@@ -10146,6 +10230,10 @@
         r.foregroundId = 0;
         r.foregroundNoti = null;
         
+        // Clear start entries.
+        r.deliveredStarts.clear();
+        r.pendingStarts.clear();
+        
         if (r.app != null) {
             synchronized (r.stats.getBatteryStats()) {
                 r.stats.stopLaunchedLocked();
@@ -10207,11 +10295,12 @@
                         + r.shortName);
             }
             r.startRequested = true;
-            r.startArgs.add(service);
+            r.callStart = false;
             r.lastStartId++;
             if (r.lastStartId < 1) {
                 r.lastStartId = 1;
             }
+            r.pendingStarts.add(new ServiceRecord.StartItem(r.lastStartId, service));
             r.lastActivity = SystemClock.uptimeMillis();
             synchronized (r.stats.getBatteryStats()) {
                 r.stats.startRunningLocked();
@@ -10279,6 +10368,7 @@
                         r.record.stats.stopRunningLocked();
                     }
                     r.record.startRequested = false;
+                    r.record.callStart = false;
                     final long origId = Binder.clearCallingIdentity();
                     bringDownServiceLocked(r.record, false);
                     Binder.restoreCallingIdentity(origId);
@@ -10327,10 +10417,35 @@
             if (DEBUG_SERVICE) Log.v(TAG, "stopServiceToken: " + className
                     + " " + token + " startId=" + startId);
             ServiceRecord r = findServiceLocked(className, token);
-            if (r != null && (startId < 0 || r.lastStartId == startId)) {
+            if (r != null) {
+                if (startId >= 0) {
+                    // Asked to only stop if done with all work.  Note that
+                    // to avoid leaks, we will take this as dropping all
+                    // start items up to and including this one.
+                    ServiceRecord.StartItem si = r.findDeliveredStart(startId, false);
+                    if (si != null) {
+                        while (r.deliveredStarts.size() > 0) {
+                            if (r.deliveredStarts.remove(0) == si) {
+                                break;
+                            }
+                        }
+                    }
+                    
+                    if (r.lastStartId != startId) {
+                        return false;
+                    }
+                    
+                    if (r.deliveredStarts.size() > 0) {
+                        Log.w(TAG, "stopServiceToken startId " + startId
+                                + " is last, but have " + r.deliveredStarts.size()
+                                + " remaining args");
+                    }
+                }
+                
                 synchronized (r.stats.getBatteryStats()) {
                     r.stats.stopRunningLocked();
                     r.startRequested = false;
+                    r.callStart = false;
                 }
                 final long origId = Binder.clearCallingIdentity();
                 bringDownServiceLocked(r, false);
@@ -10674,7 +10789,7 @@
         }
     }
 
-    public void serviceDoneExecuting(IBinder token) {
+    public void serviceDoneExecuting(IBinder token, int type, int startId, int res) {
         synchronized(this) {
             if (!(token instanceof ServiceRecord)) {
                 throw new IllegalArgumentException("Invalid service token");
@@ -10692,6 +10807,51 @@
                     return;
                 }
 
+                if (type == 1) {
+                    // This is a call from a service start...  take care of
+                    // book-keeping.
+                    r.callStart = true;
+                    switch (res) {
+                        case Service.START_STICKY_COMPATIBILITY:
+                        case Service.START_STICKY: {
+                            // We are done with the associated start arguments.
+                            r.findDeliveredStart(startId, true);
+                            // Don't stop if killed.
+                            r.stopIfKilled = false;
+                            break;
+                        }
+                        case Service.START_NOT_STICKY: {
+                            // We are done with the associated start arguments.
+                            r.findDeliveredStart(startId, true);
+                            if (r.lastStartId == startId) {
+                                // There is no more work, and this service
+                                // doesn't want to hang around if killed.
+                                r.stopIfKilled = true;
+                            }
+                            break;
+                        }
+                        case Service.START_REDELIVER_INTENT: {
+                            // We'll keep this item until they explicitly
+                            // call stop for it, but keep track of the fact
+                            // that it was delivered.
+                            ServiceRecord.StartItem si = r.findDeliveredStart(startId, false);
+                            if (si != null) {
+                                si.deliveryCount = 0;
+                                si.doneExecutingCount++;
+                                // Don't stop if killed.
+                                r.stopIfKilled = true;
+                            }
+                            break;
+                        }
+                        default:
+                            throw new IllegalArgumentException(
+                                    "Unknown service start result: " + res);
+                    }
+                    if (res == Service.START_STICKY_COMPATIBILITY) {
+                        r.callStart = false;
+                    }
+                }
+                
                 final long origId = Binder.clearCallingIdentity();
                 serviceDoneExecutingLocked(r, inStopping);
                 Binder.restoreCallingIdentity(origId);