Add new method 'requestServiceFeatures' in AppPredictionService. It allows Frontend to get backend service impl features info (e.g. feature readiness)

Bug: 292565550
Test: atest AppPredictionServiceTest
Change-Id: I0f2ec9ba0c1b332fa73afb461c29fe25de9b01e2
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index f5bf437..60ef8b4 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -48,6 +48,7 @@
     ":android.service.controls.flags-aconfig-java{.generated_srcjars}",
     ":android.service.dreams.flags-aconfig-java{.generated_srcjars}",
     ":android.service.notification.flags-aconfig-java{.generated_srcjars}",
+    ":android.service.appprediction.flags-aconfig-java{.generated_srcjars}",
     ":android.service.voice.flags-aconfig-java{.generated_srcjars}",
     ":android.speech.flags-aconfig-java{.generated_srcjars}",
     ":android.tracing.flags-aconfig-java{.generated_srcjars}",
@@ -112,6 +113,7 @@
         "android.provider.flags-aconfig",
         "android.security.flags-aconfig",
         "android.server.app.flags-aconfig",
+        "android.service.appprediction.flags-aconfig",
         "android.service.autofill.flags-aconfig",
         "android.service.chooser.flags-aconfig",
         "android.service.controls.flags-aconfig",
@@ -698,6 +700,19 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+// App prediction
+aconfig_declarations {
+    name: "android.service.appprediction.flags-aconfig",
+    package: "android.service.appprediction.flags",
+    srcs: ["core/java/android/service/appprediction/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.service.appprediction.flags-aconfig-java",
+    aconfig_declarations: "android.service.appprediction.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Controls
 aconfig_declarations {
     name: "android.service.controls.flags-aconfig",
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 318badf..783180a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2193,6 +2193,7 @@
     method public void notifyLaunchLocationShown(@NonNull String, @NonNull java.util.List<android.app.prediction.AppTargetId>);
     method public void registerPredictionUpdates(@NonNull java.util.concurrent.Executor, @NonNull android.app.prediction.AppPredictor.Callback);
     method public void requestPredictionUpdate();
+    method @FlaggedApi("android.service.appprediction.flags.service_features_api") public void requestServiceFeatures(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.os.Bundle>);
     method @Nullable public void sortTargets(@NonNull java.util.List<android.app.prediction.AppTarget>, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>);
     method public void unregisterPredictionUpdates(@NonNull android.app.prediction.AppPredictor.Callback);
   }
@@ -11833,6 +11834,7 @@
     method @MainThread public void onDestroyPredictionSession(@NonNull android.app.prediction.AppPredictionSessionId);
     method @MainThread public abstract void onLaunchLocationShown(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull String, @NonNull java.util.List<android.app.prediction.AppTargetId>);
     method @MainThread public abstract void onRequestPredictionUpdate(@NonNull android.app.prediction.AppPredictionSessionId);
+    method @FlaggedApi("android.service.appprediction.flags.service_features_api") @MainThread public void onRequestServiceFeatures(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull java.util.function.Consumer<android.os.Bundle>);
     method @MainThread public abstract void onSortAppTargets(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull java.util.List<android.app.prediction.AppTarget>, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>);
     method @MainThread public void onStartPredictionUpdates();
     method @MainThread public void onStopPredictionUpdates();
diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java
index d628b7f..0c1a28a 100644
--- a/core/java/android/app/prediction/AppPredictor.java
+++ b/core/java/android/app/prediction/AppPredictor.java
@@ -16,6 +16,7 @@
 package android.app.prediction;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -24,9 +25,12 @@
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.service.appprediction.flags.Flags;
 import android.util.ArrayMap;
 import android.util.Log;
 
@@ -263,6 +267,34 @@
     }
 
     /**
+     * Requests a Bundle which includes service features info or {@code null} if the service is not
+     * available.
+     *
+     * @param callbackExecutor The callback executor to use when calling the callback. It cannot be
+     *                        null.
+     * @param callback The callback to return the Bundle which includes service features info. It
+     *                cannot be null.
+     *
+     * @throws IllegalStateException If this AppPredictor has already been destroyed.
+     * @throws RuntimeException If there is a failure communicating with the remote service.
+     */
+    @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API)
+    public void requestServiceFeatures(@NonNull Executor callbackExecutor,
+            @NonNull Consumer<Bundle> callback) {
+        if (mIsClosed.get()) {
+            throw new IllegalStateException("This client has already been destroyed.");
+        }
+
+        try {
+            mPredictionManager.requestServiceFeatures(mSessionId,
+                    new RemoteCallbackWrapper(callbackExecutor, callback));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to request service feature info", e);
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
      * Destroys the client and unregisters the callback. Any method on this class after this call
      * with throw {@link IllegalStateException}.
      */
@@ -347,6 +379,28 @@
         }
     }
 
