ServiceConnectionManager.java

/*
 * Copyright 2021 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.activity;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;

import static java.util.Objects.requireNonNull;

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.car.app.HandshakeInfo;
import androidx.car.app.activity.renderer.ICarAppActivity;
import androidx.car.app.activity.renderer.IRendererService;
import androidx.car.app.versioning.CarAppApiLevels;

import java.util.List;

/**
 * Manages the renderer service connection state.
 *
 * This class handles binding and unbinding to the renderer service and make sure the renderer
 * service gets initialized and terminated properly.
 *
 * @hide
 */
@RestrictTo(LIBRARY)
public class ServiceConnectionManager {
    @SuppressLint({"ActionValue"})
    @VisibleForTesting
    static final String ACTION_RENDER = "android.car.template.host.RendererService";

    final ServiceConnectionListener mListener;
    private final ComponentName mServiceComponentName;
    private final Context mContext;
    private final ServiceDispatcher mServiceDispatcher;
    private int mDisplayId;
    @Nullable
    private Intent mIntent;
    @Nullable
    private ICarAppActivity mICarAppActivity;
    @Nullable
    private HandshakeInfo mHandshakeInfo;

    @Nullable
    IRendererService mRendererService;

    /** A listener receive connection status updates */
    public interface ServiceConnectionListener extends ErrorHandler {
        /** Callback invoked when the connection to the host is established */
        void onConnect();
    }

    public ServiceConnectionManager(@NonNull Context context,
            @NonNull ComponentName serviceComponentName,
            @NonNull ServiceConnectionListener listener) {
        mContext = context;
        mListener = listener;
        mServiceComponentName = serviceComponentName;
        mServiceDispatcher = new ServiceDispatcher(listener, this::isBound);
    }

    /**
     * Returns a {@link ServiceDispatcher} that can be used to communicate with the renderer
     * service.
     */
    @NonNull
    ServiceDispatcher getServiceDispatcher() {
        return mServiceDispatcher;
    }

    @VisibleForTesting
    ComponentName getServiceComponentName() {
        return mServiceComponentName;
    }

    @VisibleForTesting
    ServiceConnection getServiceConnection() {
        return mServiceConnectionImpl;
    }

    @VisibleForTesting
    void setServiceConnection(ServiceConnection serviceConnection) {
        mServiceConnectionImpl = serviceConnection;
    }

    @VisibleForTesting
    void setRendererService(@Nullable IRendererService rendererService) {
        mRendererService = rendererService;
    }

    /**
     * Returns the {@link HandshakeInfo} that has been agreed with the host.
     */
    @Nullable
    public HandshakeInfo getHandshakeInfo() {
        return mHandshakeInfo;
    }

    /** Returns true if the service is currently bound and able to receive messages */
    boolean isBound() {
        return mRendererService != null;
    }

    /** The service connection for the renderer service. */
    private ServiceConnection mServiceConnectionImpl =
            new ServiceConnection() {
                @Override
                public void onServiceConnected(
                        @NonNull ComponentName name, @NonNull IBinder service) {
                    requireNonNull(name);
                    requireNonNull(service);
                    Log.i(LogTags.TAG, String.format("Host service %s is connected",
                            name.flattenToShortString()));
                    IRendererService rendererService = IRendererService.Stub.asInterface(service);
                    if (rendererService == null) {
                        Log.w(LogTags.TAG, "Failed to get IRenderService binder from host: "
                                + name);
                        mListener.onError(ErrorHandler.ErrorType.HOST_INCOMPATIBLE);
                        return;
                    }

                    mRendererService = rendererService;
                    initializeService();
                }

                @Override
                public void onServiceDisconnected(@NonNull ComponentName name) {
                    requireNonNull(name);

                    // Connection lost, but it might reconnect.
                    Log.w(LogTags.TAG, String.format("Host service %s is disconnected",
                            name.flattenToShortString()));
                }

                @Override
                public void onBindingDied(@NonNull ComponentName name) {
                    requireNonNull(name);

                    // Connection permanently lost
                    Log.i(LogTags.TAG, "Host service " + name + " is permanently disconnected");
                    mListener.onError(ErrorHandler.ErrorType.HOST_CONNECTION_LOST);
                }

                @Override
                public void onNullBinding(@NonNull ComponentName name) {
                    requireNonNull(name);

                    // Host rejected the binding.
                    Log.i(LogTags.TAG, "Host service " + name + " rejected the binding "
                            + "request");
                    mListener.onError(ErrorHandler.ErrorType.HOST_INCOMPATIBLE);
                }
            };

