Merge "Initial implementation of IntelligenceService.onContentCaptureEvents()"
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 8d54e91..86ed267 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -120,6 +120,7 @@
 import android.view.autofill.AutofillManager.AutofillClient;
 import android.view.autofill.AutofillPopupWindow;
 import android.view.autofill.IAutofillWindowPresenter;
+import android.view.intelligence.ContentCaptureEvent;
 import android.view.intelligence.IntelligenceManager;
 import android.widget.AdapterView;
 import android.widget.Toast;
@@ -1023,6 +1024,31 @@
         return mIntelligenceManager;
     }
 
+    private void notifyIntelligenceManagerIfNeeded(@ContentCaptureEvent.EventType int event) {
+        final IntelligenceManager im = getIntelligenceManager();
+        if (im == null || !im.isContentCaptureEnabled()) {
+            return;
+        }
+        switch (event) {
+            case ContentCaptureEvent.TYPE_ACTIVITY_CREATED:
+                //TODO(b/111276913): decide whether the InteractionSessionId should be
+                // saved / restored in the activity bundle.
+                im.onActivityCreated(mToken, getComponentName());
+                break;
+            case ContentCaptureEvent.TYPE_ACTIVITY_DESTROYED:
+                im.onActivityDestroyed();
+                break;
+            case ContentCaptureEvent.TYPE_ACTIVITY_STARTED:
+            case ContentCaptureEvent.TYPE_ACTIVITY_RESUMED:
+            case ContentCaptureEvent.TYPE_ACTIVITY_PAUSED:
+            case ContentCaptureEvent.TYPE_ACTIVITY_STOPPED:
+                im.onActivityLifecycleEvent(event);
+                break;
+            default:
+                Log.w(TAG, "notifyIntelligenceManagerIfNeeded(): invalid type " + event);
+        }
+    }
+
     @Override
     protected void attachBaseContext(Context newBase) {
         super.attachBaseContext(newBase);
@@ -1099,11 +1125,7 @@
         mRestoredFromBundle = savedInstanceState != null;
         mCalled = true;
 
-        if (getIntelligenceManager() != null) {
-            //TODO(b/111276913): decide whether the screen_obs session id should be saved / restored
-            // in the activity bundle.
-            mIntelligenceManager.onActivityCreated(mToken, getComponentName());
-        }
+        notifyIntelligenceManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_CREATED);
     }
 
     /**
@@ -1337,6 +1359,7 @@
         if (mAutoFillResetNeeded) {
             getAutofillManager().onVisibleForAutofill();
         }
+        notifyIntelligenceManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_STARTED);
     }
 
     /**
@@ -1419,6 +1442,7 @@
                 }
             }
         }
+        notifyIntelligenceManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_RESUMED);
         mCalled = true;
     }
 
@@ -1812,6 +1836,7 @@
                 mAutoFillIgnoreFirstResumePause = false;
             }
         }
+        notifyIntelligenceManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_PAUSED);
         mCalled = true;
     }
 
@@ -2000,6 +2025,7 @@
                 getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_CANCEL,
                         mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN));
             }
+            notifyIntelligenceManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_STOPPED);
         }
     }
 
@@ -2071,9 +2097,8 @@
 
         getApplication().dispatchActivityDestroyed(this);
 
-        if (getIntelligenceManager() != null) {
-            mIntelligenceManager.onActivityDestroyed();
-        }
+        notifyIntelligenceManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_DESTROYED);
+
     }
 
     /**
diff --git a/core/java/android/service/intelligence/IIntelligenceService.aidl b/core/java/android/service/intelligence/IIntelligenceService.aidl
index ee93326..bacad8b 100644
--- a/core/java/android/service/intelligence/IIntelligenceService.aidl
+++ b/core/java/android/service/intelligence/IIntelligenceService.aidl
@@ -19,6 +19,11 @@
 import android.service.intelligence.InteractionSessionId;
 import android.service.intelligence.InteractionContext;
 
+import android.view.intelligence.ContentCaptureEvent;
+
+import java.util.List;
+
+
 /**
  * Interface from the system to an intelligence service.
  *
@@ -28,4 +33,7 @@
 
     // Called when session is created (context not null) or destroyed (context null)
     void onSessionLifecycle(in InteractionContext context, in InteractionSessionId sessionId);
+
+    void onContentCaptureEvents(in InteractionSessionId sessionId,
+                                in List<ContentCaptureEvent> events);
 }
diff --git a/core/java/android/service/intelligence/IntelligenceService.java b/core/java/android/service/intelligence/IntelligenceService.java
index ce0a88a..a2b60f0 100644
--- a/core/java/android/service/intelligence/IntelligenceService.java
+++ b/core/java/android/service/intelligence/IntelligenceService.java
@@ -70,6 +70,14 @@
                                 IntelligenceService.this, sessionId));
             }
         }
+        @Override
+        public void onContentCaptureEvents(InteractionSessionId sessionId,
+                List<ContentCaptureEvent> events) {
+            mHandler.sendMessage(
+                    obtainMessage(IntelligenceService::onContentCaptureEvent,
+                            IntelligenceService.this, sessionId, events));
+
+        }
     };
 
     @CallSuper
@@ -105,6 +113,7 @@
      * @param sessionId the session's Id
      * @param events the events
      */
