SessionInfo.java
/*
* Copyright 2022 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 java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.car.app.annotations.CarProtocol;
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.annotations.RequiresCarApi;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.NavigationTemplate;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.versioning.CarAppApiLevel;
import androidx.car.app.versioning.CarAppApiLevels;
import com.google.common.collect.ImmutableSet;
import java.lang.annotation.Retention;
import java.util.Objects;
import java.util.Set;
/** Information about a {@link Session}, such as the physical display and the session ID. */
@RequiresCarApi(5)
@CarProtocol
public class SessionInfo {
private static final char DIVIDER = '/';
/** The key for a {@link Bundleable} extra containing the {@link SessionInfo} for a bind. */
public static final String EXTRA_SESSION_INFO = "androidx.car.app.extra.SESSION_INFO";
/** The primary infotainment display usually in the center column of the vehicle. */
public static final int DISPLAY_TYPE_MAIN = 0;
/** The cluster display, usually located behind the steering wheel. */
public static final int DISPLAY_TYPE_CLUSTER = 1;
private static final ImmutableSet<Class<? extends Template>> CLUSTER_SUPPORTED_TEMPLATES_API_5 =
ImmutableSet.of(NavigationTemplate.class);
private static final ImmutableSet<Class<? extends Template>>
CLUSTER_SUPPORTED_TEMPLATES_LESS_THAN_API_5 = ImmutableSet.of();
/**
* @hide
*/
@IntDef({DISPLAY_TYPE_MAIN, DISPLAY_TYPE_CLUSTER})
@Retention(SOURCE)
public @interface DisplayType {
}
/**
* A default {@link SessionInfo} for the main display, used when the host is on a version
* that doesn't support this new class.
*/
@NonNull
public static final SessionInfo DEFAULT_SESSION_INFO = new SessionInfo(
DISPLAY_TYPE_MAIN, "main");
/** A string identifier unique per physical display. */
@Keep
@NonNull
private final String mSessionId;
/** The type of display the {@link Session} is rendering on. */
@Keep
@DisplayType
private final int mDisplayType;
/**
* Returns a session-stable ID, unique to the display that the {@link Session} is rendering on.
*/
@NonNull
public String getSessionId() {
return mSessionId;
}
/** Returns the type of display that the {@link Session} is rendering on. */
@DisplayType
public int getDisplayType() {
return mDisplayType;
}
/**
* Creates a new {@link SessionInfo} with the provided {@code displayType} and {@code
* sessionId}.
*/
public SessionInfo(@DisplayType int displayType, @NonNull String sessionId) {
mDisplayType = displayType;
mSessionId = sessionId;
}
/**
* Creates a new {@link SessionInfo} for a given bind {@code intent}
*/
@SuppressWarnings("deprecation")
public SessionInfo(@NonNull Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
throw new IllegalArgumentException(
"Expected the SessionInfo to be encoded in the bind intent extras, but the "
+ "extras were null.");
}
Bundleable sessionInfoBundleable = extras.getParcelable(EXTRA_SESSION_INFO);
if (sessionInfoBundleable == null) {
throw new IllegalArgumentException(
"Expected the SessionInfo to be encoded in the bind intent extras, but they "
+ "couldn't be found in the extras.");
}
try {
SessionInfo info = (SessionInfo) sessionInfoBundleable.get();
this.mSessionId = info.mSessionId;
this.mDisplayType = info.mDisplayType;
} catch (BundlerException e) {
throw new IllegalArgumentException(
"Expected the SessionInfo to be encoded in the bind intent extras, but they "
+ "were encoded improperly", e);
}
}
/**
* Adds the {@link SessionInfo} and associated identifier on the passed {@code intent} to
* pass to this service on bind.
*/
public static void setBindData(@NonNull Intent intent, @NonNull SessionInfo sessionInfo) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Api29.setIdentifier(intent, sessionInfo.toString());
} else {
intent.setData(new Uri.Builder().path(sessionInfo.toString()).build());
}
try {
intent.putExtra(EXTRA_SESSION_INFO, Bundleable.create(sessionInfo));
} catch (BundlerException e) {
throw new RuntimeException(e);
}
}
// Required for Bundler
private SessionInfo() {
mSessionId = "main";
mDisplayType = DISPLAY_TYPE_MAIN;
}
/**
* Returns the set of templates that are allowed for this {@link Session}, or {@code null} if
* there are no restrictions (ie. all templates are allowed).
*/
@Nullable
@SuppressWarnings("NullableCollection") // Set does not contain nulls
@ExperimentalCarApi
public Set<Class<? extends Template>> getSupportedTemplates(
@CarAppApiLevel int carAppApiLevel) {
if (mDisplayType == DISPLAY_TYPE_CLUSTER) {
if (carAppApiLevel >= CarAppApiLevels.LEVEL_5) {
return CLUSTER_SUPPORTED_TEMPLATES_API_5;
}
return CLUSTER_SUPPORTED_TEMPLATES_LESS_THAN_API_5;
}
return null;
}
@NonNull
@Override
public String toString() {
return String.valueOf(mDisplayType) + DIVIDER + mSessionId;
}
@Override
public int hashCode() {
return Objects.hash(mSessionId, mDisplayType);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof SessionInfo)) {
return false;
}
if (obj == this) {
return true;
}
SessionInfo object = (SessionInfo) obj;
return this.getSessionId().equals(object.getSessionId())
&& this.getDisplayType() == object.getDisplayType();
}
/** Android Q method calls wrapped in a {@link RequiresApi} class to appease the compiler. */
@RequiresApi(Build.VERSION_CODES.Q)
private static class Api29 {
// Not instantiable
private Api29() {
}
/** Wrapper for {@link Intent#getIdentifier()}. */
@DoNotInline
@Nullable
static String getIdentifier(@NonNull Intent intent) {
return intent.getIdentifier();
}
/** Wrapper for {@link Intent#setIdentifier(String)}. */
@DoNotInline
static void setIdentifier(@NonNull Intent intent, @NonNull String identifier) {
intent.setIdentifier(identifier);
}
}
}