RemoteUtils.java

/*
 * 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 androidx.car.app.utils;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.car.app.utils.CommonUtils.TAG;

import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.car.app.FailureResponse;
import androidx.car.app.HostException;
import androidx.car.app.IOnDoneCallback;
import androidx.car.app.ISurfaceCallback;
import androidx.car.app.OnDoneCallback;
import androidx.car.app.SurfaceCallback;
import androidx.car.app.SurfaceContainer;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;

/**
 * Assorted utilities to deal with serialization of remote calls.
 *
 * @hide
 */
@RestrictTo(LIBRARY)
public final class RemoteUtils {
    /** An interface that defines a remote call to be made. */
    public interface RemoteCall<ReturnT> {
        /** Performs the remote call. */
        @Nullable
        ReturnT call() throws RemoteException;
    }

    /**
     * A method that the host dispatched to be run on the main thread and notify the host of
     * success/failure.
     */
    public interface HostCall {
        void dispatch() throws BundlerException;
    }

    /**
     * Performs the remote call and handles exceptions thrown by the host.
     *
     * @throws SecurityException as a pass through from the host
     * @throws HostException     if the remote call fails with any other exception
     */
    @SuppressLint("LambdaLast")
    @Nullable
    public static <ReturnT> ReturnT call(@NonNull RemoteCall<ReturnT> remoteCall,
            @NonNull String callName) {
        try {
            Log.d(TAG, "Dispatching call " + callName + " to host");
            return remoteCall.call();
        } catch (SecurityException e) {
            // SecurityException is treated specially where we allow it to flow through since
            // this is specific to not having permissions to perform an API.
            throw e;
        } catch (RemoteException | RuntimeException e) {
            throw new HostException("Remote " + callName + " call failed", e);
        }
    }

    /**
     * Returns an {@link ISurfaceCallback} stub that invokes the input {@link SurfaceCallback}
     * if it is not {@code null}, or {@code null} if the input {@link SurfaceCallback} is {@code
     * null}
     */
    @Nullable
    public static ISurfaceCallback stubSurfaceCallback(@Nullable SurfaceCallback surfaceCallback) {
        if (surfaceCallback == null) {
            return null;
        }

        return new SurfaceCallbackStub(surfaceCallback);
    }

    /**
     * Dispatches the given {@link HostCall} to the client in the main thread, and notifies the host
     * of outcome.
     *
     * <p>If the app processes the response, will call {@link IOnDoneCallback#onSuccess} with a
     * {@code null}.
     *
     * <p>If the app throws an exception, will call {@link IOnDoneCallback#onFailure} with a {@link
     * FailureResponse} including information from the caught exception.
     */
    @SuppressLint("LambdaLast")
    public static void dispatchHostCall(
            @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback,
            @NonNull String callName) {
        ThreadUtils.runOnMain(
                () -> {
                    try {
                        hostCall.dispatch();
                    } catch (BundlerException e) {
                        sendFailureResponse(callback, callName, e);
                        throw new HostException("Serialization failure in " + callName, e);
                    } catch (RuntimeException e) {
                        sendFailureResponse(callback, callName, e);
                        throw new RuntimeException(e);
                    }
                    sendSuccessResponse(callback, callName, null);
                });
    }

    public static void sendSuccessResponse(
            @NonNull IOnDoneCallback callback, @NonNull String callName,
            @Nullable Object response) {
        call(() -> {
            try {
                callback.onSuccess(response == null ? null : Bundleable.create(response));
            } catch (BundlerException e) {
                sendFailureResponse(callback, callName, e);
                throw new IllegalStateException("Serialization failure in " + callName, e);
            }
            return null;
        }, callName + " onSuccess");
    }

    public static void sendFailureResponse(@NonNull IOnDoneCallback callback,
            @NonNull String callName,
            @NonNull Throwable e) {
        call(() -> {
            try {
                callback.onFailure(Bundleable.create(new FailureResponse(e)));
            } catch (BundlerException bundlerException) {
                // Not possible, but catching since BundlerException is not runtime.
                throw new IllegalStateException(
                        "Serialization failure in " + callName, bundlerException);
            }
            return null;
        }, callName + " onFailure");
    }

    /**
     * Provides a {@link IOnDoneCallback} that forwards success and failure callbacks to a
     * {@link OnDoneCallback}.
     */
    @NonNull
    public static IOnDoneCallback createOnDoneCallbackStub(@NonNull OnDoneCallback callback) {
        return new IOnDoneCallback.Stub() {
            @Override
            public void onSuccess(Bundleable response) {
                callback.onSuccess(response);
            }

            @Override
            public void onFailure(Bundleable failureResponse) {
                callback.onFailure(failureResponse);
            }
        };
    }

    private static class SurfaceCallbackStub extends ISurfaceCallback.Stub {
        private final SurfaceCallback mSurfaceCallback;

        SurfaceCallbackStub(SurfaceCallback surfaceCallback) {
            mSurfaceCallback = surfaceCallback;
        }

        @Override
        public void onSurfaceAvailable(Bundleable surfaceContainer, IOnDoneCallback callback) {
            dispatchHostCall(
                    () -> mSurfaceCallback.onSurfaceAvailable(
                            (SurfaceContainer) surfaceContainer.get()),
                    callback,
                    "onSurfaceAvailable");
        }

        @Override
        public void onVisibleAreaChanged(Rect visibleArea, IOnDoneCallback callback) {
            dispatchHostCall(
                    () -> mSurfaceCallback.onVisibleAreaChanged(visibleArea),
                    callback,
                    "onVisibleAreaChanged");
        }

        @Override
        public void onStableAreaChanged(Rect stableArea, IOnDoneCallback callback) {
            dispatchHostCall(
                    () -> mSurfaceCallback.onStableAreaChanged(stableArea), callback,
                    "onStableAreaChanged");
        }

        @Override
        public void onSurfaceDestroyed(Bundleable surfaceContainer, IOnDoneCallback callback) {
            dispatchHostCall(
                    () -> mSurfaceCallback.onSurfaceDestroyed(
                            (SurfaceContainer) surfaceContainer.get()),
                    callback,
                    "onSurfaceDestroyed");
        }
    }

    private RemoteUtils() {
    }
}