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

Commit 206d1f88 authored by Aldi Fahrezi's avatar Aldi Fahrezi
Browse files

Add AppFunctionService.

- Ported from implementation in AppSearch.
- Simplified not using wrapper on top of ExecutionAppResponse.

Bug: 359983784
Flag: android.app.appfunctions.flags.enable_app_function_manager
API-Coverage-Bug: 357551503
Test: none (deferred with adding the CTS for executeAppFunction)
Change-Id: Ic8b53585269be1be7738a654511dcb25211074fb
parent db13dfe6
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);
        }
    }
}