TemplateWrapper.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.model;

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

import static java.util.Objects.requireNonNull;

import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.car.app.annotations.CarProtocol;
import androidx.car.app.utils.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * A wrapper for mapping a {@link Template} with a unique ID used for implementing task flow
 * restrictions.
 *
 * <p>This is what is sent to the host, so that the host can determine whether the template is a new
 * template (e.g. a step counts toward the task limit), or an existing template update (e.g. a
 * refresh that does not count towards the task limit), by checking whether the ID have changed.
 *
 * <p><strong>This class is for use by host implementations and not by apps.</strong>
 */
@CarProtocol
public final class TemplateWrapper {
    @Keep
    @Nullable
    private Template mTemplate;
    @Keep
    @Nullable
    private String mId;
    @Keep
    private List<TemplateInfo> mTemplateInfoForScreenStack = new ArrayList<>();

    /** The current step in a task that the template is in. */
    private int mCurrentTaskStep;

    /** Whether the template wrapper is a refresh of the current template. */
    private boolean mIsRefresh;

    /**
     * Creates a {@link TemplateWrapper} instance with the given {@link Template}.
     *
     * <p>The host will treat the {@link Template} as a new task step, unless it determines through
     * its internal logic that the {@link Template} is a refresh of the existing view, in which case
     * the task step will remain the same.
     */
    @NonNull
    public static TemplateWrapper wrap(@NonNull Template template) {
        // Assign a random ID to the template. This should be unique so that the host knows the
        // template is a new step. We are not using hashCode() here as we might override
        // template's hash codes in the future.
        //
        // Note: There is a chance of collision here, in which case the host will reset the
        // task step to the value of a previous template that has the colliding ID. The chance of
        // this happening should be negligible given we are dealing with a very small number of
        // templates in the stack.
        return wrap(template, createRandomId());
    }

    /**
     * Creates a {@link TemplateWrapper} instance with the given {@link Template} and ID.
     *
     * <p>The ID is primarily used to inform the host that the given {@link Template} shares the
     * same ID as a previously sent {@link Template}, even though their contents differ. In such
     * cases, the host will reset the task step to where the previous {@link Template} was.
     *
     * <p>For example, the client sends template A (task step 1), then move forwards a screen and
     * sends template B (task step 2). Now the client pops the screen and sends template C. By
     * assigning the ID of template A to template C, the client library informs the host that it
     * is a back operation and the task step should be set to 1 again.
     */
    @NonNull
    public static TemplateWrapper wrap(@NonNull Template template, @NonNull String id) {
        return new TemplateWrapper(requireNonNull(template), requireNonNull(id));
    }

    /** Returns the wrapped {@link Template}. */
    @NonNull
    public Template getTemplate() {
        // Intentionally kept as non-null because the library creates these classes internally after
        // the app returns a non-null template, a null-value should not be expected here.
        return requireNonNull(mTemplate);
    }

    /** Returns the ID associated with the wrapped {@link Template}. */
    @NonNull
    public String getId() {
        // Intentionally kept as non-null because the library creates these classes internally after
        // the app returns a non-null template, a null-value should not be expected here.
        return requireNonNull(mId);
    }

    /**
     * Sets the {@link TemplateInfo} of each of the last known templates for each of the screens in
     * the stack managed by the screen manager.
     *
     * @hide
     * @see #getTemplateInfosForScreenStack
     */
    @RestrictTo(LIBRARY)
    public void setTemplateInfosForScreenStack(
            @NonNull List<TemplateInfo> templateInfoForScreenStack) {
        mTemplateInfoForScreenStack = templateInfoForScreenStack;
    }

    /**
     * Returns a {@link TemplateInfo} for the last returned template for each of the screens in the
     * screen stack managed by the screen manager.
     *
     * <p>The return values are in order, where position 0 is the top of the stack, and position
     * n is the bottom of the stack given n screens on the stack.
     */
    @NonNull
    public List<TemplateInfo> getTemplateInfosForScreenStack() {
        return CollectionUtils.emptyIfNull(mTemplateInfoForScreenStack);
    }

    /** Retrieves the current task step that the template is in. */
    public int getCurrentTaskStep() {
        return mCurrentTaskStep;
    }

    /** Sets the current task step that the template is in. */
    public void setCurrentTaskStep(int currentTaskStep) {
        mCurrentTaskStep = currentTaskStep;
    }

    /** Sets whether the template is a refresh of the current template. */
    public void setRefresh(boolean isRefresh) {
        mIsRefresh = isRefresh;
    }

    /** Returns {@code true} if the template is a refresh for the previous template. */
    public boolean isRefresh() {
        return mIsRefresh;
    }

    /** Updates the {@link Template} this {@link TemplateWrapper} instance wraps. */
    public void setTemplate(@NonNull Template template) {
        mTemplate = template;
    }

    /** Updates the ID associated with the wrapped {@link Template}. */
    public void setId(@NonNull String id) {
        mId = id;
    }

    /** Creates a copy of the given {@link TemplateWrapper}. */
    @NonNull
    public static TemplateWrapper copyOf(@NonNull TemplateWrapper source) {
        TemplateWrapper destination = TemplateWrapper.wrap(source.getTemplate(), source.getId());
        destination.setRefresh(source.isRefresh());
        destination.setCurrentTaskStep(source.getCurrentTaskStep());
        List<TemplateInfo> templateInfos = source.getTemplateInfosForScreenStack();
        if (templateInfos != null) {
            destination.setTemplateInfosForScreenStack(templateInfos);
        }
        return destination;
    }

    @NonNull
    @Override
    public String toString() {
        return "[template: " + mTemplate + ", ID: " + mId + "]";
    }

    private TemplateWrapper(Template template, String id) {
        mTemplate = template;
        mId = id;
    }

    private TemplateWrapper() {
        mTemplate = null;
        mId = "";
    }

    private static String createRandomId() {
        return UUID.randomUUID().toString();
    }
}