+     // TODO(b/111276913): rename to onContentCaptureEvents
     public abstract void onContentCaptureEvent(@NonNull InteractionSessionId sessionId,
             @NonNull List<ContentCaptureEvent> events);
 
diff --git a/core/java/android/view/intelligence/ContentCaptureEvent.aidl b/core/java/android/view/intelligence/ContentCaptureEvent.aidl
new file mode 100644
index 0000000..c66a6cb
--- /dev/null
+++ b/core/java/android/view/intelligence/ContentCaptureEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.intelligence;
+
+parcelable ContentCaptureEvent;
diff --git a/core/java/android/view/intelligence/ContentCaptureEvent.java b/core/java/android/view/intelligence/ContentCaptureEvent.java
index d6aec34..2530ae3 100644
--- a/core/java/android/view/intelligence/ContentCaptureEvent.java
+++ b/core/java/android/view/intelligence/ContentCaptureEvent.java
@@ -30,6 +30,11 @@
 @SystemApi
 public final class ContentCaptureEvent implements Parcelable {
 
+    /** @hide */
+    public static final int TYPE_ACTIVITY_DESTROYED = -2;
+    /** @hide */
+    public static final int TYPE_ACTIVITY_CREATED = -1;
+
     /**
      * Called when the activity is started.
      */
@@ -85,10 +90,18 @@
             TYPE_VIEW_TEXT_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
-    @interface EventType{}
+    public @interface EventType{}
+
+    private final int mType;
+    private final long mEventTime;
+    private final int mFlags;
+
 
     /** @hide */
-    ContentCaptureEvent() {
+    public ContentCaptureEvent(int type, long eventTime, int flags) {
+        mType = type;
+        mEventTime = eventTime;
+        mFlags = flags;
     }
 
     /**
@@ -99,14 +112,14 @@
      * {@link #TYPE_VIEW_ADDED}, {@link #TYPE_VIEW_REMOVED}, or {@link #TYPE_VIEW_TEXT_CHANGED}.
      */
     public @EventType int getType() {
-        return 42;
+        return mType;
     }
 
     /**
      * Gets when the event was generated, in ms.
      */
     public long getEventTime() {
-        return 48151623;
+        return mEventTime;
     }
 
     /**
@@ -116,7 +129,7 @@
      * {@link android.view.intelligence.IntelligenceManager#FLAG_USER_INPUT}.
      */
     public int getFlags() {
-        return 0;
+        return mFlags;
     }
 
     /**
@@ -150,13 +163,25 @@
     }
 
     @Override
+    public String toString() {
+        final StringBuilder string = new StringBuilder("ContentCaptureEvent[type=")
+                .append(getTypeAsString(mType)).append(", time=").append(mEventTime);
+        if (mFlags > 0) {
+            string.append(", flags=").append(mFlags);
+        }
+        return string.append(']').toString();
+    }
+
+    @Override
     public int describeContents() {
         return 0;
     }
 
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
-        // TODO(b/111276913): implement
+        parcel.writeInt(mType);
+        parcel.writeLong(mEventTime);
+        parcel.writeInt(mFlags);
     }
 
     public static final Parcelable.Creator<ContentCaptureEvent> CREATOR =
@@ -164,8 +189,10 @@
 
         @Override
         public ContentCaptureEvent createFromParcel(Parcel parcel) {
-            // TODO(b/111276913): implement
-            return null;
+            final int type = parcel.readInt();
+            final long eventTime  = parcel.readLong();
+            final int flags = parcel.readInt();
+            return new ContentCaptureEvent(type, eventTime, flags);
         }
 
         @Override
@@ -173,4 +200,27 @@
             return new ContentCaptureEvent[size];
         }
     };
+
+
+    /** @hide */
+    public static String getTypeAsString(@EventType int type) {
+        switch (type) {
+            case TYPE_ACTIVITY_STARTED:
+                return "ACTIVITY_STARTED";
+            case TYPE_ACTIVITY_RESUMED:
+                return "ACTIVITY_RESUMED";
+            case TYPE_ACTIVITY_PAUSED:
+                return "ACTIVITY_PAUSED";
+            case TYPE_ACTIVITY_STOPPED:
+                return "ACTIVITY_STOPPED";
+            case TYPE_VIEW_ADDED:
+                return "VIEW_ADDED";
+            case TYPE_VIEW_REMOVED:
+                return "VIEW_REMOVED";
+            case TYPE_VIEW_TEXT_CHANGED:
+                return "VIEW_TEXT_CHANGED";
+            default:
+                return "UKNOWN_TYPE: " + type;
+        }
+    }
 }
diff --git a/core/java/android/view/intelligence/IIntelligenceManager.aidl b/core/java/android/view/intelligence/IIntelligenceManager.aidl
index f4901c3..7bbe99a 100644
--- a/core/java/android/view/intelligence/IIntelligenceManager.aidl
+++ b/core/java/android/view/intelligence/IIntelligenceManager.aidl
@@ -17,9 +17,15 @@
 package android.view.intelligence;
 
 import android.content.ComponentName;
+
 import android.os.IBinder;
+
+import android.view.intelligence.ContentCaptureEvent;
+
 import com.android.internal.os.IResultReceiver;
 
+import java.util.List;
+
 /**
  * {@hide}
  */
@@ -33,6 +39,14 @@
     /**
       * Finishes a session.
       */
+    // TODO(b/111276913): pass just (global) session id
     void finishSession(int userId, IBinder activityToken, in ComponentName componentName,
                        int localSessionId, int globalSessionId);
+
+    /**
+      * Sends a batch of events
+      */
+    // TODO(b/111276913): pass just (global) session id
+    void sendEvents(int userId, IBinder activityToken, in ComponentName componentName,
+                    int localSessionId, int globalSessionId, in List<ContentCaptureEvent> events);
 }
