public abstract class

Screen

extends java.lang.Object

implements LifecycleOwner

 java.lang.Object

↳androidx.car.app.Screen

Gradle dependencies

compile group: 'androidx.car.app', name: 'app', version: '1.2.0-rc01'

  • groupId: androidx.car.app
  • artifactId: app
  • version: 1.2.0-rc01

Artifact androidx.car.app:app:1.2.0-rc01 it located at Google repository (https://maven.google.com/)

Overview

A Screen has a Lifecycle and provides the mechanism for the app to send Templates to display when the Screen is visible. Screen instances can also be pushed and popped to and from a Screen stack, which ensures they adhere to the template flow restrictions (see Screen.onGetTemplate() for more details on template flow).

The Screen class can be used to manage individual units of business logic within a car app. A Screen is closely tied to the CarAppService it is a part of, and cannot be used without it. Though Screen defines its own lifecycle (see Screen.getLifecycle()), that lifecycle is dependent on its CarAppService: if the car app service is stopped, no screens inside of it can be started; when the car app service is destroyed, all screens will be destroyed.

Screen objects are not thread safe and all calls should be made from the same thread.

Summary

Constructors
protectedScreen(CarContext carContext)

Methods
public voiddispatchLifecycleEvent(Lifecycle.Event event)

Dispatches lifecycle event for event on the main thread.

public final voidfinish()

Removes this screen from the stack, which will move its lifecycle state down to Lifecycle.State.DESTROYED.

public final CarContextgetCarContext()

Returns the CarContext of the CarAppService.

public final LifecyclegetLifecycle()

Returns this screen's lifecycle.

public java.lang.StringgetMarker()

Retrieves the marker that has been set for this screen, or null if one has not been set.

public java.lang.ObjectgetResultInternal()

Returns the result set via Screen.setResult(Object), or null if none is set.

public final ScreenManagergetScreenManager()

Returns the ScreenManager to use for pushing/removing screens.

public final voidinvalidate()

Requests the current template to be invalidated, which eventually triggers a call to Screen.onGetTemplate() to get the new template to display.

public abstract TemplateonGetTemplate()

Returns the Template to present in the car screen.

public voidsetMarker(java.lang.String marker)

Updates the marker for this screen.

public voidsetResult(java.lang.Object result)

Sets the result that will be sent to the OnScreenResultListener that was given when pushing this screen onto the stack using ScreenManager.pushForResult(Screen, OnScreenResultListener).

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

protected Screen(CarContext carContext)

Methods

public final void invalidate()

Requests the current template to be invalidated, which eventually triggers a call to Screen.onGetTemplate() to get the new template to display.

If the current Lifecycle.State of this screen is not at least Lifecycle.State.STARTED, then a call to this method will have no effect.

After the call to invalidate is made, subsequent calls have no effect until the new template is returned by Screen.onGetTemplate().

To avoid race conditions with calls to Screen.onGetTemplate() you should call this method with the main thread.

public final void finish()

Removes this screen from the stack, which will move its lifecycle state down to Lifecycle.State.DESTROYED.

Call when your screen is done and should be removed from the stack.

If this screen is the only one in the stack, it will not be finished.

public void setResult(java.lang.Object result)

Sets the result that will be sent to the OnScreenResultListener that was given when pushing this screen onto the stack using ScreenManager.pushForResult(Screen, OnScreenResultListener).

Only the final result set will be sent.

The result will be propagated when this screen is being destroyed. This can be due to being removed from the stack or explicitly calling Screen.finish().

Parameters:

result: the value to send to the OnScreenResultListener that was given when pushing this screen onto the stack using ScreenManager.pushForResult(Screen, OnScreenResultListener)

public java.lang.Object getResultInternal()

Returns the result set via Screen.setResult(Object), or null if none is set.

public void setMarker(java.lang.String marker)

Updates the marker for this screen.

Set the marker to null to clear it.

This is used for setting a marker to where you can jump back to by calling ScreenManager.popTo(String).

public java.lang.String getMarker()

Retrieves the marker that has been set for this screen, or null if one has not been set.

See also: Screen.setMarker(String)

public final Lifecycle getLifecycle()

Returns this screen's lifecycle.

Here are some ways you can use a Screen's Lifecycle:

What each Lifecycle.Event means for a screen:

Lifecycle.Event.ON_CREATE
The screen is in the process of being pushed to the screen stack, it is valid, but contents from it are not yet visible in the car screen. You should get a callback to Screen.onGetTemplate() at a point after this call. This is where you can make decision on whether this Screen is still relevant, and if you choose to not return a Template from this Screen call Screen.finish().
Lifecycle.Event.ON_START
The template returned from this screen is visible in the car screen.
Lifecycle.Event.ON_RESUME
The user can now interact with the template returned from this screen.
Lifecycle.Event.ON_PAUSE
The user can no longer interact with this screen's template.
Lifecycle.Event.ON_STOP
The template returned from this screen is no longer visible.
Lifecycle.Event.ON_DESTROY
This screen is no longer valid and is removed from the screen stack.

Listeners that are added in Lifecycle.Event.ON_START, should be removed in Lifecycle.Event.ON_STOP.

Similarly, listeners that are added in Lifecycle.Event.ON_CREATE should be removed in Lifecycle.Event.ON_DESTROY.

See also: LifecycleObserver

public final CarContext getCarContext()

Returns the CarContext of the CarAppService.

public final ScreenManager getScreenManager()

Returns the ScreenManager to use for pushing/removing screens.

public abstract Template onGetTemplate()

Returns the Template to present in the car screen.

This method is invoked whenever a new template is needed, for example, the first time the screen is created, or when the UI is invalidated through a call to Screen.invalidate().

Throttling of UI updates

To minimize user distraction while driving, the host will throttle template updates to the car screen. When the app invalidates multiple times in a short period, the host will call this method for each call, but it may not update the actual car screen right away. This will ensure there are not excessive UI changes in a short period of time.

For example, if the app sends the host two or more templates within a period of time shorter than the throttle period, only the last template will be displayed on the car screen.

Template Restrictions

The host limits the number of templates to display for a given task to a maximum of 5, of which the last template of the 5 must be one of the following types:

If the 5 template quota is exhausted and the app attempts to send a new template, the host will display an error message to the user before closing the app. Note that this limit applies to the number of templates, and not the number of screen instances in the stack. For example, if while in screen A an app sends 2 templates, and then pushes screen B, it can now send 3 more templates. Alternatively, if each screen is structured to send a single template, then the app can push 5 Screen instances onto the ScreenManager stack.

There are special cases to these restrictions: template refreshes, back and reset operations.

Template Refreshes
Certain content updates are not counted towards the template limit. In general, as long as an app returns a template that is of the same type and contains the same main content as the previous template, the new template will not be counted against the quota. For example, updating the toggle state of a row in a ListTemplate does not count against the quota. See the documentation of individual Template classes to learn more about what types of content updates can be considered a refresh.
Back Operations
To enable sub-flows within a task, the host detects when an app is popping a Screen from the ScreenManager's stack, and updates the remaining quota based on the number of templates that the app is going backwards by.

For example, if while in screen A, the app sends 2 templates and then pushes screen B and sends 2 more templates, then the app has 1 quota remaining. If the app now pops back to screen A, the host will reset the quota to 3, because the app has gone backwards by 2 templates.

Note that when popping back to a Screen, an app must send a Template that is of the same type as the one last sent by that screen. Sending any other template types would cause an error. However, as long as the type remains the same during a back operation, an app can freely modify the contents of the template without affecting the quota.

Reset Operations
Certain Template classes have special semantics that signify the end of a task. For example, the NavigationTemplate is a template that is expected to stay on the screen and be refreshed with new turn-by-turn instructions for the user's consumption. Upon reaching one of these templates, the host will reset the template quota, treating that template as if it is the first step of a new task, thus allowing the app to begin a new task. See the documentation of individual Template classes to see which ones trigger a reset on the host.

If the host receives an to start the car app from a notification action or from the launcher, the quota will also be reset. This mechanism allows an app to begin a new task flow from notifications, and it holds true even if an app is already bound and in the foreground.

See CarAppExtender for details on notifications.

public void dispatchLifecycleEvent(Lifecycle.Event event)

Dispatches lifecycle event for event on the main thread.

Source

/*
 * 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;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.car.app.utils.LogTags.TAG;

import static java.util.Objects.requireNonNull;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.car.app.model.Template;
import androidx.car.app.model.TemplateInfo;
import androidx.car.app.model.TemplateWrapper;
import androidx.car.app.utils.ThreadUtils;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.Lifecycle.State;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;

/**
 * A Screen has a {@link Lifecycle} and provides the mechanism for the app to send {@link Template}s
 * to display when the Screen is visible. Screen instances can also be pushed and popped to and from
 * a Screen stack, which ensures they adhere to the template flow restrictions (see {@link
 * #onGetTemplate} for more details on template flow).
 *
 * <p>The Screen class can be used to manage individual units of business logic within a car app. A
 * Screen is closely tied to the {@link CarAppService} it is a part of, and cannot be used without
 * it. Though Screen defines its own lifecycle (see {@link #getLifecycle}), that lifecycle is
 * dependent on its {@link CarAppService}: if the car app service is stopped, no screens inside of
 * it can be started; when the car app service is destroyed, all screens will be destroyed.
 *
 * <p>Screen objects are not thread safe and all calls should be made from the same thread.
 */
// This lint warning is triggered because this has a finish() API. Suppress because we are not
// actually cleaning any held resources in that method.
@SuppressWarnings("NotCloseable")
public abstract class Screen implements LifecycleOwner {
    private final CarContext mCarContext;

    @SuppressWarnings({"assignment.type.incompatible", "argument.type.incompatible"})
    private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

    private OnScreenResultListener mOnScreenResultListener = (obj) -> {
    };

    @Nullable
    private Object mResult;

    @Nullable
    private String mMarker;

    /**
     * A reference to the last template returned by this screen, or {@code null} if one has not been
     * returned yet.
     */
    @Nullable
    private TemplateWrapper mTemplateWrapper;

    /**
     * Whether to set the ID of the last template in the next template to be returned.
     *
     * @see #onGetTemplate
     */
    private boolean mUseLastTemplateId;

    protected Screen(@NonNull CarContext carContext) {
        mCarContext = requireNonNull(carContext);
    }

    /**
     * Requests the current template to be invalidated, which eventually triggers a call to {@link
     * #onGetTemplate} to get the new template to display.
     *
     * <p>If the current {@link State} of this screen is not at least {@link State#STARTED}, then a
     * call to this method will have no effect.
     *
     * <p>After the call to invalidate is made, subsequent calls have no effect until the new
     * template is returned by {@link #onGetTemplate}.
     *
     * <p>To avoid race conditions with calls to {@link #onGetTemplate} you should call this method
     * with the main thread.
     *
     * @throws HostException if the remote call fails
     */
    public final void invalidate() {
        if (getLifecycle().getCurrentState().isAtLeast(State.STARTED)) {
            mCarContext.getCarService(AppManager.class).invalidate();
        }
    }

    /**
     * Removes this screen from the stack, which will move its lifecycle state down to {@link
     * State#DESTROYED}.
     *
     * <p>Call when your screen is done and should be removed from the stack.
     *
     * <p>If this screen is the only one in the stack, it will not be finished.
     */
    public final void finish() {
        mCarContext.getCarService(ScreenManager.class).remove(this);
    }

    /**
     * Sets the {@code result} that will be sent to the {@link OnScreenResultListener} that was
     * given when pushing this screen onto the stack using {@link ScreenManager#pushForResult}.
     *
     * <p>Only the final {@code result} set will be sent.
     *
     * <p>The {@code result} will be propagated when this screen is being destroyed. This can be due
     * to being removed from the stack or explicitly calling {@link #finish}.
     *
     * @param result the value to send to the {@link OnScreenResultListener} that was given when
     *               pushing this screen onto the stack using {@link ScreenManager#pushForResult}
     */
    public void setResult(@Nullable Object result) {
        mResult = result;
    }

    /**
     * Returns the result set via {@link #setResult}, or {@code null} if none is set.
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @Nullable
    public Object getResultInternal() {
        return mResult;
    }

    /**
     * Updates the marker for this screen.
     *
     * <p>Set the {@code marker} to {@code null} to clear it.
     *
     * <p>This is used for setting a marker to where you can jump back to by calling {@link
     * ScreenManager#popTo}.
     */
    public void setMarker(@Nullable String marker) {
        mMarker = marker;
    }

    /**
     * Retrieves the {@code marker} that has been set for this screen, or {@code null} if one has
     * not been set.
     *
     * @see #setMarker
     */
    @Nullable
    public String getMarker() {
        return mMarker;
    }

    /**
     * Returns this screen's lifecycle.
     *
     * <p>Here are some ways you can use a Screen's {@link Lifecycle}:
     *
     * <ul>
     *   <li>Observe its {@link Lifecycle} by calling {@link Lifecycle#addObserver}. You can use the
     *       {@link androidx.lifecycle.LifecycleObserver} to take specific actions whenever the
     *       screen
     *       receives different {@link Event}s.
     *   <li>Use this Screen to observe {@link androidx.lifecycle.LiveData}s that may drive the
     *       backing data for your templates.
     * </ul>
     *
     * <p>What each {@link Event} means for a screen:
     *
     * <dl>
     *   <dt>{@link Event#ON_CREATE}
     *   <dd>The screen is in the process of being pushed to the screen stack, it is valid, but
     *       contents from it are not yet visible in the car screen. You should get a callback to
     *       {@link #onGetTemplate} at a point after this call.
     *       This is where you can make decision on whether this {@link Screen} is still
     *       relevant, and if you choose to not return a {@link Template} from this
     *       {@link Screen} call {@link #finish()}.
     *   <dt>{@link Event#ON_START}
     *   <dd>The template returned from this screen is visible in the car screen.
     *   <dt>{@link Event#ON_RESUME}
     *   <dd>The user can now interact with the template returned from this screen.
     *   <dt>{@link Event#ON_PAUSE}
     *   <dd>The user can no longer interact with this screen's template.
     *   <dt>{@link Event#ON_STOP}
     *   <dd>The template returned from this screen is no longer visible.
     *   <dt>{@link Event#ON_DESTROY}
     *   <dd>This screen is no longer valid and is removed from the screen stack.
     * </dl>
     *
     * <p>Listeners that are added in {@link Event#ON_START}, should be removed in {@link
     * Event#ON_STOP}.
     *
     * <p>Similarly, listeners that are added in {@link Event#ON_CREATE} should be removed in {@link
     * Event#ON_DESTROY}.
     *
     * @see androidx.lifecycle.LifecycleObserver
     */
    @Override
    @NonNull
    public final Lifecycle getLifecycle() {
        return mLifecycleRegistry;
    }

    /** Returns the {@link CarContext} of the {@link CarAppService}. */
    @NonNull
    public final CarContext getCarContext() {
        return mCarContext;
    }

    /** Returns the {@link ScreenManager} to use for pushing/removing screens. */
    @NonNull
    public final ScreenManager getScreenManager() {
        return mCarContext.getCarService(ScreenManager.class);
    }

    /**
     * Returns the {@link Template} to present in the car screen.
     *
     * <p>This method is invoked whenever a new template is needed, for example, the first time the
     * screen is created, or when the UI is invalidated through a call to {@link #invalidate}.
     *
     * <h4>Throttling of UI updates</h4>
     *
     * To minimize user distraction while driving, the host will throttle template updates to the
     * car screen. When the app invalidates multiple times in a short period, the host will call
     * this method for each call, but it may not update the actual car screen right away. This will
     * ensure there are not excessive UI changes in a short period of time.
     *
     * <p>For example, if the app sends the host two or more templates within a period of time
     * shorter than the throttle period, only the last template will be displayed on the car screen.
     *
     * <h4>Template Restrictions</h4>
     *
     * The host limits the number of templates to display for a given task to a maximum of 5, of
     * which the last template of the 5 must be one of the following types:
     *
     * <ul>
     *   <li>{@link androidx.car.app.navigation.model.NavigationTemplate}
     *   <li>{@link androidx.car.app.model.PaneTemplate}
     *   <li>{@link androidx.car.app.model.MessageTemplate}
     * </ul>
     *
     * <p><b>If the 5 template quota is exhausted and the app attempts to send a new template, the
     * host will display an error message to the user before closing the app.</b> Note that this
     * limit applies to the number of templates, and not the number of screen instances in the
     * stack. For example, if while in screen A an app sends 2 templates, and then pushes screen
     * B, it can now send 3 more templates. Alternatively, if each screen is structured to send a
     * single template, then the app can push 5 {@link Screen} instances onto the
     * {@link ScreenManager} stack.
     *
     * <p>There are special cases to these restrictions: template refreshes, back and reset
     * operations.
     *
     * <h5>Template Refreshes</h5>
     *
     * Certain content updates are not counted towards the template limit. In general, as long as an
     * app returns a template that is of the same type and contains the same main content as the
     * previous template, the new template will not be counted against the quota. For example,
     * updating the toggle state of a row in a {@link androidx.car.app.model.ListTemplate} does
     * not count against the quota. See the documentation of individual {@link Template} classes
     * to learn more about what types of content updates can be considered a refresh.
     *
     * <h5>Back Operations</h5>
     *
     * To enable sub-flows within a task, the host detects when an app is popping a {@link Screen}
     * from the {@link ScreenManager}'s stack, and updates the remaining quota based on the
     * number of templates that the app is going backwards by.
     *
     * <p>For example, if while in screen A, the app sends 2 templates and then pushes screen B and
     * sends 2 more templates, then the app has 1 quota remaining. If the app now pops back to
     * screen A, the host will reset the quota to 3, because the app has gone backwards by 2
     * templates.
     *
     * <p>Note that when popping back to a {@link Screen}, an app must send a {@link Template}
     * that is of the same type as the one last sent by that screen. Sending any other template
     * types would cause an error. However, as long as the type remains the same during a back
     * operation, an app can freely modify the contents of the template without affecting the quota.
     *
     * <h5>Reset Operations</h5>
     *
     * Certain {@link Template} classes have special semantics that signify the end of a task. For
     * example, the {@link androidx.car.app.navigation.model.NavigationTemplate} is a template
     * that is expected to stay on the screen and be refreshed with new turn-by-turn instructions
     * for the user's consumption. Upon reaching one of these templates, the host will reset the
     * template quota, treating that template as if it is the first step of a new task, thus
     * allowing the app to begin a new task. See the documentation of individual {@link Template}
     * classes to see which ones trigger a reset on the host.
     *
     * <p>If the host receives an {@link android.content.Intent} to start the car app from a
     * notification action or from the launcher, the quota will also be reset. This mechanism allows
     * an app to begin a new task flow from notifications, and it holds true even if an app is
     * already bound and in the foreground.
     *
     * <p>See {@link androidx.car.app.notification.CarAppExtender} for details on notifications.
     */
    @NonNull
    public abstract Template onGetTemplate();

    /** Sets a {@link OnScreenResultListener} for this {@link Screen}. */
    void setOnScreenResultListener(OnScreenResultListener onScreenResultListener) {
        mOnScreenResultListener = onScreenResultListener;
    }

    /**
     * Dispatches lifecycle event for {@code event} on the main thread.
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    // Restrict to testing library
    public void dispatchLifecycleEvent(@NonNull Event event) {
        ThreadUtils.runOnMain(
                () -> {
                    State currentState = mLifecycleRegistry.getCurrentState();
                    // Avoid handling further events if the screen is already marked as destroyed.
                    if (!currentState.isAtLeast(State.INITIALIZED)) {
                        return;
                    }

                    if (event == Event.ON_DESTROY) {
                        mOnScreenResultListener.onScreenResult(mResult);
                    }

                    mLifecycleRegistry.handleLifecycleEvent(event);
                });
    }

    /**
     * Calls {@link #onGetTemplate} to get the next {@link Template} for the screen and returns it
     * wrapped in a {@link TemplateWrapper}.
     *
     * <p>The {@link TemplateWrapper} attaches a unique ID to the wrapped template, which is used
     * for implementing flow restrictions. The host keeps track of these IDs to detect push, pop, or
     * refresh operations and handle the different cases accordingly. For example, when more than
     * a max limit of templates are pushed, the host may return an error.
     *
     * <p>If {@link #setUseLastTemplateId} is called first, this method will produce a wrapper
     * that is stamped with the same ID as the last template returned by this screen. This is
     * used to identify back (stack pop) operations.
     */
    @NonNull
    TemplateWrapper getTemplateWrapper() {
        Template template = onGetTemplate();

        TemplateWrapper wrapper;
        if (mUseLastTemplateId) {
            wrapper =
                    TemplateWrapper.wrap(
                            template, getLastTemplateInfo(
                                    requireNonNull(mTemplateWrapper)).getTemplateId());
        } else {
            wrapper = TemplateWrapper.wrap(template);
        }
        mUseLastTemplateId = false;

        mTemplateWrapper = wrapper;

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Returning " + template + " from screen " + this);
        }
        return wrapper;
    }

    /**
     * Returns the information for the template that was last returned by this screen.
     *
     * <p>If no templates have been returned from this screen yet, this will call
     * {@link #onGetTemplate} to retrieve the {@link Template} and generate an info for it. This is
     * used in the case where multiple screens are added before a {@link #onGetTemplate} method is
     * dispatched to the top screen, allowing to notify the host of the current stack of template
     * ids known to the client.
     */
    @NonNull
    TemplateInfo getLastTemplateInfo() {
        if (mTemplateWrapper == null) {
            mTemplateWrapper = TemplateWrapper.wrap(onGetTemplate());
        }
        return new TemplateInfo(mTemplateWrapper.getTemplate().getClass(),
                mTemplateWrapper.getId());
    }

    @NonNull
    private static TemplateInfo getLastTemplateInfo(TemplateWrapper lastTemplateWrapper) {
        return new TemplateInfo(lastTemplateWrapper.getTemplate().getClass(),
                lastTemplateWrapper.getId());
    }

    /**
     * Denotes whether the next {@link Template} retrieved via {@link #onGetTemplate} should reuse
     * the ID of the last {@link Template}.
     *
     * <p>When this is set to {@code true}, the host will considered the next template sent to be a
     * back operation, and will attempt to find the previous template that shares the same ID and
     * reset the task step to that point in time.
     */
    void setUseLastTemplateId(boolean useLastTemplateId) {
        mUseLastTemplateId = useLastTemplateId;
    }
}