    /**
     * Binds to the renderer service and initializes the service if not bound already.
     *
     * Initializes the renderer service with given properties if already bound to the renderer
     * service.
     */
    void bind(@NonNull Intent intent, @NonNull ICarAppActivity iCarAppActivity, int displayId) {
        mIntent = requireNonNull(intent);
        mICarAppActivity = requireNonNull(iCarAppActivity);
        mDisplayId = displayId;

        if (isBound()) {
            initializeService();
            return;
        }

        Intent rendererIntent = new Intent(ACTION_RENDER);
        List<ResolveInfo> resolveInfoList =
                mContext.getPackageManager()
                        .queryIntentServices(rendererIntent, PackageManager.GET_META_DATA);
        if (resolveInfoList.size() == 1) {
            String packageName = resolveInfoList.get(0).serviceInfo.packageName;
            Log.d(LogTags.TAG, "Initiating binding to: " + packageName);
            rendererIntent.setPackage(packageName);
            if (!mContext.bindService(
                    rendererIntent,
                    mServiceConnectionImpl,
                    Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) {
                Log.e(LogTags.TAG, "Cannot bind to the renderer host with intent: "
                        + rendererIntent);
                mListener.onError(ErrorHandler.ErrorType.HOST_INCOMPATIBLE);
            }
        } else if (resolveInfoList.isEmpty()) {
            Log.e(LogTags.TAG, "No handlers found for intent: " + rendererIntent);
            mListener.onError(ErrorHandler.ErrorType.HOST_NOT_FOUND);
        } else {
            StringBuilder logMessage =
                    new StringBuilder("Multiple hosts found, only one is allowed");
            for (ResolveInfo resolveInfo : resolveInfoList) {
                logMessage.append(
                        String.format("\nFound host %s", resolveInfo.serviceInfo.packageName));
            }
            Log.e(LogTags.TAG, logMessage.toString());
            mListener.onError(ErrorHandler.ErrorType.MULTIPLE_HOSTS);
        }
    }

    /** Closes the connection to the connected {@code rendererService} if any. */
    void unbind() {
        if (mRendererService == null) {
            return;
        }
        try {
            mRendererService.terminate(requireNonNull(mServiceComponentName));
        } catch (RemoteException e) {
            // We are already unbinding (maybe because the host has already cut the connection)
            // Let's not log more errors unnecessarily.
        }

        Log.i(LogTags.TAG, "Unbinding from " + mServiceComponentName);
        mContext.unbindService(mServiceConnectionImpl);
        mRendererService = null;
    }

    /**
     * Initializes the {@code rendererService} for the current {@code carIAppActivity},
     * {@code serviceComponentName} and {@code displayId}.
     */
    void initializeService() {
        ICarAppActivity carAppActivity = requireNonNull(mICarAppActivity);
        IRendererService rendererService = requireNonNull(mRendererService);
        ComponentName serviceComponentName = requireNonNull(mServiceComponentName);

        // If the host does not support the getHandshakeInfo API, return oldest as it means to
        // communicate at minimum level.
        mHandshakeInfo = mServiceDispatcher.fetchNoFail("performHandshake",
                new HandshakeInfo("", CarAppApiLevels.getOldest()),
                () -> (HandshakeInfo) rendererService.performHandshake(serviceComponentName,
                        CarAppApiLevels.getLatest()).get());

        Boolean success = mServiceDispatcher.fetch("initialize", false,
                () -> rendererService.initialize(carAppActivity,
                        serviceComponentName, mDisplayId));
        if (success == null || !success) {
            Log.e(LogTags.TAG, "Cannot create renderer for " + serviceComponentName);
            mListener.onError(ErrorHandler.ErrorType.HOST_ERROR);
            return;
        }

        if (!updateIntent()) {
            return;
        }

        mListener.onConnect();
    }

    /**
     * Updates the activity intent for the connected {@code rendererService}.
     *
     * @return true if intent update was successful
     */
    private boolean updateIntent() {
        ComponentName serviceComponentName = requireNonNull(mServiceComponentName);
        Intent intent = requireNonNull(mIntent);

        IRendererService service = mRendererService;
        if (service == null) {
            Log.e(LogTags.TAG, "Service dispatcher is not connected");
            mListener.onError(ErrorHandler.ErrorType.CLIENT_SIDE_ERROR);
            return false;
        }

        Boolean success = mServiceDispatcher.fetch("onNewIntent", false, () ->
                service.onNewIntent(intent, serviceComponentName, mDisplayId));
        if (success == null || !success) {
            Log.e(LogTags.TAG, "Renderer cannot handle the intent: " + intent);
            mListener.onError(ErrorHandler.ErrorType.HOST_ERROR);
            return false;
        }
        return true;
    }
}