Initial implementation of the IntelligenceService pipeline.

It's still full of TODOs, but at leats it now provides an end-to-end
workflow from the activity creation / destruction to the service implementation.

Test: mmm -j packages/experimental/FillService && \
      adb install -r ${OUT}/data/app/FillService/FillService.apk && \
      adb shell settings put secure intel_service foo.bar.fill/.AiaiService
Bug: 111276913

Change-Id: Id5daf7b8b51e97c74d9b6ec00f953ddb02b48e46
diff --git a/core/java/android/view/intelligence/IntelligenceManager.java b/core/java/android/view/intelligence/IntelligenceManager.java
index 5513ce2f..b1d06f7 100644
--- a/core/java/android/view/intelligence/IntelligenceManager.java
+++ b/core/java/android/view/intelligence/IntelligenceManager.java
@@ -18,29 +18,174 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.SystemService;
 import android.content.ComponentName;
 import android.content.Context;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
 
+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.Set;
 
 /**
- * TODO(b/111276913): add javadocs / implement / add SystemService / PackageFeature
+ * TODO(b/111276913): add javadocs / implement
  */
+@SystemService(Context.INTELLIGENCE_MANAGER_SERVICE)
 public final class IntelligenceManager {
 
+    private static final String TAG = "IntelligenceManager";
+
+    // TODO(b/111276913): define a way to dynamically set it (for example, using settings?)
+    private static final boolean VERBOSE = false;
+
     /**
      * Used to indicate that a text change was caused by user input (for example, through IME).
      */
     //TODO(b/111276913): link to notifyTextChanged() method once available
     public static final int FLAG_USER_INPUT = 0x1;
 
-    private final Context mContext;
 
     /** @hide */
-    public IntelligenceManager(@NonNull Context context) {
+    public static final int NO_SESSION = 0;
+
+    /**
+     * Initial state, when there is no session.
+     *
+     * @hide
+     */
+    public static final int STATE_UNKNOWN = 0;
+
+    /**
+     * Service's startSession() was called, but remote session id was not returned yet.
+     *
+     * @hide
+     */
+    public static final int STATE_WAITING_FOR_SESSION_ID = 1;
+
+    /**
+     * Session is active.
+     *
+     * @hide
+     */
+    public static final int STATE_ACTIVE = 2;
+
+    private static int sNextSessionId;
+
+    private final Context mContext;
+
+    @Nullable
+    private final IIntelligenceManager mService;
+
+    private final Object mLock = new Object();
+
+    // TODO(b/111276913): localSessionId might be an overkill, perhaps just the global id is enough.
+    // Let's keep both for now, and revisit once we decide whether the session id will be persisted
+    // when the activity's process is killed
+    @GuardedBy("mLock")
+    private int mLocalSessionId = NO_SESSION;
+
+    @GuardedBy("mLock")
+    private int mRemoteSessionId = NO_SESSION;
+
+    @GuardedBy("mLock")
+    private int mState = STATE_UNKNOWN;
+
+    @GuardedBy("mLock")
+    private IBinder mApplicationToken;
+
+    // TODO(b/111276913): replace by an interface name implemented by Activity, similar to
+    // AutofillClient
+    @GuardedBy("mLock")
+    private ComponentName mComponentName;
+
+    /** @hide */
+    public IntelligenceManager(@NonNull Context context, @Nullable IIntelligenceManager service) {
         mContext = Preconditions.checkNotNull(context, "context cannot be null");
+        mService = service;
+    }
+
+    /** @hide */
+    public void onActivityCreated(@NonNull IBinder token, @NonNull ComponentName componentName) {
+        if (!isContentCaptureEnabled()) return;
+
+        synchronized (mLock) {
+            if (mState != STATE_UNKNOWN) {
+                Log.w(TAG, "ignoring onActivityStarted(" + token + ") while on state "
+                        + getStateAsStringLocked());
+                return;
+            }
+            mState = STATE_WAITING_FOR_SESSION_ID;
+            mLocalSessionId = ++sNextSessionId;
+            mRemoteSessionId = NO_SESSION;
+            mApplicationToken = token;
+            mComponentName = componentName;
+
+            if (VERBOSE) {
+                Log.v(TAG, "onActivityStarted(): token=" + token + ", act=" + componentName
+                        + ", localSessionId=" + mLocalSessionId);
+            }
+            final int flags = 0; // TODO(b/111276913): get proper flags
+
+            try {
+                mService.startSession(mContext.getUserId(), mApplicationToken, componentName,
+                        mLocalSessionId, flags, new IResultReceiver.Stub() {
+                            @Override
+                            public void send(int resultCode, Bundle resultData)
+                                    throws RemoteException {
+                                synchronized (mLock) {
+                                    if (resultCode > 0) {
+                                        mRemoteSessionId = resultCode;
+                                        mState = STATE_ACTIVE;
+                                    } else {
+                                        // TODO(b/111276913): handle other cases like disabled by
+                                        // service
+                                        mState = STATE_UNKNOWN;
+                                    }
+                                    if (VERBOSE) {
+                                        Log.v(TAG, "onActivityStarted() result: code=" + resultCode
+                                                + ", remoteSession=" + mRemoteSessionId
+                                                + ", state=" + getStateAsStringLocked());
+                                    }
+                                }
+                            }
+                        });
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /** @hide */
+    public void onActivityDestroyed() {
+        if (!isContentCaptureEnabled()) return;
+
+        synchronized (mLock) {
+            //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
+            // id) and send it to the cache of batched commands
+
+            if (VERBOSE) {
+                Log.v(TAG, "onActivityDestroyed(): state=" + getStateAsStringLocked()
+                        + ", localSessionId=" + mLocalSessionId
+                        + ", mRemoteSessionId=" + mRemoteSessionId);
+            }
+
+            try {
+                mService.finishSession(mContext.getUserId(), mApplicationToken, mComponentName,
+                        mLocalSessionId, mRemoteSessionId);
+                mState = STATE_UNKNOWN;
+                mLocalSessionId = mRemoteSessionId = NO_SESSION;
+                mApplicationToken = null;
+                mComponentName = null;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
@@ -54,11 +199,13 @@
     }
 
     /**
-     * Checks whether contents capture is enabled for this activity.
+     * Checks whether content capture is enabled for this activity.
      */
     public boolean isContentCaptureEnabled() {
-        //TODO(b/111276913): implement
-        return false;
+        //TODO(b/111276913): properly implement by checking if it was explicitly disabled by
+        // service, or if service is not set
+        // (and probably renamign to isEnabledLocked()
+        return mService != null;
     }
 
     /**
@@ -68,6 +215,7 @@
      * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}.
      */
     public void disableContentCapture() {
+        //TODO(b/111276913): implement
     }
 
     /**
@@ -140,4 +288,41 @@
         //TODO(b/111276913): implement
         return null;
     }
+
+    /** @hide */
+    public void dump(String prefix, PrintWriter pw) {
+        pw.print(prefix); pw.println("IntelligenceManager");
+        final String prefix2 = prefix + "  ";
+        synchronized (mLock) {
+            pw.print(prefix2); pw.print("mContext: "); pw.println(mContext);
+            pw.print(prefix2); pw.print("mService: "); pw.println(mService);
+            pw.print(prefix2); pw.print("user: "); pw.println(mContext.getUserId());
+            pw.print(prefix2); pw.print("enabled: "); pw.println(isContentCaptureEnabled());
+            pw.print(prefix2); pw.print("mLocalSessionId: "); pw.println(mLocalSessionId);
+            pw.print(prefix2); pw.print("mRemoteSessionId: "); pw.println(mRemoteSessionId);
+            pw.print(prefix2); pw.print("mState: "); pw.print(mState); pw.print(" (");
+            pw.print(getStateAsStringLocked()); pw.println(")");
+            pw.print(prefix2); pw.print("mAppToken: "); pw.println(mApplicationToken);
+            pw.print(prefix2); pw.print("mComponentName: "); pw.println(mComponentName);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private String getStateAsStringLocked() {
+        return getStateAsString(mState);
+    }
+
+    @NonNull
+    private static String getStateAsString(int state) {
+        switch (state) {
+            case STATE_UNKNOWN:
+                return "UNKNOWN";
+            case STATE_WAITING_FOR_SESSION_ID:
+                return "WAITING_FOR_SESSION_ID";
+            case STATE_ACTIVE:
+                return "ACTIVE";
+            default:
+                return "INVALID:" + state;
+        }
+    }
 }