Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe1d8004 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add AppFunctionService." into main

parents 856e94e0 206d1f88
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -8724,6 +8724,13 @@ package android.app.appfunctions {
  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
  }
  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") 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 android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
    field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
  }
  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class ExecuteAppFunctionRequest implements android.os.Parcelable {
    method public int describeContents();
    method @NonNull public android.os.Bundle getExtras();
+126 −0
Original line number Diff line number Diff line
/*
 * 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 android.app.appfunctions;

import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;

import android.annotation.FlaggedApi;
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>
 *
 * @see AppFunctionManager
 */
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
public abstract class AppFunctionService extends 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 =
            new IAppFunctionService.Stub() {
                @Override
                public void executeAppFunction(
                        @NonNull ExecuteAppFunctionRequest request,
                        @NonNull IExecuteAppFunctionCallback callback) {
                    if (AppFunctionService.this.checkCallingPermission(
                            BIND_APP_FUNCTION_SERVICE) == PERMISSION_DENIED) {
                        throw new SecurityException("Can only be called by the system server.");
                    }
                    SafeOneTimeExecuteAppFunctionCallback safeCallback =
                            new SafeOneTimeExecuteAppFunctionCallback(callback);
                    try {
                        AppFunctionService.this.onExecuteFunction(
                                request,
                                safeCallback::onResult);
                    } catch (Exception ex) {
                        // Apps should handle exceptions. But if they don't, report the error on
                        // behalf of them.
                        safeCallback.onResult(
                                new ExecuteAppFunctionResponse.Builder(
                                        getResultCode(ex), ex.getMessage()).build());
                    }
                }
            };

    private static int getResultCode(@NonNull Throwable t) {
        if (t instanceof IllegalArgumentException) {
            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
        }
        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
    }

    @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);
}
+36 −0
Original line number Diff line number Diff line
/*
 * 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 android.app.appfunctions;

import android.os.Bundle;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appfunctions.ExecuteAppFunctionRequest;


 /** {@hide} */
oneway interface IAppFunctionService {
    /**
     * Called by the system to execute a specific app function.
     *
     * @param request  the function execution request.
     * @param callback a callback to report back the result.
     */
    void executeAppFunction(
        in ExecuteAppFunctionRequest request,
        in IExecuteAppFunctionCallback callback
    );
}
+24 −0
Original line number Diff line number Diff line
/**
 * Copyright 2020, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app.appfunctions;

import android.app.appfunctions.ExecuteAppFunctionResponse;

/** {@hide} */
oneway interface IExecuteAppFunctionCallback {
    void onResult(in ExecuteAppFunctionResponse result);
}
+75 −0
Original line number Diff line number Diff line
/*
 * 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 android.app.appfunctions;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.util.Log;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

/**
 * A wrapper of IExecuteAppFunctionCallback which swallows the {@link RemoteException}. This
 * callback is intended for one-time use only. Subsequent calls to onResult() will be ignored.
 *
 * @hide
 */
public class SafeOneTimeExecuteAppFunctionCallback {
    private static final String TAG = "SafeOneTimeExecuteApp";

    private final AtomicBoolean mOnResultCalled = new AtomicBoolean(false);

    @NonNull private final IExecuteAppFunctionCallback mCallback;

    @Nullable private final Consumer<ExecuteAppFunctionResponse> mOnDispatchCallback;

    public SafeOneTimeExecuteAppFunctionCallback(@NonNull IExecuteAppFunctionCallback callback) {
        this(callback, /* onDispatchCallback= */ null);
    }

    /**
     * @param callback The callback to wrap.
     * @param onDispatchCallback An optional callback invoked after the wrapped callback has been
     *     dispatched with a result. This callback receives the result that has been dispatched.
     */
    public SafeOneTimeExecuteAppFunctionCallback(
            @NonNull IExecuteAppFunctionCallback callback,
            @Nullable Consumer<ExecuteAppFunctionResponse> onDispatchCallback) {
        mCallback = Objects.requireNonNull(callback);
        mOnDispatchCallback = onDispatchCallback;
    }

    /** Invoke wrapped callback with the result. */
    public void onResult(@NonNull ExecuteAppFunctionResponse result) {
        if (!mOnResultCalled.compareAndSet(false, true)) {
            Log.w(TAG, "Ignore subsequent calls to onResult()");
            return;
        }
        try {
            mCallback.onResult(result);
        } catch (RemoteException ex) {
            // Failed to notify the other end. Ignore.
            Log.w(TAG, "Failed to invoke the callback", ex);
        }
        if (mOnDispatchCallback != null) {
            mOnDispatchCallback.accept(result);
        }
    }
}