public abstract class

BaseCarAppActivity

extends FragmentActivity

 java.lang.Object

↳Activity

androidx.core.app.ComponentActivity

androidx.activity.ComponentActivity

androidx.fragment.app.FragmentActivity

↳androidx.car.app.activity.BaseCarAppActivity

Subclasses:

CarAppActivity

Gradle dependencies

compile group: 'androidx.car.app', name: 'app-automotive', version: '1.7.0-beta01'

  • groupId: androidx.car.app
  • artifactId: app-automotive
  • version: 1.7.0-beta01

Artifact androidx.car.app:app-automotive:1.7.0-beta01 it located at Google repository (https://maven.google.com/)

Overview

Core logic for CarAppLibrary Activity interaction with a host.

This base class handles binding and providing the UI surface of the real activity to the host, and responds to instruction by the host to display content. 3P must not change the layout; otherwise the host does not guarantee the right execution.

Summary

Constructors
publicBaseCarAppActivity()

Methods
public voidbindToViewModel(SessionInfo sessionInfo)

Binds the BaseCarAppActivity and it's view against the view model.

public booleangetDecorFitsSystemWindows()

TODO(b/283985939): Workaround for testing setDecorFitsSystemWindows for older versions of Roboelectric that don't support getDecorFitsSystemWindows.

public ComponentNamegetServiceComponentName()

Retrieves the to which the view model will talk in order to render.

public voidonBackPressed()

Called when the activity has detected the user's press of the back key.

protected voidonCreate(Bundle savedInstanceState)

Perform initialization of all fragments.

protected voidonDestroy()

Destroy all fragments.

protected voidonNewIntent(Intent intent)

Dispatches this call to all listeners added via ComponentActivity.addOnNewIntentListener(Consumer).

protected voidonPause()

Dispatch onPause() to fragments.

public ComponentNameretrieveServiceComponentName()

from FragmentActivitydump, getSupportFragmentManager, getSupportLoaderManager, onActivityResult, onAttachFragment, onCreateView, onCreateView, onMenuItemSelected, onPostResume, onRequestPermissionsResult, onResume, onResumeFragments, onStart, onStateNotSaved, onStop, setEnterSharedElementCallback, setExitSharedElementCallback, startActivityFromFragment, startActivityFromFragment, startIntentSenderFromFragment, supportFinishAfterTransition, supportInvalidateOptionsMenu, supportPostponeEnterTransition, supportStartPostponedEnterTransition, validateRequestPermissionsRequestCode
from ComponentActivityaddContentView, addMenuProvider, addMenuProvider, addMenuProvider, addOnConfigurationChangedListener, addOnContextAvailableListener, addOnMultiWindowModeChangedListener, addOnNewIntentListener, addOnPictureInPictureModeChangedListener, addOnTrimMemoryListener, getActivityResultRegistry, getDefaultViewModelCreationExtras, getDefaultViewModelProviderFactory, getLastCustomNonConfigurationInstance, getLifecycle, getOnBackPressedDispatcher, getSavedStateRegistry, getViewModelStore, invalidateMenu, onConfigurationChanged, onCreateOptionsMenu, onMultiWindowModeChanged, onMultiWindowModeChanged, onOptionsItemSelected, onPanelClosed, onPictureInPictureModeChanged, onPictureInPictureModeChanged, onPrepareOptionsMenu, onRetainCustomNonConfigurationInstance, onRetainNonConfigurationInstance, onSaveInstanceState, onTrimMemory, peekAvailableContext, registerForActivityResult, registerForActivityResult, removeMenuProvider, removeOnConfigurationChangedListener, removeOnContextAvailableListener, removeOnMultiWindowModeChangedListener, removeOnNewIntentListener, removeOnPictureInPictureModeChangedListener, removeOnTrimMemoryListener, reportFullyDrawn, setContentView, setContentView, setContentView, startActivityForResult, startActivityForResult, startIntentSenderForResult, startIntentSenderForResult
from ComponentActivitydispatchKeyEvent, dispatchKeyShortcutEvent, getExtraData, putExtraData, shouldDumpInternalState, superDispatchKeyEvent
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public BaseCarAppActivity()

Methods

protected void onCreate(Bundle savedInstanceState)

Perform initialization of all fragments.

public void bindToViewModel(SessionInfo sessionInfo)

Binds the BaseCarAppActivity and it's view against the view model.

public boolean getDecorFitsSystemWindows()

TODO(b/283985939): Workaround for testing setDecorFitsSystemWindows for older versions of Roboelectric that don't support getDecorFitsSystemWindows. Remove this once Roboelectric version is upgraded to v4.10.3.

protected void onPause()

Dispatch onPause() to fragments.

public void onBackPressed()

Called when the activity has detected the user's press of the back key. The OnBackPressedDispatcher will be given a chance to handle the back button before the default behavior of is invoked.

See also: ComponentActivity.getOnBackPressedDispatcher()

protected void onNewIntent(Intent intent)

Dispatches this call to all listeners added via ComponentActivity.addOnNewIntentListener(Consumer).

protected void onDestroy()

Destroy all fragments.

public ComponentName retrieveServiceComponentName()

See also: BaseCarAppActivity.getServiceComponentName()

public ComponentName getServiceComponentName()

Retrieves the to which the view model will talk in order to render.

Source

/*
 * 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 android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;

import static androidx.car.app.CarAppService.SERVICE_INTERFACE;

import static java.util.Objects.requireNonNull;

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.PixelCopy;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.ImageView;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.car.app.SessionInfo;
import androidx.car.app.activity.renderer.ICarAppActivity;
import androidx.car.app.activity.renderer.IInsetsListener;
import androidx.car.app.activity.renderer.IRendererCallback;
import androidx.car.app.activity.renderer.IRendererService;
import androidx.car.app.activity.renderer.surface.ISurfaceListener;
import androidx.car.app.activity.renderer.surface.OnBackPressedListener;
import androidx.car.app.activity.renderer.surface.SurfaceHolderListener;
import androidx.car.app.activity.renderer.surface.SurfaceWrapperProvider;
import androidx.car.app.activity.renderer.surface.TemplateSurfaceView;
import androidx.car.app.activity.ui.ErrorMessageView;
import androidx.car.app.activity.ui.LoadingView;
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.automotive.R;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.ThreadUtils;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModelProvider;

import java.util.List;

/**
 * Core logic for CarAppLibrary Activity interaction with a host.
 *
 * <p> This base class handles binding and providing the UI surface of the real activity to the
 * host, and responds to instruction by the host to display content. 3P must not change the layout;
 * otherwise the host does not guarantee the right execution.
 */
@SuppressLint({"ForbiddenSuperClass"})
public abstract class BaseCarAppActivity extends FragmentActivity {

    TemplateSurfaceView mSurfaceView;
    ErrorMessageView mErrorMessageView;
    LoadingView mLoadingView;
    View mActivityContainerView;
    View mLocalContentContainerView;

    boolean mDecorFitsSystemWindows = false;

    /**
     * Displays the snapshot of the surface view to avoid a visual glitch when app comes
     * to foreground. This view sits behind the surface view and will be visible only when surface
     * is hidden (or not created yet).
     */
    ImageView mSurfaceSnapshotView;

    // The handler used to take surface view snapshot.
    private final Handler mSnapshotHandler = new Handler(Looper.myLooper());

    @Nullable
    SurfaceHolderListener mSurfaceHolderListener;
    @Nullable
    ActivityLifecycleDelegate mActivityLifecycleDelegate;
    @Nullable
    CarAppViewModel mViewModel;
    @Nullable
    OnBackPressedListener mOnBackPressedListener;
    @Nullable
    HostUpdateReceiver mHostUpdateReceiver;

    /**
     * A listener to conditionally send insets to the host, or handle them locally if the host
     * is not capable.
     */
    private final View.OnApplyWindowInsetsListener mWindowInsetsListener =
            new View.OnApplyWindowInsetsListener() {
                @Nullable
                @Override
                public WindowInsets onApplyWindowInsets(@NonNull View view,
                        @NonNull WindowInsets windowInsets) {
                    // Do not report inset changes if the activity is not in resumed state.
                    // Reporting the inset changes when the app is going away results in visible
                    // rescaling of certain UI elements such as maps right before app goes to the
                    // background. These inset changes then need to be corrected again once the
                    // app comes to the foreground resulting with another rescaling of the
                    // screen which is not desired.
                    if (getLifecycle().getCurrentState() != Lifecycle.State.RESUMED) {
                        return WindowInsetsCompat.CONSUMED.toWindowInsets();
                    }

                    // IMPORTANT: The insets calculated here must match the windowing settings in
                    // SystemUiVisibility set in CarAppActivity#onCreate(). Failing to do so would
                    // cause a mismatch between the insets applied to the content on the hosts side
                    // vs. the actual visible window available on the client side.
                    Insets insets;
                    if (Build.VERSION.SDK_INT >= 30) {
                        insets = Api30Impl.getInsets(windowInsets);
                    } else {
                        insets = WindowInsetsCompat.toWindowInsetsCompat(windowInsets)
                                .getInsets(WindowInsetsCompat.Type.systemBars()
                                        | WindowInsetsCompat.Type.ime())
                                .toPlatformInsets();
                    }
                    DisplayCutoutCompat displayCutout =
                            WindowInsetsCompat.toWindowInsetsCompat(windowInsets)
                                    .getDisplayCutout();
                    requireNonNull(mViewModel).updateWindowInsets(insets, displayCutout);

                    // Insets are handled by the host. Only local content need padding.
                    mActivityContainerView.setPadding(0, 0, 0, 0);
                    mLocalContentContainerView.setPadding(insets.left, insets.top,
                            insets.right, insets.bottom);

                    return WindowInsetsCompat.CONSUMED.toWindowInsets();
                }
            };

    /**
     * {@link ICarAppActivity} implementation that allows the {@link IRendererService} to
     * communicate with this {@link CarAppActivity}.
     */
    private final ICarAppActivity.Stub mCarActivity =
            new ICarAppActivity.Stub() {
                @Override
                public void setSurfacePackage(@NonNull Bundleable bundleable) {
                    requireNonNull(bundleable);
                    try {
                        Object surfacePackage = bundleable.get();
                        ThreadUtils.runOnMain(() -> mSurfaceView.setSurfacePackage(surfacePackage));
                    } catch (BundlerException e) {
                        Log.e(LogTags.TAG, "Unable to set surface package", e);
                        requireNonNull(mViewModel).onError(ErrorHandler.ErrorType.HOST_ERROR);
                    }
                }

                @Override
                public void registerRendererCallback(@NonNull IRendererCallback callback) {
                    requireNonNull(callback);
                    ThreadUtils.runOnMain(
                            () -> {
                                mSurfaceView.setOnCreateInputConnectionListener(editorInfo ->
                                        getServiceDispatcher().fetch("OnCreateInputConnection",
                                                null,
                                                () -> callback.onCreateInputConnection(editorInfo))
                                );

                                mOnBackPressedListener = () ->
                                        getServiceDispatcher().dispatch("onBackPressed",
                                                callback::onBackPressed);

                                requireNonNull(mActivityLifecycleDelegate)
                                        .registerRendererCallback(callback);
                                requireNonNull(mViewModel).setRendererCallback(callback);
                            });
                }

                @Override
                public void setInsetsListener(@NonNull IInsetsListener listener) {
                    requireNonNull(listener);
                    ThreadUtils.runOnMain(
                            () -> {
                                requireNonNull(mViewModel).setInsetsListener(listener);
                                // We need to adjust local insets now that we know the host will
                                // take care of them.
                                mActivityContainerView.requestApplyInsets();
                            });
                }

                @Override
                public void setSurfaceListener(@NonNull ISurfaceListener listener) {
                    requireNonNull(listener);
                    ThreadUtils.runOnMain(
                            () -> requireNonNull(mSurfaceHolderListener)
                                    .setSurfaceListener(listener));
                }

                @Override
                public void onStartInput() {
                    ThreadUtils.runOnMain(() -> mSurfaceView.onStartInput());
                }

                @Override
                public void onStopInput() {
                    ThreadUtils.runOnMain(() -> mSurfaceView.onStopInput());
                }

                @Override
                public void startCarApp(@NonNull Intent intent) {
                    startActivity(intent);
                }

                @Override
                public void finishCarApp() {
                    finish();
                }

                @Override
                public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
                        int newSelEnd) {
                    ThreadUtils.runOnMain(() -> mSurfaceView.onUpdateSelection(oldSelStart,
                            oldSelEnd, newSelStart, newSelEnd));
                }

                @Override
                public void showAssist(Bundle args) {
                    BaseCarAppActivity.this.showAssist(args);
                }
            };

    @RequiresApi(Build.VERSION_CODES.R)
    private static class Api30Impl {
        private Api30Impl() {
        }

        @DoNotInline
        static Insets getInsets(WindowInsets windowInsets) {
            return windowInsets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime());
        }

        @DoNotInline
        static WindowInsets getDecorViewInsets(WindowInsets insets) {
            return new WindowInsets.Builder(insets).setInsets(
                    WindowInsets.Type.displayCutout(), Insets.NONE).build();
        }

        @DoNotInline
        static void setDecorFitsSystemWindows(BaseCarAppActivity activity, Window window,
                boolean decorFitsSystemWindows) {
            // Set mDecorFitsSystemWindows so we can retrieve its value for testing.
            activity.mDecorFitsSystemWindows = decorFitsSystemWindows;
            window.setDecorFitsSystemWindows(decorFitsSystemWindows);
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setSoftInputHandling();
        setContentView(R.layout.activity_template);
        mActivityContainerView = requireViewById(R.id.activity_container);
        mLocalContentContainerView = requireViewById(R.id.local_content_container);
        mSurfaceView = requireViewById(R.id.template_view_surface);
        mErrorMessageView = requireViewById(R.id.error_message_view);
        mLoadingView = requireViewById(R.id.loading_view);
        mSurfaceSnapshotView = requireViewById(R.id.template_view_snapshot);

    }

    /**
     * Binds the {@link BaseCarAppActivity} and it's view against the view model.
     */
    public void bindToViewModel(@NonNull SessionInfo sessionInfo) {
        ComponentName serviceComponentName = getServiceComponentName();
        if (serviceComponentName == null) {
            Log.e(LogTags.TAG, "Unspecified service class name");
            finish();
            return;
        }
        initializeViewModel(serviceComponentName, sessionInfo);

        mHostUpdateReceiver = new HostUpdateReceiver(requireNonNull(mViewModel));
        mHostUpdateReceiver.register(this);
        mActivityLifecycleDelegate = new ActivityLifecycleDelegate(getServiceDispatcher());
        mSurfaceHolderListener = new SurfaceHolderListener(getServiceDispatcher(),
                new SurfaceWrapperProvider(mSurfaceView));

        registerActivityLifecycleCallbacks(requireNonNull(mActivityLifecycleDelegate));

        configureSurfaceView();
        //View Model State Observations requires a non null mSurfaceHolderListener
        viewModelObserveAndBind();
        // Inset Changes require view model to be initialized
        observeInsetChanges();
    }

    private void initializeViewModel(ComponentName serviceComponentName, SessionInfo sessionInfo) {
        CarAppViewModelFactory factory = CarAppViewModelFactory.getInstance(getApplication(),
                serviceComponentName, sessionInfo);
        mViewModel = new ViewModelProvider(this, factory).get(CarAppViewModel.class);
        mViewModel.setActivity(this);
        mViewModel.resetState();
    }

    private void configureSurfaceView() {
        // Set the z-order to receive the UI events on the surface.
        mSurfaceView.setZOrderOnTop(true);
        mSurfaceView.setServiceDispatcher(getServiceDispatcher());
        mSurfaceView.setViewModel(requireNonNull(mViewModel));
        mSurfaceView.getHolder().addCallback(mSurfaceHolderListener);
    }

    private void viewModelObserveAndBind() {
        requireNonNull(mViewModel).getError().observe(this, this::onErrorChanged);
        requireNonNull(mViewModel).getState().observe(this, this::onStateChanged);
        requireNonNull(mViewModel).bind(getIntent(), mCarActivity, getDisplayId());
    }

    private void observeInsetChanges() {
        mActivityContainerView.setOnApplyWindowInsetsListener(mWindowInsetsListener);

        WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
        layoutParams.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;

        // Remove display cut-out insets on DecorView
        getWindow().getDecorView().setOnApplyWindowInsetsListener((view, insets) -> {
            if (Build.VERSION.SDK_INT >= 30) {
                insets = Api30Impl.getDecorViewInsets(insets);
            }
            return view.onApplyWindowInsets(insets);
        });

        if (Build.VERSION.SDK_INT >= 30) {
            Api30Impl.setDecorFitsSystemWindows(this, getWindow(), false);
        } else {
            getWindow().getDecorView().setFitsSystemWindows(false);
        }
        mActivityContainerView.requestApplyInsets();
    }

    /**
     * TODO(b/283985939): Workaround for testing {@code setDecorFitsSystemWindows} for older
     * versions of Roboelectric that don't support {@code getDecorFitsSystemWindows}. Remove this
     * once Roboelectric version is upgraded to v4.10.3.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean getDecorFitsSystemWindows() {
        return mDecorFitsSystemWindows;
    }

    /** Takes a snapshot of the surface view and puts it in the surfaceSnapshotView if succeeded. */
    private void takeSurfaceSnapshot() {
        // Nothing to do if the surface is not ready yet.
        if (mSurfaceView.getHolder().getSurface() == null
                || mSurfaceView.getWidth() == 0 || mSurfaceView.getHeight() == 0) {
            return;
        }
        try {
            Bitmap bitmap = Bitmap.createBitmap(mSurfaceView.getWidth(), mSurfaceView.getHeight(),
                    Bitmap.Config.ARGB_8888);
            PixelCopy.request(mSurfaceView, bitmap, status -> {
                if (status == PixelCopy.SUCCESS) {
                    mSurfaceSnapshotView.setImageBitmap(bitmap);
                } else {
                    Log.w(LogTags.TAG, "Failed to take snapshot of the surface view");
                    mSurfaceSnapshotView.setImageBitmap(null);
                }
            }, mSnapshotHandler);
        } catch (Exception e) {
            Log.e(LogTags.TAG, "Failed to take snapshot of the surface view", e);
            mSurfaceSnapshotView.setImageBitmap(null);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        takeSurfaceSnapshot();
    }

    // TODO(b/189862860): Address SOFT_INPUT_ADJUST_RESIZE deprecation
    @SuppressWarnings("deprecation")
    private void setSoftInputHandling() {
        getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE);
    }

    @Override
    public void onBackPressed() {
        if (mOnBackPressedListener != null) {
            mOnBackPressedListener.onBackPressed();
        }
    }

    private void onErrorChanged(@Nullable ErrorHandler.ErrorType errorType) {
        ThreadUtils.runOnMain(() -> mErrorMessageView.setError(errorType));
    }

    private void onStateChanged(@NonNull CarAppViewModel.State state) {
        ThreadUtils.runOnMain(() -> {
            requireNonNull(mSurfaceView);
            requireNonNull(mSurfaceSnapshotView);
            requireNonNull(mSurfaceHolderListener);

            switch (state) {
                case IDLE:
                    mSurfaceView.setVisibility(View.GONE);
                    mSurfaceSnapshotView.setVisibility(View.VISIBLE);
                    mSurfaceHolderListener.setSurfaceListener(null);
                    mErrorMessageView.setVisibility(View.GONE);
                    mLoadingView.setVisibility(View.GONE);
                    break;
                case ERROR:
                    mSurfaceView.setVisibility(View.GONE);
                    mSurfaceSnapshotView.setVisibility(View.GONE);
                    mSurfaceHolderListener.setSurfaceListener(null);
                    mErrorMessageView.setVisibility(View.VISIBLE);
                    mLoadingView.setVisibility(View.GONE);
                    break;
                case CONNECTING:
                    mSurfaceView.setVisibility(View.GONE);
                    mSurfaceSnapshotView.setVisibility(View.VISIBLE);
                    mErrorMessageView.setVisibility(View.GONE);
                    mLoadingView.setVisibility(View.VISIBLE);
                    break;
                case CONNECTED:
                    mSurfaceView.setVisibility(View.VISIBLE);
                    mSurfaceSnapshotView.setVisibility(View.VISIBLE);
                    mErrorMessageView.setVisibility(View.GONE);
                    mLoadingView.setVisibility(View.GONE);
                    break;
            }
        });
    }

    @Override
    protected void onNewIntent(@NonNull Intent intent) {
        super.onNewIntent(intent);

        requireNonNull(mSurfaceHolderListener).setSurfaceListener(null);
        requireNonNull(mActivityLifecycleDelegate).registerRendererCallback(null);

        requireNonNull(mViewModel).bind(intent, mCarActivity, getDisplayId());
    }

    // TODO(b/189864400): Address WindowManager#getDefaultDisplay() deprecation
    @SuppressWarnings("deprecation")
    @VisibleForTesting
    int getDisplayId() {
        return getWindowManager().getDefaultDisplay().getDisplayId();
    }

    @VisibleForTesting
    ServiceDispatcher getServiceDispatcher() {
        return requireNonNull(mViewModel).getServiceDispatcher();
    }

    @Override
    protected void onDestroy() {
        if (mHostUpdateReceiver != null) {
            mHostUpdateReceiver.unregister(this);
        }
        if (mSurfaceHolderListener != null) {
            mSurfaceHolderListener.setSurfaceListener(null);
        }
        if (mViewModel != null) {
            mViewModel.setActivity(null);
        }
        super.onDestroy();
    }

    /**
     * @see #getServiceComponentName()
     */
    @Nullable
    @ExperimentalCarApi
    public ComponentName retrieveServiceComponentName() {
        return getServiceComponentName();
    }

    /**
     * Retrieves the {@link  ComponentName} to which the view model will talk
     * in order to render.
     */
    @Nullable
    @SuppressWarnings("deprecation")
    public ComponentName getServiceComponentName() {
        Intent intent = new Intent(SERVICE_INTERFACE);
        intent.setPackage(getPackageName());
        List<ResolveInfo> infos = getPackageManager().queryIntentServices(intent, 0);
        if (infos == null || infos.isEmpty()) {
            Log.e(LogTags.TAG, "Unable to find required " + SERVICE_INTERFACE
                    + " implementation. App manifest must include exactly one car app service.");
            return null;
        } else if (infos.size() != 1) {
            Log.e(LogTags.TAG, "Found more than one " + SERVICE_INTERFACE
                    + " implementation. App manifest must include exactly one car app service.");
            return null;
        }
        String serviceName = infos.get(0).serviceInfo.name;
        return new ComponentName(this, serviceName);
    }
}