Add AppFunctions Sidecar

Test: atest CtsAppFunctionTestCases
Flag: NONE sidecar library (non platform)

Change-Id: I2f8af326a2852314ab4ed9b54166024a8888dc63
diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp
new file mode 100644
index 0000000..09e2f42
--- /dev/null
+++ b/libs/appfunctions/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_sdk_library {
+    name: "com.google.android.appfunctions.sidecar",
+    owner: "google",
+    srcs: ["java/**/*.java"],
+    api_packages: ["com.google.android.appfunctions.sidecar"],
+    dex_preopt: {
+        enabled: false,
+    },
+    system_ext_specific: true,
+    no_dist: true,
+    unsafe_ignore_missing_latest_api: true,
+}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
new file mode 100644
index 0000000..504e329
--- /dev/null
+++ b/libs/appfunctions/api/current.txt
@@ -0,0 +1,49 @@
+// Signature format: 2.0
+package com.google.android.appfunctions.sidecar {
+
+  public final class AppFunctionManager {
+    ctor public AppFunctionManager(android.content.Context);
+    method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+  }
+
+  public abstract class AppFunctionService extends android.app.Service {
+    ctor public AppFunctionService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
+    field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+  }
+
+  public final class ExecuteAppFunctionRequest {
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public String getFunctionIdentifier();
+    method @NonNull public android.app.appsearch.GenericDocument getParameters();
+    method @NonNull public String getTargetPackageName();
+  }
+
+  public static final class ExecuteAppFunctionRequest.Builder {
+    ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String);
+    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build();
+    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
+    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
+  }
+
+  public final class ExecuteAppFunctionResponse {
+    method @Nullable public String getErrorMessage();
+    method @NonNull public android.os.Bundle getExtras();
+    method public int getResultCode();
+    method @NonNull public android.app.appsearch.GenericDocument getResultDocument();
+    method public boolean isSuccess();
+    method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle);
+    method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle);
+    field public static final String PROPERTY_RETURN_VALUE = "returnValue";
+    field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
+    field public static final int RESULT_DENIED = 1; // 0x1
+    field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
+    field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
+    field public static final int RESULT_OK = 0; // 0x0
+    field public static final int RESULT_TIMED_OUT = 5; // 0x5
+  }
+
+}
+
diff --git a/libs/appfunctions/api/removed.txt b/libs/appfunctions/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/system-current.txt b/libs/appfunctions/api/system-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/system-removed.txt b/libs/appfunctions/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/test-current.txt b/libs/appfunctions/api/test-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/test-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/test-removed.txt b/libs/appfunctions/api/test-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/test-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
new file mode 100644
index 0000000..b1dd467
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 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 com.google.android.appfunctions.sidecar;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+
+/**
+ * Provides app functions related functionalities.
+ *
+ * <p>App function is a specific piece of functionality that an app offers to the system. These
+ * functionalities can be integrated into various system features.
+ *
+ * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and
+ * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
+ */
+// TODO(b/357551503): Implement get and set enabled app function APIs.
+// TODO(b/367329899): Add sidecar library to Android B builds.
+public final class AppFunctionManager {
+    private final android.app.appfunctions.AppFunctionManager mManager;
+    private final Context mContext;
+
+    /**
+     * Creates an instance.
+     *
+     * @param context A {@link Context}.
+     * @throws java.lang.IllegalStateException if the underlying {@link
+     *   android.app.appfunctions.AppFunctionManager} is not found.
+     */
+    public AppFunctionManager(Context context) {
+        mContext = Objects.requireNonNull(context);
+        mManager = context.getSystemService(android.app.appfunctions.AppFunctionManager.class);
+        if (mManager == null) {
+            throw new IllegalStateException(
+                    "Underlying AppFunctionManager system service not found.");
+        }
+    }
+
+    /**
+     * Executes the app function.
+     *
+     * <p>Proxies request and response to the underlying {@link
+     * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
+     * response in the appropriate type required by the function.
+     */
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest sidecarRequest,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        Objects.requireNonNull(sidecarRequest);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        android.app.appfunctions.ExecuteAppFunctionRequest platformRequest =
+                SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest);
+        mManager.executeAppFunction(
+                platformRequest, executor, (platformResponse) -> {
+                    callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse(
+                            platformResponse));
+                });
+    }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
new file mode 100644
index 0000000..65959df
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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 com.google.android.appfunctions.sidecar;
+
+import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import java.util.function.Consumer;
+
+/**
+ * Abstract base class to provide app functions to the system.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourService"
+ *       android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
+ *    <intent-filter>
+ *      <action android:name="android.app.appfunctions.AppFunctionService" />
+ *    </intent-filter>
+ * </service>
+ * }
+ * </pre>
+ *
+ * <p>This class wraps {@link android.app.appfunctions.AppFunctionService} functionalities and
+ * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
+ *
+ * @see AppFunctionManager
+ */
+public abstract class AppFunctionService extends Service {
+    /**
+     * The permission to only allow system access to the functions through {@link
+     * AppFunctionManagerService}.
+     */
+    @NonNull
+    public static final String BIND_APP_FUNCTION_SERVICE =
+            "android.permission.BIND_APP_FUNCTION_SERVICE";
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service. To be supported, the
+     * service must also require the {@link BIND_APP_FUNCTION_SERVICE} permission so that other
+     * applications can not abuse it.
+     */
+    @NonNull
+    public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+
+    private final Binder mBinder =
+            android.app.appfunctions.AppFunctionService.createBinder(
+                    /* context= */ this,
+                    /* onExecuteFunction= */ (platformRequest, callback) -> {
+                        AppFunctionService.this.onExecuteFunction(
+                                SidecarConverter.getSidecarExecuteAppFunctionRequest(
+                                        platformRequest),
+                                (sidecarResponse) -> {
+                                    callback.accept(
+                                            SidecarConverter.getPlatformExecuteAppFunctionResponse(
+                                                    sidecarResponse));
+                                });
+                    }
+            );
+
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * @param request The function execution request.
+     * @param callback A callback to report back the result.
+     */
+    @MainThread
+    public abstract void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
new file mode 100644
index 0000000..fa6d2ff
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 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 com.google.android.appfunctions.sidecar;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/**
+ * A request to execute an app function.
+ *
+ * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionRequest} without parcel
+ * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
+ * platform API).
+ */
+public final class ExecuteAppFunctionRequest {
+    /** Returns the package name of the app that hosts the function. */
+    @NonNull private final String mTargetPackageName;
+
+    /**
+     * Returns the unique string identifier of the app function to be executed. TODO(b/357551503):
+     * Document how callers can get the available function identifiers.
+     */
+    @NonNull private final String mFunctionIdentifier;
+
+    /** Returns additional metadata relevant to this function execution request. */
+    @NonNull private final Bundle mExtras;
+
+    /**
+     * Returns the parameters required to invoke this function. Within this [GenericDocument], the
+     * property names are the names of the function parameters and the property values are the
+     * values of those parameters.
+     *
+     * <p>The document may have missing parameters. Developers are advised to implement defensive
+     * handling measures.
+     *
+     * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution
+     */
+    @NonNull private final GenericDocument mParameters;
+
+    private ExecuteAppFunctionRequest(
+            @NonNull String targetPackageName,
+            @NonNull String functionIdentifier,
+            @NonNull Bundle extras,
+            @NonNull GenericDocument parameters) {
+        mTargetPackageName = Objects.requireNonNull(targetPackageName);
+        mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        mExtras = Objects.requireNonNull(extras);
+        mParameters = Objects.requireNonNull(parameters);
+    }
+
+    /** Returns the package name of the app that hosts the function. */
+    @NonNull
+    public String getTargetPackageName() {
+        return mTargetPackageName;
+    }
+
+    /** Returns the unique string identifier of the app function to be executed. */
+    @NonNull
+    public String getFunctionIdentifier() {
+        return mFunctionIdentifier;
+    }
+
+    /**
+     * Returns the function parameters. The key is the parameter name, and the value is the
+     * parameter value.
+     *
+     * <p>The bundle may have missing parameters. Developers are advised to implement defensive
+     * handling measures.
+     */
+    @NonNull
+    public GenericDocument getParameters() {
+        return mParameters;
+    }
+
+    /** Returns the additional data relevant to this function execution. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /** Builder for {@link ExecuteAppFunctionRequest}. */
+    public static final class Builder {
+        @NonNull private final String mTargetPackageName;
+        @NonNull private final String mFunctionIdentifier;
+        @NonNull private Bundle mExtras = Bundle.EMPTY;
+
+        @NonNull
+        private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();
+
+        public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) {
+            mTargetPackageName = Objects.requireNonNull(targetPackageName);
+            mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        }
+
+        /** Sets the additional data relevant to this function execution. */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = Objects.requireNonNull(extras);
+            return this;
+        }
+
+        /** Sets the function parameters. */
+        @NonNull
+        public Builder setParameters(@NonNull GenericDocument parameters) {
+            Objects.requireNonNull(parameters);
+            mParameters = parameters;
+            return this;
+        }
+
+        /** Builds the {@link ExecuteAppFunctionRequest}. */
+        @NonNull
+        public ExecuteAppFunctionRequest build() {
+            return new ExecuteAppFunctionRequest(
+                    mTargetPackageName,
+                    mFunctionIdentifier,
+                    mExtras,
+                    mParameters);
+        }
+    }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
new file mode 100644
index 0000000..60c25fa
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2024 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 com.google.android.appfunctions.sidecar;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The response to an app function execution.
+ *
+ * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel
+ * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
+ * platform API).
+ */
+public final class ExecuteAppFunctionResponse {
+    /**
+     * The name of the property that stores the function return value within the {@code
+     * resultDocument}.
+     *
+     * <p>See {@link GenericDocument#getProperty(String)} for more information.
+     *
+     * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will
+     * be empty {@link GenericDocument}.
+     *
+     * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will
+     * return {@code null}.
+     *
+     * <p>See {@link #getResultDocument} for more information on extracting the return value.
+     */
+    public static final String PROPERTY_RETURN_VALUE = "returnValue";
+
+    /** The call was successful. */
+    public static final int RESULT_OK = 0;
+
+    /** The caller does not have the permission to execute an app function. */
+    public static final int RESULT_DENIED = 1;
+
+    /** An unknown error occurred while processing the call in the AppFunctionService. */
+    public static final int RESULT_APP_UNKNOWN_ERROR = 2;
+
+    /**
+     * An internal error occurred within AppFunctionManagerService.
+     *
+     * <p>This error may be considered similar to {@link IllegalStateException}
+     */
+    public static final int RESULT_INTERNAL_ERROR = 3;
+
+    /**
+     * The caller supplied invalid arguments to the call.
+     *
+     * <p>This error may be considered similar to {@link IllegalArgumentException}.
+     */
+    public static final int RESULT_INVALID_ARGUMENT = 4;
+
+    /** The operation was timed out. */
+    public static final int RESULT_TIMED_OUT = 5;
+
+    /** The result code of the app function execution. */
+    @ResultCode private final int mResultCode;
+
+    /**
+     * The error message associated with the result, if any. This is {@code null} if the result code
+     * is {@link #RESULT_OK}.
+     */
+    @Nullable private final String mErrorMessage;
+
+    /**
+     * Returns the return value of the executed function.
+     *
+     * <p>The return value is stored in a {@link GenericDocument} with the key {@link
+     * #PROPERTY_RETURN_VALUE}.
+     *
+     * <p>See {@link #getResultDocument} for more information on extracting the return value.
+     */
+    @NonNull private final GenericDocument mResultDocument;
+
+    /** Returns the additional metadata data relevant to this function execution response. */
+    @NonNull private final Bundle mExtras;
+
+    private ExecuteAppFunctionResponse(
+            @NonNull GenericDocument resultDocument,
+            @NonNull Bundle extras,
+            @ResultCode int resultCode,
+            @Nullable String errorMessage) {
+        mResultDocument = Objects.requireNonNull(resultDocument);
+        mExtras = Objects.requireNonNull(extras);
+        mResultCode = resultCode;
+        mErrorMessage = errorMessage;
+    }
+
+    /**
+     * Returns result codes from throwable.
+     *
+     * @hide
+     */
+    static @ResultCode int getResultCode(@NonNull Throwable t) {
+        if (t instanceof IllegalArgumentException) {
+            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
+        }
+        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+    }
+
+    /**
+     * Returns a successful response.
+     *
+     * @param resultDocument The return value of the executed function.
+     * @param extras The additional metadata data relevant to this function execution response.
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse newSuccess(
+            @NonNull GenericDocument resultDocument, @Nullable Bundle extras) {
+        Objects.requireNonNull(resultDocument);
+        Bundle actualExtras = getActualExtras(extras);
+
+        return new ExecuteAppFunctionResponse(
+                resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null);
+    }
+
+    /**
+     * Returns a failure response.
+     *
+     * @param resultCode The result code of the app function execution.
+     * @param extras The additional metadata data relevant to this function execution response.
+     * @param errorMessage The error message associated with the result, if any.
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse newFailure(
+            @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) {
+        if (resultCode == RESULT_OK) {
+            throw new IllegalArgumentException("resultCode must not be RESULT_OK");
+        }
+        Bundle actualExtras = getActualExtras(extras);
+        GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build();
+        return new ExecuteAppFunctionResponse(
+                emptyDocument, actualExtras, resultCode, errorMessage);
+    }
+
+    private static Bundle getActualExtras(@Nullable Bundle extras) {
+        if (extras == null) {
+            return Bundle.EMPTY;
+        }
+        return extras;
+    }
+
+    /**
+     * Returns a generic document containing the return value of the executed function.
+     *
+     * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.
+     *
+     * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed
+     * function does not produce a return value.
+     *
+     * <p>Sample code for extracting the return value:
+     *
+     * <pre>
+     *     GenericDocument resultDocument = response.getResultDocument();
+     *     Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE);
+     *     if (returnValue != null) {
+     *       // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString},
+     *       // {@link GenericDocument#getPropertyLong} etc.
+     *       // Do something with the returnValue
+     *     }
+     * </pre>
+     */
+    @NonNull
+    public GenericDocument getResultDocument() {
+        return mResultDocument;
+    }
+
+    /** Returns the extras of the app function execution. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Returns {@code true} if {@link #getResultCode} equals {@link
+     * ExecuteAppFunctionResponse#RESULT_OK}.
+     */
+    public boolean isSuccess() {
+        return getResultCode() == RESULT_OK;
+    }
+
+    /**
+     * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /**
+     * Returns the error message associated with this result.
+     *
+     * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}.
+     */
+    @Nullable
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Result codes.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"RESULT_"},
+            value = {
+                    RESULT_OK,
+                    RESULT_DENIED,
+                    RESULT_APP_UNKNOWN_ERROR,
+                    RESULT_INTERNAL_ERROR,
+                    RESULT_INVALID_ARGUMENT,
+                    RESULT_TIMED_OUT,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultCode {}
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
new file mode 100644
index 0000000..b1b05f7
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 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 com.google.android.appfunctions.sidecar;
+
+import android.annotation.NonNull;
+
+/**
+ * Utility class containing methods to convert Sidecar objects of AppFunctions API into the
+ * underlying platform classes.
+ *
+ * @hide
+ */
+public final class SidecarConverter {
+    private SidecarConverter() {}
+
+    /**
+     * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
+     * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.ExecuteAppFunctionRequest
+            getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) {
+        return new
+                android.app.appfunctions.ExecuteAppFunctionRequest.Builder(
+                request.getTargetPackageName(),
+                request.getFunctionIdentifier())
+                .setExtras(request.getExtras())
+                .setParameters(request.getParameters())
+                .build();
+    }
+
+    /**
+     * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
+     * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.ExecuteAppFunctionResponse
+            getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) {
+        if (response.isSuccess()) {
+            return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess(
+                    response.getResultDocument(), response.getExtras());
+        } else {
+            return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure(
+                    response.getResultCode(),
+                    response.getErrorMessage(),
+                    response.getExtras());
+        }
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
+     * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
+     *
+     * @hide
+     */
+    @NonNull
+    public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest(
+            @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) {
+        return new ExecuteAppFunctionRequest.Builder(
+                request.getTargetPackageName(),
+                request.getFunctionIdentifier())
+                .setExtras(request.getExtras())
+                .setParameters(request.getParameters())
+                .build();
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
+     * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
+     *
+     * @hide
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse(
+            @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) {
+        if (response.isSuccess()) {
+            return ExecuteAppFunctionResponse.newSuccess(
+                    response.getResultDocument(), response.getExtras());
+        } else {
+            return ExecuteAppFunctionResponse.newFailure(
+                    response.getResultCode(),
+                    response.getErrorMessage(),
+                    response.getExtras());
+        }
+    }
+}