From c0c499e18c84cdc04136201782fb455c53a753a2 Mon Sep 17 00:00:00 2001 From: Oluwarotimi Adesina Date: Wed, 6 Nov 2024 18:14:54 +0000 Subject: remame appfunction sidecar jar Flag: android.app.appfunctions.flags.enable_app_function_manager Test: cts Bug: 357551503 Change-Id: I19b5d9a0856429ad9b072aaa3f9b8e5a0dfde063 --- .../appfunctions/AppFunctionManager.java | 186 +++++++++++ .../appfunctions/AppFunctionService.java | 126 ++++++++ .../appfunctions/ExecuteAppFunctionRequest.java | 150 +++++++++ .../appfunctions/ExecuteAppFunctionResponse.java | 359 +++++++++++++++++++++ .../extensions/appfunctions/SidecarConverter.java | 97 ++++++ .../appfunctions/sidecar/AppFunctionManager.java | 186 ----------- .../appfunctions/sidecar/AppFunctionService.java | 127 -------- .../sidecar/ExecuteAppFunctionRequest.java | 150 --------- .../sidecar/ExecuteAppFunctionResponse.java | 359 --------------------- .../appfunctions/sidecar/SidecarConverter.java | 104 ------ 10 files changed, 918 insertions(+), 926 deletions(-) create mode 100644 libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java create mode 100644 libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java create mode 100644 libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java create mode 100644 libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java create mode 100644 libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java delete mode 100644 libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java delete mode 100644 libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java delete mode 100644 libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java delete mode 100644 libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java delete mode 100644 libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java (limited to 'libs/appfunctions/java') diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java new file mode 100644 index 000000000000..d64593d8ff5f --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java @@ -0,0 +1,186 @@ +/* + * 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.android.extensions.appfunctions; + +import android.Manifest; +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; +import android.annotation.UserHandleAware; +import android.content.Context; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Provides app functions related functionalities. + * + *

App function is a specific piece of functionality that an app offers to the system. These + * functionalities can be integrated into various system features. + * + *

This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and + * exposes it here as a sidecar library (avoiding direct dependency on the platform API). + */ +public final class AppFunctionManager { + /** + * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset + * enabled state to the default value. + */ + public static final int APP_FUNCTION_STATE_DEFAULT = 0; + + /** + * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_ENABLED = 1; + + /** + * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_DISABLED = 2; + + /** + * The enabled state of the app function. + * + * @hide + */ + @IntDef( + prefix = {"APP_FUNCTION_STATE_"}, + value = { + APP_FUNCTION_STATE_DEFAULT, + APP_FUNCTION_STATE_ENABLED, + APP_FUNCTION_STATE_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EnabledState {} + + 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. + * + *

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. + * + *

See {@link android.app.appfunctions.AppFunctionManager#executeAppFunction} for the + * documented behaviour of this method. + */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest sidecarRequest, + @NonNull @CallbackExecutor Executor executor, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer callback) { + Objects.requireNonNull(sidecarRequest); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = + SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); + mManager.executeAppFunction( + platformRequest, + executor, + cancellationSignal, + (platformResponse) -> { + callback.accept( + SidecarConverter.getSidecarExecuteAppFunctionResponse( + platformResponse)); + }); + } + + /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + *

See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the + * documented behaviour of this method. + */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + mManager.isAppFunctionEnabled(functionIdentifier, targetPackage, executor, callback); + } + + /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + *

See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the + * documented behaviour of this method. + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + mManager.isAppFunctionEnabled(functionIdentifier, executor, callback); + } + + /** + * Sets the enabled state of the app function owned by the calling package. + * + *

See {@link android.app.appfunctions.AppFunctionManager#setAppFunctionEnabled} for the + * documented behavoir of this method. + */ + // Constants in @EnabledState should always mirror those in + // android.app.appfunctions.AppFunctionManager. + @SuppressLint("WrongConstant") + @UserHandleAware + public void setAppFunctionEnabled( + @NonNull String functionIdentifier, + @EnabledState int newEnabledState, + @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + mManager.setAppFunctionEnabled(functionIdentifier, newEnabledState, executor, callback); + } +} diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java new file mode 100644 index 000000000000..1a4d9da8bd63 --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -0,0 +1,126 @@ +/* + * 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.android.extensions.appfunctions; + +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.CancellationSignal; +import android.os.IBinder; + +import java.util.function.Consumer; + +/** + * Abstract base class to provide app functions to the system. + * + *

Include the following in the manifest: + * + *

+ * {@literal
+ * 
+ *    
+ *      
+ *    
+ * 
+ * }
+ * 
+ * + *

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, + callingPackage, + cancellationSignal, + callback) -> { + AppFunctionService.this.onExecuteFunction( + SidecarConverter.getSidecarExecuteAppFunctionRequest( + platformRequest), + callingPackage, + cancellationSignal, + (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. + * + *

This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + *

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()}. + * + *

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. + * + *

This method also accepts a {@link CancellationSignal} that the app should listen to cancel + * the execution of function if requested by the system. + * + * @param request The function execution request. + * @param callingPackage The package name of the app that is requesting the execution. + * @param cancellationSignal A signal to cancel the execution. + * @param callback A callback to report back the result. + */ + @MainThread + public abstract void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull String callingPackage, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer callback); +} diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java new file mode 100644 index 000000000000..baddc245f0f1 --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java @@ -0,0 +1,150 @@ +/* + * 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.android.extensions.appfunctions; + +import android.annotation.NonNull; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.util.Objects; + +/** + * A request to execute an app function. + * + *

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; + + /** + * The unique string identifier of the app function to be executed. This identifier is used to + * execute a specific app function. + */ + @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. + * + *

The document may have missing parameters. Developers are advised to implement defensive + * handling measures. + */ + @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. + * + *

When there is a package change or the device starts up, the metadata of available + * functions is indexed by AppSearch. AppSearch stores the indexed information as {@code + * AppFunctionStaticMetadata} document. + * + *

The ID can be obtained by querying the {@code AppFunctionStaticMetadata} documents from + * AppSearch. + * + *

If the {@code functionId} provided is invalid, the caller will get an invalid argument + * response. + */ + @NonNull + public String getFunctionIdentifier() { + return mFunctionIdentifier; + } + + /** + * Returns the function parameters. The key is the parameter name, and the value is the + * parameter value. + * + *

The {@link GenericDocument} may have missing parameters. Developers are advised to + * implement defensive handling measures. + * + *

Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be + * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This + * metadata will contain enough information for the caller to resolve the required parameters + * either using information from the metadata itself or using the AppFunction SDK for function + * callers. + */ + @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/android/extensions/appfunctions/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java new file mode 100644 index 000000000000..7c5ddcd9edfb --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java @@ -0,0 +1,359 @@ +/* + * 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.android.extensions.appfunctions; + +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. + * + *

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}. + * + *

See {@link GenericDocument#getProperty(String)} for more information. + * + *

If the function returns {@code void} or throws an error, the {@code resultDocument} will + * be empty {@link GenericDocument}. + * + *

If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will + * return {@code null}. + * + *

See {@link #getResultDocument} for more information on extracting the return value. + */ + public static final String PROPERTY_RETURN_VALUE = "android_app_appfunctions_returnvalue"; + + /** + * The call was successful. + * + *

This result code does not belong in an error category. + */ + public static final int RESULT_OK = 0; + + /** + * The caller does not have the permission to execute an app function. + * + *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DENIED = 1000; + + /** + * The caller supplied invalid arguments to the execution request. + * + *

This error may be considered similar to {@link IllegalArgumentException}. + * + *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_INVALID_ARGUMENT = 1001; + + /** + * The caller tried to execute a disabled app function. + * + *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DISABLED = 1002; + + /** + * The caller tried to execute a function that does not exist. + * + *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_FUNCTION_NOT_FOUND = 1003; + + /** + * An internal unexpected error coming from the system. + * + *

This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_SYSTEM_ERROR = 2000; + + /** + * The operation was cancelled. Use this error code to report that a cancellation is done after + * receiving a cancellation signal. + * + *

This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_CANCELLED = 2001; + + /** + * An unknown error occurred while processing the call in the AppFunctionService. + * + *

This error is thrown when the service is connected in the remote application but an + * unexpected error is thrown from the bound application. + * + *

This error is in the {@link #ERROR_CATEGORY_APP} category. + */ + public static final int RESULT_APP_UNKNOWN_ERROR = 3000; + + /** + * The error category is unknown. + * + *

This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; + + /** + * The error is caused by the app requesting a function execution. + * + *

For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + *

Errors in the category fall in the range 1000-1999 inclusive. + */ + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; + + /** + * The error is caused by an issue in the system. + * + *

For example, the AppFunctionService implementation is not found by the system. + * + *

Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; + + /** + * The error is caused by the app providing the function. + * + *

For example, the app crashed when the system is executing the request. + * + *

Errors in the category fall in the range 3000-3999 inclusive. + */ + public static final int ERROR_CATEGORY_APP = 3; + + /** 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. + * + *

The return value is stored in a {@link GenericDocument} with the key {@link + * #PROPERTY_RETURN_VALUE}. + * + *

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 the error category of the {@link ExecuteAppFunctionResponse}. + * + *

This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of result codes to specific error categories. + * + *

When constructing a {@link #newFailure} response, use the appropriate result code value to + * ensure correct categorization of the failed response. + * + *

This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to + * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. + * + *

See {@link ErrorCategory} for a complete list of error categories and their corresponding + * result code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mResultCode >= 1000 && mResultCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mResultCode >= 2000 && mResultCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mResultCode >= 3000 && mResultCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + /** + * Returns a generic document containing the return value of the executed function. + * + *

The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. + * + *

An empty document is returned if {@link #isSuccess} is {@code false} or if the executed + * function does not produce a return value. + * + *

Sample code for extracting the return value: + * + *

+     *     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
+     *     }
+     * 
+ */ + @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. + * + *

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_SYSTEM_ERROR, + RESULT_FUNCTION_NOT_FOUND, + RESULT_INVALID_ARGUMENT, + RESULT_DISABLED, + RESULT_CANCELLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} +} diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java new file mode 100644 index 000000000000..56f2725fccc7 --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java @@ -0,0 +1,97 @@ +/* + * 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.android.extensions.appfunctions; + +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 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 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 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 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()); + } + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java deleted file mode 100644 index 2075104ff868..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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.Manifest; -import android.annotation.CallbackExecutor; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.RequiresPermission; -import android.annotation.SuppressLint; -import android.annotation.UserHandleAware; -import android.content.Context; -import android.os.CancellationSignal; -import android.os.OutcomeReceiver; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Objects; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -/** - * Provides app functions related functionalities. - * - *

App function is a specific piece of functionality that an app offers to the system. These - * functionalities can be integrated into various system features. - * - *

This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and - * exposes it here as a sidecar library (avoiding direct dependency on the platform API). - */ -public final class AppFunctionManager { - /** - * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset - * enabled state to the default value. - */ - public static final int APP_FUNCTION_STATE_DEFAULT = 0; - - /** - * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled} - * with this value. - */ - public static final int APP_FUNCTION_STATE_ENABLED = 1; - - /** - * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled} - * with this value. - */ - public static final int APP_FUNCTION_STATE_DISABLED = 2; - - /** - * The enabled state of the app function. - * - * @hide - */ - @IntDef( - prefix = {"APP_FUNCTION_STATE_"}, - value = { - APP_FUNCTION_STATE_DEFAULT, - APP_FUNCTION_STATE_ENABLED, - APP_FUNCTION_STATE_DISABLED - }) - @Retention(RetentionPolicy.SOURCE) - public @interface EnabledState {} - - 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. - * - *

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. - * - *

See {@link android.app.appfunctions.AppFunctionManager#executeAppFunction} for the - * documented behaviour of this method. - */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) - public void executeAppFunction( - @NonNull ExecuteAppFunctionRequest sidecarRequest, - @NonNull @CallbackExecutor Executor executor, - @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer callback) { - Objects.requireNonNull(sidecarRequest); - Objects.requireNonNull(executor); - Objects.requireNonNull(callback); - - android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = - SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); - mManager.executeAppFunction( - platformRequest, - executor, - cancellationSignal, - (platformResponse) -> { - callback.accept( - SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse)); - }); - } - - /** - * Returns a boolean through a callback, indicating whether the app function is enabled. - * - *

See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the - * documented behaviour of this method. - */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) - public void isAppFunctionEnabled( - @NonNull String functionIdentifier, - @NonNull String targetPackage, - @NonNull Executor executor, - @NonNull OutcomeReceiver callback) { - mManager.isAppFunctionEnabled(functionIdentifier, targetPackage, executor, callback); - } - - /** - * Returns a boolean through a callback, indicating whether the app function is enabled. - * - *

See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the - * documented behaviour of this method. - */ - public void isAppFunctionEnabled( - @NonNull String functionIdentifier, - @NonNull Executor executor, - @NonNull OutcomeReceiver callback) { - mManager.isAppFunctionEnabled(functionIdentifier, executor, callback); - } - - /** - * Sets the enabled state of the app function owned by the calling package. - * - *

See {@link android.app.appfunctions.AppFunctionManager#setAppFunctionEnabled} for the - * documented behavoir of this method. - */ - // Constants in @EnabledState should always mirror those in - // android.app.appfunctions.AppFunctionManager. - @SuppressLint("WrongConstant") - @UserHandleAware - public void setAppFunctionEnabled( - @NonNull String functionIdentifier, - @EnabledState int newEnabledState, - @NonNull Executor executor, - @NonNull OutcomeReceiver callback) { - mManager.setAppFunctionEnabled(functionIdentifier, newEnabledState, executor, callback); - } -} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java deleted file mode 100644 index 0dc87e45b7e3..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.CancellationSignal; -import android.os.IBinder; -import android.util.Log; - -import java.util.function.Consumer; - -/** - * Abstract base class to provide app functions to the system. - * - *

Include the following in the manifest: - * - *

- * {@literal
- * 
- *    
- *      
- *    
- * 
- * }
- * 
- * - *

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, - callingPackage, - cancellationSignal, - callback) -> { - AppFunctionService.this.onExecuteFunction( - SidecarConverter.getSidecarExecuteAppFunctionRequest( - platformRequest), - callingPackage, - cancellationSignal, - (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. - * - *

This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - *

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()}. - * - *

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. - * - *

This method also accepts a {@link CancellationSignal} that the app should listen to cancel - * the execution of function if requested by the system. - * - * @param request The function execution request. - * @param callingPackage The package name of the app that is requesting the execution. - * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result. - */ - @MainThread - public abstract void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull String callingPackage, - @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer 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 deleted file mode 100644 index 9cf4c6a9b3c5..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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. - * - *

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; - - /** - * The unique string identifier of the app function to be executed. This identifier is used to - * execute a specific app function. - */ - @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. - * - *

The document may have missing parameters. Developers are advised to implement defensive - * handling measures. - */ - @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. - * - *

When there is a package change or the device starts up, the metadata of available - * functions is indexed by AppSearch. AppSearch stores the indexed information as {@code - * AppFunctionStaticMetadata} document. - * - *

The ID can be obtained by querying the {@code AppFunctionStaticMetadata} documents from - * AppSearch. - * - *

If the {@code functionId} provided is invalid, the caller will get an invalid argument - * response. - */ - @NonNull - public String getFunctionIdentifier() { - return mFunctionIdentifier; - } - - /** - * Returns the function parameters. The key is the parameter name, and the value is the - * parameter value. - * - *

The {@link GenericDocument} may have missing parameters. Developers are advised to - * implement defensive handling measures. - * - *

Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be - * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This - * metadata will contain enough information for the caller to resolve the required parameters - * either using information from the metadata itself or using the AppFunction SDK for function - * callers. - */ - @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 deleted file mode 100644 index c806a0780551..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * 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. - * - *

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}. - * - *

See {@link GenericDocument#getProperty(String)} for more information. - * - *

If the function returns {@code void} or throws an error, the {@code resultDocument} will - * be empty {@link GenericDocument}. - * - *

If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will - * return {@code null}. - * - *

See {@link #getResultDocument} for more information on extracting the return value. - */ - public static final String PROPERTY_RETURN_VALUE = "android_app_appfunctions_returnvalue"; - - /** - * The call was successful. - * - *

This result code does not belong in an error category. - */ - public static final int RESULT_OK = 0; - - /** - * The caller does not have the permission to execute an app function. - * - *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DENIED = 1000; - - /** - * The caller supplied invalid arguments to the execution request. - * - *

This error may be considered similar to {@link IllegalArgumentException}. - * - *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_INVALID_ARGUMENT = 1001; - - /** - * The caller tried to execute a disabled app function. - * - *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DISABLED = 1002; - - /** - * The caller tried to execute a function that does not exist. - * - *

This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_FUNCTION_NOT_FOUND = 1003; - - /** - * An internal unexpected error coming from the system. - * - *

This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_SYSTEM_ERROR = 2000; - - /** - * The operation was cancelled. Use this error code to report that a cancellation is done after - * receiving a cancellation signal. - * - *

This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_CANCELLED = 2001; - - /** - * An unknown error occurred while processing the call in the AppFunctionService. - * - *

This error is thrown when the service is connected in the remote application but an - * unexpected error is thrown from the bound application. - * - *

This error is in the {@link #ERROR_CATEGORY_APP} category. - */ - public static final int RESULT_APP_UNKNOWN_ERROR = 3000; - - /** - * The error category is unknown. - * - *

This is the default value for {@link #getErrorCategory}. - */ - public static final int ERROR_CATEGORY_UNKNOWN = 0; - - /** - * The error is caused by the app requesting a function execution. - * - *

For example, the caller provided invalid parameters in the execution request e.g. an - * invalid function ID. - * - *

Errors in the category fall in the range 1000-1999 inclusive. - */ - public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; - - /** - * The error is caused by an issue in the system. - * - *

For example, the AppFunctionService implementation is not found by the system. - * - *

Errors in the category fall in the range 2000-2999 inclusive. - */ - public static final int ERROR_CATEGORY_SYSTEM = 2; - - /** - * The error is caused by the app providing the function. - * - *

For example, the app crashed when the system is executing the request. - * - *

Errors in the category fall in the range 3000-3999 inclusive. - */ - public static final int ERROR_CATEGORY_APP = 3; - - /** 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. - * - *

The return value is stored in a {@link GenericDocument} with the key {@link - * #PROPERTY_RETURN_VALUE}. - * - *

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 the error category of the {@link ExecuteAppFunctionResponse}. - * - *

This method categorizes errors based on their underlying cause, allowing developers to - * implement targeted error handling and provide more informative error messages to users. It - * maps ranges of result codes to specific error categories. - * - *

When constructing a {@link #newFailure} response, use the appropriate result code value to - * ensure correct categorization of the failed response. - * - *

This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to - * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. - * - *

See {@link ErrorCategory} for a complete list of error categories and their corresponding - * result code ranges. - */ - @ErrorCategory - public int getErrorCategory() { - if (mResultCode >= 1000 && mResultCode < 2000) { - return ERROR_CATEGORY_REQUEST_ERROR; - } - if (mResultCode >= 2000 && mResultCode < 3000) { - return ERROR_CATEGORY_SYSTEM; - } - if (mResultCode >= 3000 && mResultCode < 4000) { - return ERROR_CATEGORY_APP; - } - return ERROR_CATEGORY_UNKNOWN; - } - - /** - * Returns a generic document containing the return value of the executed function. - * - *

The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. - * - *

An empty document is returned if {@link #isSuccess} is {@code false} or if the executed - * function does not produce a return value. - * - *

Sample code for extracting the return value: - * - *

-     *     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
-     *     }
-     * 
- */ - @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. - * - *

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_SYSTEM_ERROR, - RESULT_FUNCTION_NOT_FOUND, - RESULT_INVALID_ARGUMENT, - RESULT_DISABLED, - RESULT_CANCELLED - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ResultCode {} - - /** - * Error categories. - * - * @hide - */ - @IntDef( - prefix = {"ERROR_CATEGORY_"}, - value = { - ERROR_CATEGORY_UNKNOWN, - ERROR_CATEGORY_REQUEST_ERROR, - ERROR_CATEGORY_APP, - ERROR_CATEGORY_SYSTEM - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ErrorCategory {} -} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java deleted file mode 100644 index b1b05f79f33f..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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()); - } - } -} -- cgit v1.2.3-59-g8ed1b