diff --git a/core/java/android/view/intelligence/IntelligenceManager.java b/core/java/android/view/intelligence/IntelligenceManager.java
index b1d06f7..a30d77e 100644
--- a/core/java/android/view/intelligence/IntelligenceManager.java
+++ b/core/java/android/view/intelligence/IntelligenceManager.java
@@ -24,13 +24,17 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.Log;
+import android.view.intelligence.ContentCaptureEvent.EventType;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.util.Preconditions;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -161,6 +165,38 @@
         }
     }
 
+    /**
+     * Used for intermediate events (i.e, other than created and destroyed).
+     *
+     * @hide
+     */
+    public void onActivityLifecycleEvent(@EventType int type) {
+        if (!isContentCaptureEnabled()) return;
+
+        //TODO(b/111276913): should buffer event (and call service on handler thread), instead of
+        // calling right away
+        final ContentCaptureEvent event = new ContentCaptureEvent(type, SystemClock.uptimeMillis(),
+                0);
+        final List<ContentCaptureEvent> events = Arrays.asList(event);
+
+        synchronized (mLock) {
+            //TODO(b/111276913): check session state; for example, how to handle if it's waiting for
+            // remote id
+
+            if (VERBOSE) {
+                Log.v(TAG, "onActivityLifecycleEvent() for " + mComponentName.flattenToShortString()
+                        + ": " + ContentCaptureEvent.getTypeAsString(type));
+            }
+
+            try {
+                mService.sendEvents(mContext.getUserId(), mApplicationToken, mComponentName,
+                        mLocalSessionId, mRemoteSessionId, events);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
     /** @hide */
     public void onActivityDestroyed() {
         if (!isContentCaptureEnabled()) return;
diff --git a/services/autofill/java/com/android/server/intelligence/ContentCaptureSession.java b/services/autofill/java/com/android/server/intelligence/ContentCaptureSession.java
index a437a39..14b28d3 100644
--- a/services/autofill/java/com/android/server/intelligence/ContentCaptureSession.java
+++ b/services/autofill/java/com/android/server/intelligence/ContentCaptureSession.java
@@ -23,12 +23,14 @@
 import android.service.intelligence.InteractionContext;
 import android.service.intelligence.InteractionSessionId;
 import android.util.Slog;
+import android.view.intelligence.ContentCaptureEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.AbstractRemoteService;
 import com.android.server.intelligence.RemoteIntelligenceService.RemoteIntelligenceServiceCallbacks;
 
 import java.io.PrintWriter;
+import java.util.List;
 
 final class ContentCaptureSession implements RemoteIntelligenceServiceCallbacks {
 
@@ -66,6 +68,13 @@
     }
 
     /**
+     * Notifies the {@link IntelligenceService} of a batch of events.
+     */
+    public void sendEventsLocked(List<ContentCaptureEvent> events) {
+        mRemoteService.onContentCaptureEventsRequest(mId, events);
+    }
+
+    /**
      * Cleans up the session and remove itself from the service.
      *
      * @param notifyRemoteService whether it should trigger a {@link
@@ -95,10 +104,10 @@
     }
 
     @Override // from RemoteScreenObservationServiceCallbacks
-    public void onSessionLifecycleRequestFailureOrTimeout(boolean timedOut) {
+    public void onFailureOrTimeout(boolean timedOut) {
         // TODO(b/111276913): log metrics on whether timed out or not
         if (mService.isDebug()) {
-            Slog.d(TAG, "onSessionLifecycleRequestFailure(" + mId + "): timed out=" + timedOut);
+            Slog.d(TAG, "onFailureOrTimeout(" + mId + "): timed out=" + timedOut);
         }
         synchronized (mLock) {
             removeSelfLocked(/* notifyRemoteService= */ false);
diff --git a/services/autofill/java/com/android/server/intelligence/IntelligenceManagerService.java b/services/autofill/java/com/android/server/intelligence/IntelligenceManagerService.java
index 4ea9036..d67c15b 100644
--- a/services/autofill/java/com/android/server/intelligence/IntelligenceManagerService.java
+++ b/services/autofill/java/com/android/server/intelligence/IntelligenceManagerService.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.os.IBinder;
 import android.os.UserManager;
+import android.view.intelligence.ContentCaptureEvent;
 import android.view.intelligence.IIntelligenceManager;
 
 import com.android.internal.annotations.GuardedBy;
@@ -35,6 +36,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.List;
 
 /**
  * A service used to observe the contents of the screen.
@@ -104,6 +106,18 @@
         }
 
         @Override
+        public void sendEvents(int userId, @NonNull IBinder activityToken,
+                @NonNull ComponentName componentName, int localSessionId, int globalSessionId,
+                List<ContentCaptureEvent> events) {
+            Preconditions.checkNotNull(events);
+
+            synchronized (mLock) {
+                final IntelligencePerUserService service = getServiceForUserLocked(userId);
+                service.sendEventsLocked(componentName, events);
+            }
+        }
+
+        @Override
         public void finishSession(int userId, @NonNull IBinder activityToken,
                 @NonNull ComponentName componentName, int localSessionId, int globalSessionId) {
             Preconditions.checkNotNull(activityToken);
diff --git a/services/autofill/java/com/android/server/intelligence/IntelligencePerUserService.java b/services/autofill/java/com/android/server/intelligence/IntelligencePerUserService.java
index b62b239..1d98819 100644
--- a/services/autofill/java/com/android/server/intelligence/IntelligencePerUserService.java
+++ b/services/autofill/java/com/android/server/intelligence/IntelligencePerUserService.java
@@ -27,12 +27,14 @@
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Slog;
+import android.view.intelligence.ContentCaptureEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.IResultReceiver;
 import com.android.server.AbstractPerUserSystemService;
 
 import java.io.PrintWriter;
+import java.util.List;
 
 /**
  * Per-user instance of {@link IntelligenceManagerService}.
@@ -142,6 +144,20 @@
     }
 
     @GuardedBy("mLock")
+    public void sendEventsLocked(@NonNull ComponentName componentName,
+            @NonNull List<ContentCaptureEvent> events) {
+        final ContentCaptureSession session = mSessions.get(componentName);
+        if (session == null) {
+            Slog.w(TAG, "sendEventsLocked(): no session for " + componentName);
+            return;
+        }
+        if (mMaster.verbose) {
+            Slog.v(TAG, "sendEventsLocked(): comp=" + componentName + "; events =" + events.size());
+        }
+        session.sendEventsLocked(events);
+    }
+
+    @GuardedBy("mLock")
     public void removeSessionLocked(@NonNull ComponentName key) {
         mSessions.remove(key);
     }
@@ -171,4 +187,5 @@
             Slog.w(TAG, "Error async reporting result to client: " + e);
         }
     }
+
 }
diff --git a/services/autofill/java/com/android/server/intelligence/RemoteIntelligenceService.java b/services/autofill/java/com/android/server/intelligence/RemoteIntelligenceService.java
index ee66b4e..9d241fb 100644
--- a/services/autofill/java/com/android/server/intelligence/RemoteIntelligenceService.java
+++ b/services/autofill/java/com/android/server/intelligence/RemoteIntelligenceService.java
@@ -27,9 +27,12 @@
 import android.service.intelligence.InteractionSessionId;
 import android.text.format.DateUtils;
 import android.util.Slog;
+import android.view.intelligence.ContentCaptureEvent;
 
 import com.android.server.AbstractRemoteService;
 
+import java.util.List;
+
 final class RemoteIntelligenceService extends AbstractRemoteService {
 
     private static final String TAG = "RemoteIntelligenceService";
@@ -79,44 +82,89 @@
         scheduleRequest(new PendingSessionLifecycleRequest(this, context, sessionId));
     }
 
-    private static final class PendingSessionLifecycleRequest
+    /**
+     * Called by {@link ContentCaptureSession} to send a batch of events to the service.
+     */
+    public void onContentCaptureEventsRequest(@NonNull InteractionSessionId sessionId,
+            @NonNull List<ContentCaptureEvent> events) {
+        cancelScheduledUnbind();
+        scheduleRequest(new PendingOnContentCaptureEventsRequest(this, sessionId, events));
+    }
+
+
+    private abstract static class MyPendingRequest
             extends PendingRequest<RemoteIntelligenceService> {
+        protected final InteractionSessionId mSessionId;
 
-        private final InteractionContext mContext;
-        private final InteractionSessionId mSessionId;
-
-        protected PendingSessionLifecycleRequest(@NonNull RemoteIntelligenceService service,
-                @Nullable InteractionContext context, @NonNull InteractionSessionId sessionId) {
+        private MyPendingRequest(@NonNull RemoteIntelligenceService service,
+                @NonNull InteractionSessionId sessionId) {
             super(service);
-            mContext = context;
             mSessionId = sessionId;
         }
 
         @Override // from PendingRequest
-        public void run() {
+        protected final void onTimeout(RemoteIntelligenceService remoteService) {
+            Slog.w(TAG, "timed out handling " + getClass().getSimpleName() + " for "
+                    + mSessionId);
+            remoteService.mCallbacks.onFailureOrTimeout(/* timedOut= */ true);
+        }
+
+        @Override // from PendingRequest
+        public final void run() {
             final RemoteIntelligenceService remoteService = getService();
             if (remoteService != null) {
                 try {
-                    remoteService.mService.onSessionLifecycle(mContext, mSessionId);
+                    myRun(remoteService);
+                    // We don't expect the service to call us back, so we finish right away.
+                    finish();
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "exception handling PendingSessionLifecycleRequest for "
+                    Slog.w(TAG, "exception handling " + getClass().getSimpleName() + " for "
                             + mSessionId + ": " + e);
-                    remoteService.mCallbacks
-                        .onSessionLifecycleRequestFailureOrTimeout(/* timedOut= */ false);
+                    remoteService.mCallbacks.onFailureOrTimeout(/* timedOut= */ false);
                 }
             }
         }
 
-        @Override // from PendingRequest
-        protected void onTimeout(RemoteIntelligenceService remoteService) {
-            Slog.w(TAG, "timed out handling PendingSessionLifecycleRequest for "
-                    + mSessionId);
-            remoteService.mCallbacks
-                .onSessionLifecycleRequestFailureOrTimeout(/* timedOut= */ true);
+        protected abstract void myRun(@NonNull RemoteIntelligenceService service)
+                throws RemoteException;
+
+    }
+
+    private static final class PendingSessionLifecycleRequest extends MyPendingRequest {
+
+        private final InteractionContext mContext;
+
+        protected PendingSessionLifecycleRequest(@NonNull RemoteIntelligenceService service,
+                @Nullable InteractionContext context, @NonNull InteractionSessionId sessionId) {
+            super(service, sessionId);
+            mContext = context;
+        }
+
+        @Override // from MyPendingRequest
+        public void myRun(@NonNull RemoteIntelligenceService remoteService) throws RemoteException {
+            remoteService.mService.onSessionLifecycle(mContext, mSessionId);
+        }
+    }
+
+    private static final class PendingOnContentCaptureEventsRequest extends MyPendingRequest {
+
+        private final List<ContentCaptureEvent> mEvents;
+
+        protected PendingOnContentCaptureEventsRequest(@NonNull RemoteIntelligenceService service,
+                @NonNull InteractionSessionId sessionId,
+                @NonNull List<ContentCaptureEvent> events) {
+            super(service, sessionId);
+            mEvents = events;
+        }
+
+        @Override // from MyPendingRequest
+        public void myRun(@NonNull RemoteIntelligenceService remoteService) throws RemoteException {
+            remoteService.mService.onContentCaptureEvents(mSessionId, mEvents);
         }
     }
 
     public interface RemoteIntelligenceServiceCallbacks extends VultureCallback {
-        void onSessionLifecycleRequestFailureOrTimeout(boolean timedOut);
+        // To keep it simple, we use the same callback for all failures / timeouts.
+        void onFailureOrTimeout(boolean timedOut);
     }
 }
diff --git a/services/core/java/com/android/server/AbstractRemoteService.java b/services/core/java/com/android/server/AbstractRemoteService.java
index 1d3a34c..181d7fd 100644
--- a/services/core/java/com/android/server/AbstractRemoteService.java
+++ b/services/core/java/com/android/server/AbstractRemoteService.java
@@ -366,11 +366,12 @@
                     mCompleted = true;
                 }
 
-                Slog.w(mTag, "timed out");
                 final S remoteService = mWeakService.get();
                 if (remoteService != null) {
-                    Slog.w(mTag, " timed out after " + service.getRemoteRequestMillis() + " ms");
+                    Slog.w(mTag, "timed out after " + service.getRemoteRequestMillis() + " ms");
                     onTimeout(remoteService);
+                } else {
+                    Slog.w(mTag, "timed out (no service)");
                 }
             };
             mServiceHandler.postAtTime(mTimeoutTrigger,