+    static class RemoteCallbackWrapper extends IRemoteCallback.Stub {
+
+        private final Consumer<Bundle> mCallback;
+        private final Executor mExecutor;
+
+        RemoteCallbackWrapper(@NonNull Executor callbackExecutor,
+                @NonNull Consumer<Bundle> callback) {
+            mExecutor = callbackExecutor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void sendResult(Bundle result) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.accept(result));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
     private static class Token {
         static final IBinder sBinder = new Binder(TAG);
     }
diff --git a/core/java/android/app/prediction/IPredictionManager.aidl b/core/java/android/app/prediction/IPredictionManager.aidl
index 863fc6f9..94b4f5b 100644
--- a/core/java/android/app/prediction/IPredictionManager.aidl
+++ b/core/java/android/app/prediction/IPredictionManager.aidl
@@ -22,6 +22,7 @@
 import android.app.prediction.AppPredictionSessionId;
 import android.app.prediction.IPredictionCallback;
 import android.content.pm.ParceledListSlice;
+import android.os.IRemoteCallback;
 
 /**
  * @hide
@@ -48,4 +49,6 @@
     void requestPredictionUpdate(in AppPredictionSessionId sessionId);
 
     void onDestroyPredictionSession(in AppPredictionSessionId sessionId);
+
+    void requestServiceFeatures(in AppPredictionSessionId sessionId, in IRemoteCallback callback);
 }
diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java
index a2ffa5d..2402cfd 100644
--- a/core/java/android/service/appprediction/AppPredictionService.java
+++ b/core/java/android/service/appprediction/AppPredictionService.java
@@ -18,6 +18,7 @@
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 
 import android.annotation.CallSuper;
+import android.annotation.FlaggedApi;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -31,12 +32,15 @@
 import android.app.prediction.IPredictionCallback;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.service.appprediction.IPredictionService.Stub;
+import android.service.appprediction.flags.Flags;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
@@ -134,6 +138,16 @@
                     obtainMessage(AppPredictionService::doDestroyPredictionSession,
                             AppPredictionService.this, sessionId));
         }
+
+        @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API)
+        @Override
+        public void requestServiceFeatures(AppPredictionSessionId sessionId,
+                IRemoteCallback callback) {
+            mHandler.sendMessage(
+                    obtainMessage(AppPredictionService::onRequestServiceFeatures,
+                            AppPredictionService.this, sessionId,
+                            new RemoteCallbackWrapper(callback, null)));
+        }
     };
 
     @CallSuper
@@ -277,6 +291,18 @@
     public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {}
 
     /**
+     * Called by the client app to request {@link AppPredictionService} features info.
+     *
+     * @param sessionId the session's Id. It is @NonNull.
+     * @param callback the callback to return the Bundle which includes service features info. It
+     *                is @NonNull.
+     */
+    @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API)
+    @MainThread
+    public void onRequestServiceFeatures(@NonNull AppPredictionSessionId sessionId,
+            @NonNull Consumer<Bundle> callback) {}
+
+    /**
      * Used by the prediction factory to send back results the client app. The can be called
      * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as
      * a result of changes in predictions.
@@ -357,4 +383,50 @@
             }
         }
     }
+
+    private static final class RemoteCallbackWrapper implements Consumer<Bundle>,
+            IBinder.DeathRecipient {
+
+        private IRemoteCallback mCallback;
+        private final Consumer<RemoteCallbackWrapper> mOnBinderDied;
+
+        RemoteCallbackWrapper(IRemoteCallback callback,
+                @Nullable Consumer<RemoteCallbackWrapper> onBinderDied) {
+            mCallback = callback;
+            mOnBinderDied = onBinderDied;
+            if (mOnBinderDied != null) {
+                try {
+                    mCallback.asBinder().linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to link to death: " + e);
+                }
+            }
+        }
+
+        public void destroy() {
+            if (mCallback != null && mOnBinderDied != null) {
+                mCallback.asBinder().unlinkToDeath(this, 0);
+            }
+        }
+
+        @Override
+        public void accept(Bundle bundle) {
+            try {
+                if (mCallback != null) {
+                    mCallback.sendResult(bundle);
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error sending result:" + e);
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            destroy();
+            mCallback = null;
+            if (mOnBinderDied != null) {
+                mOnBinderDied.accept(this);
+            }
+        }
+    }
 }
diff --git a/core/java/android/service/appprediction/IPredictionService.aidl b/core/java/android/service/appprediction/IPredictionService.aidl
index 0f3df85..e144dfa 100644
--- a/core/java/android/service/appprediction/IPredictionService.aidl
+++ b/core/java/android/service/appprediction/IPredictionService.aidl
@@ -22,6 +22,7 @@
 import android.app.prediction.AppPredictionSessionId;
 import android.app.prediction.IPredictionCallback;
 import android.content.pm.ParceledListSlice;
+import android.os.IRemoteCallback;
 
 /**
  * Interface from the system to a prediction service.
@@ -50,4 +51,6 @@
     void requestPredictionUpdate(in AppPredictionSessionId sessionId);
 
     void onDestroyPredictionSession(in AppPredictionSessionId sessionId);
+
+    void requestServiceFeatures(in AppPredictionSessionId sessionId, in IRemoteCallback callback);
 }
diff --git a/core/java/android/service/appprediction/flags/flags.aconfig b/core/java/android/service/appprediction/flags/flags.aconfig
new file mode 100644
index 0000000..c7e47d4
--- /dev/null
+++ b/core/java/android/service/appprediction/flags/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.service.appprediction.flags"
+
+flag {
+  name: "service_features_api"
+  namespace: "systemui"
+  description: "Guards the new requestServiceFeatures api"
+  bug: "292565550"
+}
\ No newline at end of file
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java
index 2c50389..df4e699 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java
@@ -35,6 +35,7 @@
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Process;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
@@ -162,6 +163,13 @@
                     (service) -> service.onDestroyPredictionSessionLocked(sessionId));
         }
 
+        @Override
+        public void requestServiceFeatures(@NonNull AppPredictionSessionId sessionId,
+                IRemoteCallback callback) {
+            runForUserLocked("requestServiceFeatures", sessionId,
+                    (service) -> service.requestServiceFeaturesLocked(sessionId, callback));
+        }
+
         public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
                 @Nullable FileDescriptor err,
                 @NonNull String[] args, @Nullable ShellCallback callback,
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 84707a8..a0198f2 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -31,6 +31,7 @@
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ServiceInfo;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.provider.DeviceConfig;
@@ -237,6 +238,18 @@
         sessionInfo.destroy();
     }
 
+    /**
+     * Requests the service to provide AppPredictionService features info.
+     */
+    @GuardedBy("mLock")
+    public void requestServiceFeaturesLocked(@NonNull AppPredictionSessionId sessionId,
+            @NonNull IRemoteCallback callback) {
+        final AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+        if (sessionInfo == null) return;
+        resolveService(sessionId, true, sessionInfo.mUsesPeopleService,
+                s -> s.requestServiceFeatures(sessionId, callback));
+    }
+
     @Override
     public void onFailureOrTimeout(boolean timedOut) {
         if (isDebug()) {
diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java
index 885ed35..b9f00d7 100644
--- a/services/people/java/com/android/server/people/PeopleService.java
+++ b/services/people/java/com/android/server/people/PeopleService.java
@@ -38,6 +38,7 @@
 import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -474,6 +475,10 @@
             getDataManager().restore(userId, payload);
         }
 
+        @Override
+        public void requestServiceFeatures(AppPredictionSessionId sessionId,
+                IRemoteCallback callback) {}
+
         @VisibleForTesting
         SessionInfo getSessionInfo(AppPredictionSessionId sessionId) {
             return mSessions.get(sessionId);