CarIconSpan.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.IntDef;
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.model.constraints.CarIconConstraints;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;

/**
 * A span that replaces the text it is attached to with a {@link CarIcon} that is aligned with the
 * surrounding text.
 *
 * <p>The image may be scaled with the text differently depending on the template that the text
 * belongs to. Refer to the documentation of each template for that information.
 *
 * <p>For example, the following code creates a string for a navigation maneuver that has an image
 * with the number of a highway rendered as an icon in between "on" and "East":
 *
 * <pre>{@code
 * SpannableString string = new SpannableString("Turn right on 520 East");
 * string.setSpan(
 *     CarIconSpan.create(new CarIcon.Builder(
 *         IconCompat.createWithResource(getCarContext(), R.drawable.ic_520_highway))),
 *         14, 17, SPAN_INCLUSIVE_EXCLUSIVE);
 * }</pre>
 *
 * <p>{@link CarIconSpan}s in strings passed to the library templates may be ignored by the host
 * when displaying the text unless support for them is explicitly documented in the API that takes
 * the string.
 *
 * <p>This span will be ignored if it overlaps with any span that replaces text, such as another
 * {@link DistanceSpan}, {@link DurationSpan}, or {@link CarIconSpan}.
 *
 * @see CarIcon
 */
@CarProtocol
public final class CarIconSpan extends CarSpan {
    /**
     * Indicates how to align a car icon span with its surrounding text.
     *
     * @hide
     */
    @IntDef(
            value = {
                    ALIGN_CENTER,
                    ALIGN_BOTTOM,
                    ALIGN_BASELINE,
            })
    @Retention(RetentionPolicy.SOURCE)
    @RestrictTo(LIBRARY)
    public @interface Alignment {
    }

    /**
     * A constant indicating that the bottom of this span should be aligned with the bottom of the
     * surrounding text, at the same level as the lowest descender in the text.
     */
    @Alignment
    public static final int ALIGN_BOTTOM = 0;

    /**
     * A constant indicating that the bottom of this span should be aligned with the baseline of the
     * surrounding text.
     */
    @Alignment
    public static final int ALIGN_BASELINE = 1;

    /**
     * A constant indicating that this span should be vertically centered between the top and the
     * lowest descender.
     */
    @Alignment
    public static final int ALIGN_CENTER = 2;

    @Nullable
    @Keep
    private final CarIcon mIcon;
    @Alignment
    @Keep
    private final int mAlignment;

    /**
     * Creates a {@link CarIconSpan} from a {@link CarIcon} with a default alignment of {@link
     * #ALIGN_BASELINE}.
     *
     * @throws NullPointerException if {@code icon} is {@code null}
     * @see #create(CarIcon, int)
     */
    @NonNull
    public static CarIconSpan create(@NonNull CarIcon icon) {
        return create(icon, ALIGN_BASELINE);
    }

    /**
     * Creates a {@link CarIconSpan} from a {@link CarIcon}, specifying the alignment of the icon
     * with respect to its surrounding text.
     *
     * @param icon      the {@link CarIcon} to replace the text with
     * @param alignment the alignment of the {@link CarIcon} relative to the text. This should be
     *                  one of {@link #ALIGN_BASELINE}, {@link #ALIGN_BOTTOM} or
     *                  {@link #ALIGN_CENTER}
     * @throws NullPointerException     if {@code icon} is {@code null}
     * @throws IllegalArgumentException if {@code alignment} is not a valid value
     * @see #ALIGN_BASELINE
     * @see #ALIGN_BOTTOM
     * @see #ALIGN_CENTER
     */
    @NonNull
    public static CarIconSpan create(@NonNull CarIcon icon, @Alignment int alignment) {
        CarIconConstraints.DEFAULT.validateOrThrow(icon);
        if (alignment != ALIGN_BASELINE && alignment != ALIGN_BOTTOM && alignment != ALIGN_CENTER) {
            throw new IllegalStateException("Invalid alignment value: " + alignment);
        }

        return new CarIconSpan(requireNonNull(icon), alignment);
    }

    private CarIconSpan(@Nullable CarIcon icon, @Alignment int alignment) {
        mIcon = icon;
        mAlignment = alignment;
    }

    private CarIconSpan() {
        mIcon = null;
        mAlignment = ALIGN_BASELINE;
    }

    /**
     * Returns the {@link CarIcon} instance associated with this span.
     */
    @NonNull
    public CarIcon getIcon() {
        return requireNonNull(mIcon);
    }

    /**
     * Returns the alignment that should be used with this span.
     */
    @Alignment
    public int getAlignment() {
        return mAlignment;
    }

    @Override
    @NonNull
    public String toString() {
        return "[icon: " + mIcon + ", alignment: " + alignmentToString(mAlignment) + "]";
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(mIcon);
    }

    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof CarIconSpan)) {
            return false;
        }
        CarIconSpan otherIconSpan = (CarIconSpan) other;

        return Objects.equals(mIcon, otherIconSpan.mIcon);
    }

    private static String alignmentToString(@Alignment int alignment) {
        switch (alignment) {
            case ALIGN_BASELINE:
                return "baseline";
            case ALIGN_BOTTOM:
                return "bottom";
            case ALIGN_CENTER:
                return "center";
            default:
                return "unknown";
        }
    }
}