Merge "Add MacrobenchmarkRule as primary API surface" into androidx-master-dev
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt
index 01455bb..4af0b67 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt
@@ -34,8 +34,6 @@
assertEquals(10, stats.max)
assertEquals(10, stats.min)
assertEquals(0.0, stats.standardDeviation, 0.0)
- assertEquals(10, stats.percentile90)
- assertEquals(10, stats.percentile95)
}
@Test
@@ -46,8 +44,6 @@
assertEquals(10, stats.max)
assertEquals(10, stats.min)
assertEquals(Double.NaN, stats.standardDeviation, 0.0)
- assertEquals(10, stats.percentile90)
- assertEquals(10, stats.percentile95)
}
@Test
@@ -58,8 +54,6 @@
assertEquals(100, stats.max)
assertEquals(1, stats.min)
assertEquals(29.01, stats.standardDeviation, 0.05)
- assertEquals(90, stats.percentile90)
- assertEquals(95, stats.percentile95)
}
@Test
@@ -76,11 +70,4 @@
assertEquals(it.toLong(), Stats.getPercentile(listOf(0L, 25L, 50L, 75L, 100L), it))
}
}
-
- @Test
- fun fractionalPercentile() {
- val stats = Stats(longArrayOf(0L, 25L, 50L, 75L, 100L), "test")
- assertEquals(90, stats.percentile90)
- assertEquals(95, stats.percentile95)
- }
}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Stats.kt b/benchmark/common/src/main/java/androidx/benchmark/Stats.kt
index 368fbcf..7937c01 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Stats.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Stats.kt
@@ -29,8 +29,6 @@
val median: Long
val min: Long
val max: Long
- val percentile90: Long
- val percentile95: Long
val mean: Double = data.average()
val standardDeviation: Double
@@ -44,8 +42,6 @@
min = values.first()
max = values.last()
median = getPercentile(values, 50)
- percentile90 = getPercentile(values, 90)
- percentile95 = getPercentile(values, 95)
standardDeviation = if (size == 1) {
NaN
} else {
@@ -82,7 +78,7 @@
override fun hashCode(): Int {
return min.hashCode() + max.hashCode() + median.hashCode() + standardDeviation.hashCode() +
- mean.hashCode() + percentile90.hashCode() + percentile95.hashCode()
+ mean.hashCode()
}
companion object {
diff --git a/biometric/biometric/api/1.2.0-alpha01.txt b/biometric/biometric/api/1.2.0-alpha01.txt
new file mode 100644
index 0000000..2d2401f
--- /dev/null
+++ b/biometric/biometric/api/1.2.0-alpha01.txt
@@ -0,0 +1,96 @@
+// Signature format: 4.0
+package androidx.biometric {
+
+ public class BiometricManager {
+ method @Deprecated public int canAuthenticate();
+ method public int canAuthenticate(int);
+ method public static androidx.biometric.BiometricManager from(android.content.Context);
+ field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+ field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+ field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+ field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+ field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+ field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+ }
+
+ public static interface BiometricManager.Authenticators {
+ field public static final int BIOMETRIC_STRONG = 15; // 0xf
+ field public static final int BIOMETRIC_WEAK = 255; // 0xff
+ field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+ }
+
+ public class BiometricPrompt {
+ ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+ method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+ method public void cancelAuthentication();
+ field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+ field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+ field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int ERROR_CANCELED = 5; // 0x5
+ field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+ field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_LOCKOUT = 7; // 0x7
+ field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+ field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+ field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+ field public static final int ERROR_NO_SPACE = 4; // 0x4
+ field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+ field public static final int ERROR_TIMEOUT = 3; // 0x3
+ field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+ field public static final int ERROR_USER_CANCELED = 10; // 0xa
+ field public static final int ERROR_VENDOR = 8; // 0x8
+ }
+
+ public abstract static class BiometricPrompt.AuthenticationCallback {
+ ctor public BiometricPrompt.AuthenticationCallback();
+ method public void onAuthenticationError(int, CharSequence);
+ method public void onAuthenticationFailed();
+ method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+ }
+
+ public static class BiometricPrompt.AuthenticationResult {
+ method public int getAuthenticationType();
+ method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+ }
+
+ public static class BiometricPrompt.CryptoObject {
+ ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+ ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+ ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+ method public javax.crypto.Cipher? getCipher();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+ method public javax.crypto.Mac? getMac();
+ method public java.security.Signature? getSignature();
+ }
+
+ public static class BiometricPrompt.PromptInfo {
+ method public int getAllowedAuthenticators();
+ method public CharSequence? getDescription();
+ method public CharSequence getNegativeButtonText();
+ method public CharSequence? getSubtitle();
+ method public CharSequence getTitle();
+ method public boolean isConfirmationRequired();
+ method @Deprecated public boolean isDeviceCredentialAllowed();
+ }
+
+ public static class BiometricPrompt.PromptInfo.Builder {
+ ctor public BiometricPrompt.PromptInfo.Builder();
+ method public androidx.biometric.BiometricPrompt.PromptInfo build();
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+ method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+ }
+
+}
+
diff --git a/biometric/biometric/api/public_plus_experimental_1.2.0-alpha01.txt b/biometric/biometric/api/public_plus_experimental_1.2.0-alpha01.txt
new file mode 100644
index 0000000..2d2401f
--- /dev/null
+++ b/biometric/biometric/api/public_plus_experimental_1.2.0-alpha01.txt
@@ -0,0 +1,96 @@
+// Signature format: 4.0
+package androidx.biometric {
+
+ public class BiometricManager {
+ method @Deprecated public int canAuthenticate();
+ method public int canAuthenticate(int);
+ method public static androidx.biometric.BiometricManager from(android.content.Context);
+ field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+ field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+ field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+ field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+ field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+ field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+ }
+
+ public static interface BiometricManager.Authenticators {
+ field public static final int BIOMETRIC_STRONG = 15; // 0xf
+ field public static final int BIOMETRIC_WEAK = 255; // 0xff
+ field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+ }
+
+ public class BiometricPrompt {
+ ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+ method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+ method public void cancelAuthentication();
+ field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+ field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+ field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int ERROR_CANCELED = 5; // 0x5
+ field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+ field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_LOCKOUT = 7; // 0x7
+ field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+ field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+ field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+ field public static final int ERROR_NO_SPACE = 4; // 0x4
+ field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+ field public static final int ERROR_TIMEOUT = 3; // 0x3
+ field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+ field public static final int ERROR_USER_CANCELED = 10; // 0xa
+ field public static final int ERROR_VENDOR = 8; // 0x8
+ }
+
+ public abstract static class BiometricPrompt.AuthenticationCallback {
+ ctor public BiometricPrompt.AuthenticationCallback();
+ method public void onAuthenticationError(int, CharSequence);
+ method public void onAuthenticationFailed();
+ method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+ }
+
+ public static class BiometricPrompt.AuthenticationResult {
+ method public int getAuthenticationType();
+ method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+ }
+
+ public static class BiometricPrompt.CryptoObject {
+ ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+ ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+ ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+ method public javax.crypto.Cipher? getCipher();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+ method public javax.crypto.Mac? getMac();
+ method public java.security.Signature? getSignature();
+ }
+
+ public static class BiometricPrompt.PromptInfo {
+ method public int getAllowedAuthenticators();
+ method public CharSequence? getDescription();
+ method public CharSequence getNegativeButtonText();
+ method public CharSequence? getSubtitle();
+ method public CharSequence getTitle();
+ method public boolean isConfirmationRequired();
+ method @Deprecated public boolean isDeviceCredentialAllowed();
+ }
+
+ public static class BiometricPrompt.PromptInfo.Builder {
+ ctor public BiometricPrompt.PromptInfo.Builder();
+ method public androidx.biometric.BiometricPrompt.PromptInfo build();
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+ method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+ }
+
+}
+
diff --git a/biometric/biometric/api/res-1.2.0-alpha01.txt b/biometric/biometric/api/res-1.2.0-alpha01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/biometric/biometric/api/res-1.2.0-alpha01.txt
diff --git a/biometric/biometric/api/restricted_1.2.0-alpha01.txt b/biometric/biometric/api/restricted_1.2.0-alpha01.txt
new file mode 100644
index 0000000..2d2401f
--- /dev/null
+++ b/biometric/biometric/api/restricted_1.2.0-alpha01.txt
@@ -0,0 +1,96 @@
+// Signature format: 4.0
+package androidx.biometric {
+
+ public class BiometricManager {
+ method @Deprecated public int canAuthenticate();
+ method public int canAuthenticate(int);
+ method public static androidx.biometric.BiometricManager from(android.content.Context);
+ field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+ field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+ field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+ field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+ field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+ field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+ }
+
+ public static interface BiometricManager.Authenticators {
+ field public static final int BIOMETRIC_STRONG = 15; // 0xf
+ field public static final int BIOMETRIC_WEAK = 255; // 0xff
+ field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+ }
+
+ public class BiometricPrompt {
+ ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+ method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+ method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+ method public void cancelAuthentication();
+ field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+ field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+ field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int ERROR_CANCELED = 5; // 0x5
+ field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+ field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_LOCKOUT = 7; // 0x7
+ field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+ field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+ field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+ field public static final int ERROR_NO_SPACE = 4; // 0x4
+ field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+ field public static final int ERROR_TIMEOUT = 3; // 0x3
+ field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+ field public static final int ERROR_USER_CANCELED = 10; // 0xa
+ field public static final int ERROR_VENDOR = 8; // 0x8
+ }
+
+ public abstract static class BiometricPrompt.AuthenticationCallback {
+ ctor public BiometricPrompt.AuthenticationCallback();
+ method public void onAuthenticationError(int, CharSequence);
+ method public void onAuthenticationFailed();
+ method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+ }
+
+ public static class BiometricPrompt.AuthenticationResult {
+ method public int getAuthenticationType();
+ method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+ }
+
+ public static class BiometricPrompt.CryptoObject {
+ ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+ ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+ ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+ method public javax.crypto.Cipher? getCipher();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+ method public javax.crypto.Mac? getMac();
+ method public java.security.Signature? getSignature();
+ }
+
+ public static class BiometricPrompt.PromptInfo {
+ method public int getAllowedAuthenticators();
+ method public CharSequence? getDescription();
+ method public CharSequence getNegativeButtonText();
+ method public CharSequence? getSubtitle();
+ method public CharSequence getTitle();
+ method public boolean isConfirmationRequired();
+ method @Deprecated public boolean isDeviceCredentialAllowed();
+ }
+
+ public static class BiometricPrompt.PromptInfo.Builder {
+ ctor public BiometricPrompt.PromptInfo.Builder();
+ method public androidx.biometric.BiometricPrompt.PromptInfo build();
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+ method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+ }
+
+}
+
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 2bb6f18..31a5aa0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -32,7 +32,7 @@
val ASYNCLAYOUTINFLATER = Version("1.1.0-alpha01")
val AUTOFILL = Version("1.1.0-rc01")
val BENCHMARK = Version("1.1.0-alpha02")
- val BIOMETRIC = Version("1.1.0-rc01")
+ val BIOMETRIC = Version("1.2.0-alpha01")
val BROWSER = Version("1.3.0-rc01")
val BUILDSRC_TESTS = Version("1.0.0-alpha01")
val CAMERA = Version("1.0.0-beta12")
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 8839e7a..bfa3df9 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -111,6 +111,7 @@
method public final androidx.lifecycle.Lifecycle getLifecycle();
method public String? getMarker();
method public final androidx.car.app.ScreenManager getScreenManager();
+ method public abstract androidx.car.app.model.Template getTemplate();
method public final void invalidate();
method public void setMarker(String?);
method public void setResult(Object?);
@@ -152,6 +153,885 @@
}
+package androidx.car.app.model {
+
+ public final class Action {
+ method public static androidx.car.app.model.Action.Builder builder();
+ method public androidx.car.app.model.CarColor getBackgroundColor();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public int getType();
+ method public boolean isStandard();
+ method public androidx.car.app.model.Action.Builder newBuilder();
+ method public static String typeToString(int);
+ field public static final androidx.car.app.model.Action APP_ICON;
+ field public static final androidx.car.app.model.Action BACK;
+ field public static final int TYPE_APP_ICON = 65538; // 0x10002
+ field public static final int TYPE_BACK = 65539; // 0x10003
+ field public static final int TYPE_CUSTOM = 1; // 0x1
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class Action.Builder {
+ method public androidx.car.app.model.Action build();
+ method public androidx.car.app.model.Action.Builder setBackgroundColor(androidx.car.app.model.CarColor);
+ method public androidx.car.app.model.Action.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.Action.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.Action.Builder setTitle(CharSequence?);
+ }
+
+ public class ActionList {
+ method public static androidx.car.app.model.ActionList create(java.util.List<androidx.car.app.model.Action!>);
+ method public java.util.List<androidx.car.app.model.Action!> getList();
+ }
+
+ public class ActionStrip {
+ method public static androidx.car.app.model.ActionStrip.Builder builder();
+ method public androidx.car.app.model.Action? getActionOfType(int);
+ method public java.util.List<java.lang.Object!> getActions();
+ }
+
+ public static final class ActionStrip.Builder {
+ ctor public ActionStrip.Builder();
+ method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
+ method public androidx.car.app.model.ActionStrip build();
+ method public androidx.car.app.model.ActionStrip.Builder clearActions();
+ }
+
+ public class CarColor {
+ method public static androidx.car.app.model.CarColor createCustom(@ColorInt int, @ColorInt int);
+ method @ColorInt public int getColor();
+ method @ColorInt public int getColorDark();
+ method public int getType();
+ field public static final androidx.car.app.model.CarColor BLUE;
+ field public static final androidx.car.app.model.CarColor DEFAULT;
+ field public static final androidx.car.app.model.CarColor GREEN;
+ field public static final androidx.car.app.model.CarColor PRIMARY;
+ field public static final androidx.car.app.model.CarColor RED;
+ field public static final androidx.car.app.model.CarColor SECONDARY;
+ field public static final int TYPE_BLUE = 6; // 0x6
+ field public static final int TYPE_CUSTOM = 0; // 0x0
+ field public static final int TYPE_DEFAULT = 1; // 0x1
+ field public static final int TYPE_GREEN = 5; // 0x5
+ field public static final int TYPE_PRIMARY = 2; // 0x2
+ field public static final int TYPE_RED = 4; // 0x4
+ field public static final int TYPE_SECONDARY = 3; // 0x3
+ field public static final int TYPE_YELLOW = 7; // 0x7
+ field public static final androidx.car.app.model.CarColor YELLOW;
+ }
+
+ public class CarIcon {
+ method public static androidx.car.app.model.CarIcon.Builder builder(androidx.core.graphics.drawable.IconCompat);
+ method public androidx.core.graphics.drawable.IconCompat? getIcon();
+ method public androidx.car.app.model.CarColor? getTint();
+ method public int getType();
+ method public androidx.car.app.model.CarIcon.Builder newBuilder();
+ method public static androidx.car.app.model.CarIcon of(androidx.core.graphics.drawable.IconCompat);
+ field public static final androidx.car.app.model.CarIcon ALERT;
+ field public static final androidx.car.app.model.CarIcon APP_ICON;
+ field public static final androidx.car.app.model.CarIcon BACK;
+ field public static final androidx.car.app.model.CarIcon ERROR;
+ field public static final int TYPE_ALERT = 4; // 0x4
+ field public static final int TYPE_APP = 5; // 0x5
+ field public static final int TYPE_BACK = 3; // 0x3
+ field public static final int TYPE_CUSTOM = 1; // 0x1
+ field public static final int TYPE_ERROR = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
+ field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
+ }
+
+ public static final class CarIcon.Builder {
+ method public androidx.car.app.model.CarIcon build();
+ method public androidx.car.app.model.CarIcon.Builder setIcon(androidx.car.app.model.CarIcon);
+ method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
+ }
+
+ public class CarIconSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon);
+ method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon, int);
+ method public int getAlignment();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public void updateDrawState(android.text.TextPaint?);
+ method public static int validateAlignment(int);
+ field public static final int ALIGN_BASELINE = 1; // 0x1
+ field public static final int ALIGN_BOTTOM = 0; // 0x0
+ field public static final int ALIGN_CENTER = 2; // 0x2
+ }
+
+ public class CarText {
+ ctor public CarText();
+ method public static androidx.car.app.model.CarText create(CharSequence);
+ method public java.util.List<androidx.car.app.model.CarText.SpanWrapper!> getSpans();
+ method public String? getText();
+ method public boolean isEmpty();
+ method public static boolean isNullOrEmpty(androidx.car.app.model.CarText?);
+ method public static String? toShortString(androidx.car.app.model.CarText?);
+ field public static final androidx.car.app.model.CarText EMPTY;
+ }
+
+ public static class CarText.SpanWrapper {
+ field @Keep public final int end;
+ field @Keep public final int flags;
+ field @Keep public final Object? span;
+ field @Keep public final int start;
+ }
+
+ public class DateTimeWithZone {
+ method public static androidx.car.app.model.DateTimeWithZone create(long, int, String);
+ method public static androidx.car.app.model.DateTimeWithZone create(long, java.util.TimeZone);
+ method @RequiresApi(26) public static androidx.car.app.model.DateTimeWithZone create(java.time.ZonedDateTime);
+ method public long getTimeSinceEpochMillis();
+ method public int getZoneOffsetSeconds();
+ method public String? getZoneShortName();
+ }
+
+ public final class Distance {
+ method public static androidx.car.app.model.Distance create(double, int);
+ method public double getDisplayDistance();
+ method public int getDisplayUnit();
+ field public static final int UNIT_FEET = 6; // 0x6
+ field public static final int UNIT_KILOMETERS = 2; // 0x2
+ field public static final int UNIT_KILOMETERS_P1 = 3; // 0x3
+ field public static final int UNIT_METERS = 1; // 0x1
+ field public static final int UNIT_MILES = 4; // 0x4
+ field public static final int UNIT_MILES_P1 = 5; // 0x5
+ field public static final int UNIT_YARDS = 7; // 0x7
+ }
+
+ public class DistanceSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.DistanceSpan create(androidx.car.app.model.Distance);
+ method public androidx.car.app.model.Distance getDistance();
+ method public void updateDrawState(android.text.TextPaint?);
+ }
+
+ public class DurationSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.DurationSpan create(long);
+ method @RequiresApi(26) public static androidx.car.app.model.DurationSpan create(java.time.Duration);
+ method public long getDurationSeconds();
+ method public void updateDrawState(android.text.TextPaint?);
+ }
+
+ public class ForegroundCarColorSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.ForegroundCarColorSpan create(androidx.car.app.model.CarColor);
+ method public androidx.car.app.model.CarColor getColor();
+ method public void updateDrawState(android.text.TextPaint);
+ }
+
+ public class GridItem implements androidx.car.app.model.Item {
+ method public static androidx.car.app.model.GridItem.Builder builder();
+ method public androidx.car.app.model.CarIcon getImage();
+ method public int getImageType();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public androidx.car.app.model.CarText? getText();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public androidx.car.app.model.Toggle? getToggle();
+ field public static final int IMAGE_TYPE_ICON = 1; // 0x1
+ field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+ }
+
+ public static final class GridItem.Builder {
+ method public androidx.car.app.model.GridItem build();
+ method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon);
+ method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
+ }
+
+ public final class GridTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.GridTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.CarIcon? getBackgroundImage();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getSingleList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class GridTemplate.Builder {
+ method public androidx.car.app.model.GridTemplate build();
+ method public androidx.car.app.model.GridTemplate.Builder clearAllLists();
+ method public androidx.car.app.model.GridTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.GridTemplate.Builder setBackgroundImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.GridTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.GridTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.GridTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+ method public androidx.car.app.model.GridTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public interface Item {
+ }
+
+ public final class ItemList {
+ method public static androidx.car.app.model.ItemList.Builder builder();
+ method public java.util.List<java.lang.Object!> getItems();
+ method public androidx.car.app.model.CarText? getNoItemsMessage();
+ method public int getSelectedIndex();
+ method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
+ }
+
+ public static final class ItemList.Builder {
+ ctor public ItemList.Builder();
+ method public androidx.car.app.model.ItemList.Builder addItem(androidx.car.app.model.Item);
+ method public androidx.car.app.model.ItemList build();
+ method public androidx.car.app.model.ItemList.Builder clearItems();
+ method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
+ method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+ method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+ method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
+ }
+
+ public static interface ItemList.OnItemVisibilityChangedListener {
+ method public void onItemVisibilityChanged(int, int);
+ }
+
+ public static interface ItemList.OnSelectedListener {
+ method public void onSelected(int);
+ }
+
+ public final class LatLng {
+ method public static androidx.car.app.model.LatLng create(double, double);
+ method public static androidx.car.app.model.LatLng create(android.location.Location);
+ method public double getLatitude();
+ method public double getLongitude();
+ }
+
+ public final class ListTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.ListTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public java.util.List<androidx.car.app.model.SectionedItemList!> getSectionLists();
+ method public androidx.car.app.model.ItemList? getSingleList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class ListTemplate.Builder {
+ method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
+ method public androidx.car.app.model.ListTemplate build();
+ method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
+ method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.ListTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+ method public androidx.car.app.model.ListTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class MessageTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
+ method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.CarText? getDebugMessage();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public androidx.car.app.model.CarText getMessage();
+ method public androidx.car.app.model.CarText? getTitle();
+ }
+
+ public static final class MessageTemplate.Builder {
+ method public androidx.car.app.model.MessageTemplate build();
+ method public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
+ method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
+ method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class Metadata {
+ method public static androidx.car.app.model.Metadata.Builder builder();
+ method public androidx.car.app.model.Place? getPlace();
+ method public androidx.car.app.model.Metadata.Builder newBuilder();
+ method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
+ field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
+ }
+
+ public static final class Metadata.Builder {
+ method public androidx.car.app.model.Metadata build();
+ method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
+ }
+
+ public interface OnClickListener {
+ method public void onClick();
+ }
+
+ public class OnClickListenerWrapper {
+ method public boolean isParkedOnly();
+ }
+
+ public final class Pane {
+ method public static androidx.car.app.model.Pane.Builder builder();
+ method public androidx.car.app.model.ActionList? getActionList();
+ method public java.util.List<java.lang.Object!> getRows();
+ method public boolean isLoading();
+ method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
+ }
+
+ public static final class Pane.Builder {
+ ctor public Pane.Builder();
+ method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
+ method public androidx.car.app.model.Pane build();
+ method public androidx.car.app.model.Pane.Builder clearRows();
+ method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method public androidx.car.app.model.Pane.Builder setLoading(boolean);
+ }
+
+ public final class PaneTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.PaneTemplate.Builder builder(androidx.car.app.model.Pane);
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.Pane getPane();
+ method public androidx.car.app.model.CarText? getTitle();
+ }
+
+ public static final class PaneTemplate.Builder {
+ method public androidx.car.app.model.PaneTemplate build();
+ method public androidx.car.app.model.PaneTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.PaneTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.PaneTemplate.Builder setPane(androidx.car.app.model.Pane);
+ method public androidx.car.app.model.PaneTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class ParkedOnlyOnClickListener implements androidx.car.app.model.OnClickListener {
+ method public static androidx.car.app.model.ParkedOnlyOnClickListener create(androidx.car.app.model.OnClickListener);
+ method public void onClick();
+ }
+
+ public class Place {
+ method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
+ method public androidx.car.app.model.LatLng getLatLng();
+ method public androidx.car.app.model.PlaceMarker? getMarker();
+ method public androidx.car.app.model.Place.Builder newBuilder();
+ }
+
+ public static final class Place.Builder {
+ method public androidx.car.app.model.Place build();
+ method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
+ method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
+ }
+
+ public final class PlaceListMapTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.PlaceListMapTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Place? getAnchor();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isCurrentLocationEnabled();
+ method public boolean isLoading();
+ }
+
+ public static final class PlaceListMapTemplate.Builder {
+ ctor public PlaceListMapTemplate.Builder();
+ method public androidx.car.app.model.PlaceListMapTemplate build();
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setAnchor(androidx.car.app.model.Place?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setCurrentLocationEnabled(boolean);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class PlaceMarker {
+ method public static androidx.car.app.model.PlaceMarker.Builder builder();
+ method public androidx.car.app.model.CarColor? getColor();
+ method public static androidx.car.app.model.PlaceMarker getDefault();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public int getIconType();
+ method public androidx.car.app.model.CarText? getLabel();
+ method public static boolean isDefaultMarker(androidx.car.app.model.PlaceMarker?);
+ field public static final int TYPE_ICON = 0; // 0x0
+ field public static final int TYPE_IMAGE = 1; // 0x1
+ }
+
+ public static final class PlaceMarker.Builder {
+ method public androidx.car.app.model.PlaceMarker build();
+ method public androidx.car.app.model.PlaceMarker.Builder setColor(androidx.car.app.model.CarColor?);
+ method public androidx.car.app.model.PlaceMarker.Builder setIcon(androidx.car.app.model.CarIcon?, int);
+ method public androidx.car.app.model.PlaceMarker.Builder setLabel(CharSequence?);
+ }
+
+ public class Row implements androidx.car.app.model.Item {
+ method public static androidx.car.app.model.Row.Builder builder();
+ method public int getFlags();
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.Metadata getMetadata();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public int getRowImageType();
+ method public java.util.List<androidx.car.app.model.CarText!> getTexts();
+ method public androidx.car.app.model.CarText getTitle();
+ method public androidx.car.app.model.Toggle? getToggle();
+ method public boolean isBrowsable();
+ method public androidx.car.app.model.Row row();
+ method public void yourBoat();
+ field public static final int IMAGE_TYPE_ICON = 4; // 0x4
+ field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+ field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+ field public static final int ROW_FLAG_NONE = 1; // 0x1
+ field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
+ field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
+ }
+
+ public static final class Row.Builder {
+ method public androidx.car.app.model.Row.Builder addText(CharSequence);
+ method public androidx.car.app.model.Row build();
+ method public androidx.car.app.model.Row.Builder clearText();
+ method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
+ method public androidx.car.app.model.Row.Builder setFlags(int);
+ method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
+ method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+ method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
+ method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
+ }
+
+ public final class SearchTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public String? getInitialSearchText();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public String? getSearchHint();
+ method public boolean isLoading();
+ method public boolean isShowKeyboardByDefault();
+ }
+
+ public static final class SearchTemplate.Builder {
+ method public androidx.car.app.model.SearchTemplate build();
+ method public androidx.car.app.model.SearchTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.SearchTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.SearchTemplate.Builder setInitialSearchText(String?);
+ method public androidx.car.app.model.SearchTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.model.SearchTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.SearchTemplate.Builder setSearchHint(String?);
+ method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
+ }
+
+ public class SectionedItemList {
+ method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
+ method public androidx.car.app.model.CarText getHeader();
+ method public androidx.car.app.model.ItemList getItemList();
+ }
+
+ public interface Template {
+ method public default void checkPermissions(android.content.Context);
+ method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
+ }
+
+ public final class TemplateInfo {
+ ctor public TemplateInfo(androidx.car.app.model.Template, String);
+ method public Class<? extends androidx.car.app.model.Template> getTemplateClass();
+ method public String getTemplateId();
+ }
+
+ public final class TemplateWrapper {
+ method public static androidx.car.app.model.TemplateWrapper copyOf(androidx.car.app.model.TemplateWrapper);
+ method public int getCurrentTaskStep();
+ method public String getId();
+ method public androidx.car.app.model.Template getTemplate();
+ method public java.util.List<androidx.car.app.model.TemplateInfo!>? getTemplateInfosForScreenStack();
+ method public boolean isRefresh();
+ method public void setCurrentTaskStep(int);
+ method public void setId(String);
+ method public void setRefresh(boolean);
+ method public void setTemplate(androidx.car.app.model.Template);
+ method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template);
+ method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template, String);
+ }
+
+ public class Toggle {
+ method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+ method public boolean isChecked();
+ }
+
+ public static final class Toggle.Builder {
+ method public androidx.car.app.model.Toggle build();
+ method public androidx.car.app.model.Toggle.Builder setChecked(boolean);
+ method public androidx.car.app.model.Toggle.Builder setCheckedChangeListener(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+ }
+
+ public static interface Toggle.OnCheckedChangeListener {
+ method public void onCheckedChange(boolean);
+ }
+
+}
+
+package androidx.car.app.model.constraints {
+
+ public class ActionsConstraints {
+ method @VisibleForTesting public static androidx.car.app.model.constraints.ActionsConstraints.Builder builder();
+ method public java.util.Set<java.lang.Integer!> getDisallowedActionTypes();
+ method public int getMaxActions();
+ method public int getMaxCustomTitles();
+ method public java.util.Set<java.lang.Integer!> getRequiredActionTypes();
+ method @VisibleForTesting public androidx.car.app.model.constraints.ActionsConstraints.Builder newBuilder();
+ method public void validateOrThrow(java.util.List<java.lang.Object!>);
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_HEADER;
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION;
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE;
+ }
+
+ @VisibleForTesting public static final class ActionsConstraints.Builder {
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder addDisallowedActionType(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder addRequiredActionType(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints build();
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxActions(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxCustomTitles(int);
+ }
+
+ public class CarColorConstraints {
+ method public void validateOrThrow(androidx.car.app.model.CarColor);
+ field public static final androidx.car.app.model.constraints.CarColorConstraints STANDARD_ONLY;
+ field public static final androidx.car.app.model.constraints.CarColorConstraints UNCONSTRAINED;
+ }
+
+ public class CarIconConstraints {
+ method public androidx.core.graphics.drawable.IconCompat checkSupportedIcon(androidx.core.graphics.drawable.IconCompat);
+ method public void validateOrThrow(androidx.car.app.model.CarIcon?);
+ field public static final androidx.car.app.model.constraints.CarIconConstraints DEFAULT;
+ field public static final androidx.car.app.model.constraints.CarIconConstraints UNCONSTRAINED;
+ }
+
+ public class RowConstraints {
+ method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
+ method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
+ method public int getFlagOverrides();
+ method public int getMaxActionsExclusive();
+ method public int getMaxTextLinesPerRow();
+ method public boolean isImageAllowed();
+ method public boolean isOnClickListenerAllowed();
+ method public boolean isToggleAllowed();
+ method public androidx.car.app.model.constraints.RowConstraints.Builder newBuilder();
+ method public void validateOrThrow(Object);
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_CONSERVATIVE;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_FULL_LIST;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_PANE;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_SIMPLE;
+ field public static final androidx.car.app.model.constraints.RowConstraints UNCONSTRAINED;
+ }
+
+ public static final class RowConstraints.Builder {
+ method public androidx.car.app.model.constraints.RowConstraints build();
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setOnClickListenerAllowed(boolean);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setToggleAllowed(boolean);
+ }
+
+ public class RowListConstraints {
+ method public static androidx.car.app.model.constraints.RowListConstraints.Builder builder();
+ method public int getMaxActions();
+ method public androidx.car.app.model.constraints.RowConstraints getRowConstraints();
+ method public int getRowListType();
+ method public boolean isAllowSelectableLists();
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder newBuilder();
+ method public void validateOrThrow(androidx.car.app.model.ItemList);
+ method public void validateOrThrow(java.util.List<androidx.car.app.model.SectionedItemList!>);
+ method public void validateOrThrow(androidx.car.app.model.Pane);
+ field public static final int DEFAULT_LIST = 0; // 0x0
+ field public static final int PANE = 1; // 0x1
+ field public static final int ROUTE_PREVIEW = 2; // 0x2
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_PANE;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE;
+ }
+
+ public static final class RowListConstraints.Builder {
+ method public androidx.car.app.model.constraints.RowListConstraints build();
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setAllowSelectableLists(boolean);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setMaxActions(int);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowConstraints(androidx.car.app.model.constraints.RowConstraints);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowListType(int);
+ }
+
+}
+
+package androidx.car.app.navigation {
+
+ public class NavigationManager {
+ method @MainThread public void navigationEnded();
+ method @MainThread public void navigationStarted();
+ method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+ method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
+ }
+
+ public interface NavigationManagerListener {
+ method public void onAutoDriveEnabled();
+ method public void stopNavigation();
+ }
+
+}
+
+package androidx.car.app.navigation.model {
+
+ public final class Destination {
+ method public static androidx.car.app.navigation.model.Destination.Builder builder(CharSequence, CharSequence);
+ method public static androidx.car.app.navigation.model.Destination.Builder builder();
+ method public androidx.car.app.model.CarText? getAddress();
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.CarText? getName();
+ }
+
+ public static final class Destination.Builder {
+ method public androidx.car.app.navigation.model.Destination build();
+ method public androidx.car.app.navigation.model.Destination.Builder setAddress(CharSequence?);
+ method public androidx.car.app.navigation.model.Destination.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Destination.Builder setName(CharSequence?);
+ }
+
+ public final class Lane {
+ method public static androidx.car.app.navigation.model.Lane.Builder builder();
+ method public java.util.List<androidx.car.app.navigation.model.LaneDirection!> getDirections();
+ }
+
+ public static final class Lane.Builder {
+ ctor public Lane.Builder();
+ method public androidx.car.app.navigation.model.Lane.Builder addDirection(androidx.car.app.navigation.model.LaneDirection);
+ method public androidx.car.app.navigation.model.Lane build();
+ method public androidx.car.app.navigation.model.Lane.Builder clearDirections();
+ }
+
+ public final class LaneDirection {
+ method public static androidx.car.app.navigation.model.LaneDirection create(int, boolean);
+ method public int getShape();
+ method public boolean isHighlighted();
+ field public static final int SHAPE_NORMAL_LEFT = 5; // 0x5
+ field public static final int SHAPE_NORMAL_RIGHT = 6; // 0x6
+ field public static final int SHAPE_SHARP_LEFT = 7; // 0x7
+ field public static final int SHAPE_SHARP_RIGHT = 8; // 0x8
+ field public static final int SHAPE_SLIGHT_LEFT = 3; // 0x3
+ field public static final int SHAPE_SLIGHT_RIGHT = 4; // 0x4
+ field public static final int SHAPE_STRAIGHT = 2; // 0x2
+ field public static final int SHAPE_UNKNOWN = 1; // 0x1
+ field public static final int SHAPE_U_TURN_LEFT = 9; // 0x9
+ field public static final int SHAPE_U_TURN_RIGHT = 10; // 0xa
+ }
+
+ public final class Maneuver {
+ method public static androidx.car.app.navigation.model.Maneuver.Builder builder(int);
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public int getRoundaboutExitAngle();
+ method public int getRoundaboutExitNumber();
+ method public int getType();
+ field public static final int TYPE_DEPART = 1; // 0x1
+ field public static final int TYPE_DESTINATION = 39; // 0x27
+ field public static final int TYPE_DESTINATION_LEFT = 41; // 0x29
+ field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
+ field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
+ field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+ field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+ field public static final int TYPE_FORK_LEFT = 25; // 0x19
+ field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
+ field public static final int TYPE_KEEP_LEFT = 3; // 0x3
+ field public static final int TYPE_KEEP_RIGHT = 4; // 0x4
+ field public static final int TYPE_MERGE_LEFT = 27; // 0x1b
+ field public static final int TYPE_MERGE_RIGHT = 28; // 0x1c
+ field public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29; // 0x1d
+ field public static final int TYPE_NAME_CHANGE = 2; // 0x2
+ field public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23; // 0x17
+ field public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24; // 0x18
+ field public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21; // 0x15
+ field public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22; // 0x16
+ field public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15; // 0xf
+ field public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16; // 0x10
+ field public static final int TYPE_ON_RAMP_SHARP_LEFT = 17; // 0x11
+ field public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18; // 0x12
+ field public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13; // 0xd
+ field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
+ field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
+ field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
+ field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
+ field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+ field public static final int TYPE_STRAIGHT = 36; // 0x24
+ field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
+ field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
+ field public static final int TYPE_TURN_SHARP_LEFT = 9; // 0x9
+ field public static final int TYPE_TURN_SHARP_RIGHT = 10; // 0xa
+ field public static final int TYPE_TURN_SLIGHT_LEFT = 5; // 0x5
+ field public static final int TYPE_TURN_SLIGHT_RIGHT = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_U_TURN_LEFT = 11; // 0xb
+ field public static final int TYPE_U_TURN_RIGHT = 12; // 0xc
+ }
+
+ public static final class Maneuver.Builder {
+ method public androidx.car.app.navigation.model.Maneuver build();
+ method public androidx.car.app.navigation.model.Maneuver.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitAngle(int);
+ method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitNumber(int);
+ }
+
+ public class MessageInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+ method public static androidx.car.app.navigation.model.MessageInfo.Builder builder(CharSequence);
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.CarText? getText();
+ method public androidx.car.app.model.CarText getTitle();
+ }
+
+ public static final class MessageInfo.Builder {
+ method public androidx.car.app.navigation.model.MessageInfo build();
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setText(CharSequence?);
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setTitle(CharSequence);
+ }
+
+ public class NavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.NavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip getActionStrip();
+ method public androidx.car.app.model.CarColor? getBackgroundColor();
+ method public androidx.car.app.navigation.model.TravelEstimate? getDestinationTravelEstimate();
+ method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? getNavigationInfo();
+ }
+
+ public static final class NavigationTemplate.Builder {
+ method public androidx.car.app.navigation.model.NavigationTemplate build();
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setBackgroundColor(androidx.car.app.model.CarColor?);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate?);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setNavigationInfo(androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo?);
+ }
+
+ public static interface NavigationTemplate.NavigationInfo {
+ }
+
+ public final class PlaceListNavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class PlaceListNavigationTemplate.Builder {
+ ctor public PlaceListNavigationTemplate.Builder();
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class RoutePreviewNavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.Action? getNavigateAction();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class RoutePreviewNavigationTemplate.Builder {
+ ctor public RoutePreviewNavigationTemplate.Builder();
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class RoutingInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+ method public static androidx.car.app.navigation.model.RoutingInfo.Builder builder();
+ method public androidx.car.app.model.Distance? getCurrentDistance();
+ method public androidx.car.app.navigation.model.Step? getCurrentStep();
+ method public androidx.car.app.model.CarIcon? getJunctionImage();
+ method public androidx.car.app.navigation.model.Step? getNextStep();
+ method public boolean isLoading();
+ }
+
+ public static final class RoutingInfo.Builder {
+ method public androidx.car.app.navigation.model.RoutingInfo build();
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
+ }
+
+ public final class Step {
+ method public static androidx.car.app.navigation.model.Step.Builder builder(CharSequence);
+ method public androidx.car.app.model.CarText? getCue();
+ method public java.util.List<androidx.car.app.navigation.model.Lane!> getLanes();
+ method public androidx.car.app.model.CarIcon? getLanesImage();
+ method public androidx.car.app.navigation.model.Maneuver? getManeuver();
+ method public androidx.car.app.model.CarText? getRoad();
+ method public androidx.car.app.navigation.model.Step.Builder newBuilder();
+ }
+
+ public static final class Step.Builder {
+ method public androidx.car.app.navigation.model.Step.Builder addLane(androidx.car.app.navigation.model.Lane);
+ method public androidx.car.app.navigation.model.Step build();
+ method public androidx.car.app.navigation.model.Step.Builder clearLanes();
+ method public androidx.car.app.navigation.model.Step.Builder setCue(CharSequence);
+ method public androidx.car.app.navigation.model.Step.Builder setLanesImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Step.Builder setManeuver(androidx.car.app.navigation.model.Maneuver?);
+ method public androidx.car.app.navigation.model.Step.Builder setRoad(CharSequence);
+ }
+
+ public final class TravelEstimate {
+ method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+ method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+ method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+ method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+ method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
+ method public androidx.car.app.model.Distance getRemainingDistance();
+ method public androidx.car.app.model.CarColor getRemainingDistanceColor();
+ method public androidx.car.app.model.CarColor getRemainingTimeColor();
+ method public long getRemainingTimeSeconds();
+ }
+
+ public static final class TravelEstimate.Builder {
+ method public androidx.car.app.navigation.model.TravelEstimate build();
+ method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+ method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+ }
+
+ public final class Trip {
+ method public static androidx.car.app.navigation.model.Trip.Builder builder();
+ method public androidx.car.app.model.CarText? getCurrentRoad();
+ method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getDestinationTravelEstimates();
+ method public java.util.List<androidx.car.app.navigation.model.Destination!> getDestinations();
+ method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getStepTravelEstimates();
+ method public java.util.List<androidx.car.app.navigation.model.Step!> getSteps();
+ method public boolean isLoading();
+ }
+
+ public static final class Trip.Builder {
+ ctor public Trip.Builder();
+ method public androidx.car.app.navigation.model.Trip.Builder addDestination(androidx.car.app.navigation.model.Destination);
+ method public androidx.car.app.navigation.model.Trip.Builder addDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+ method public androidx.car.app.navigation.model.Trip.Builder addStep(androidx.car.app.navigation.model.Step?);
+ method public androidx.car.app.navigation.model.Trip.Builder addStepTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+ method public androidx.car.app.navigation.model.Trip build();
+ method public androidx.car.app.navigation.model.Trip.Builder clearDestinationTravelEstimates();
+ method public androidx.car.app.navigation.model.Trip.Builder clearDestinations();
+ method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
+ method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
+ method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
+ method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+ }
+
+}
+
package androidx.car.app.serialization {
public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1051,10 @@
package androidx.car.app.utils {
+ public interface Logger {
+ method public void log(String);
+ }
+
public class ThreadUtils {
method public static void checkMainThread();
method public static void runOnMain(Runnable);
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 8839e7a..bfa3df9 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -111,6 +111,7 @@
method public final androidx.lifecycle.Lifecycle getLifecycle();
method public String? getMarker();
method public final androidx.car.app.ScreenManager getScreenManager();
+ method public abstract androidx.car.app.model.Template getTemplate();
method public final void invalidate();
method public void setMarker(String?);
method public void setResult(Object?);
@@ -152,6 +153,885 @@
}
+package androidx.car.app.model {
+
+ public final class Action {
+ method public static androidx.car.app.model.Action.Builder builder();
+ method public androidx.car.app.model.CarColor getBackgroundColor();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public int getType();
+ method public boolean isStandard();
+ method public androidx.car.app.model.Action.Builder newBuilder();
+ method public static String typeToString(int);
+ field public static final androidx.car.app.model.Action APP_ICON;
+ field public static final androidx.car.app.model.Action BACK;
+ field public static final int TYPE_APP_ICON = 65538; // 0x10002
+ field public static final int TYPE_BACK = 65539; // 0x10003
+ field public static final int TYPE_CUSTOM = 1; // 0x1
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class Action.Builder {
+ method public androidx.car.app.model.Action build();
+ method public androidx.car.app.model.Action.Builder setBackgroundColor(androidx.car.app.model.CarColor);
+ method public androidx.car.app.model.Action.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.Action.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.Action.Builder setTitle(CharSequence?);
+ }
+
+ public class ActionList {
+ method public static androidx.car.app.model.ActionList create(java.util.List<androidx.car.app.model.Action!>);
+ method public java.util.List<androidx.car.app.model.Action!> getList();
+ }
+
+ public class ActionStrip {
+ method public static androidx.car.app.model.ActionStrip.Builder builder();
+ method public androidx.car.app.model.Action? getActionOfType(int);
+ method public java.util.List<java.lang.Object!> getActions();
+ }
+
+ public static final class ActionStrip.Builder {
+ ctor public ActionStrip.Builder();
+ method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
+ method public androidx.car.app.model.ActionStrip build();
+ method public androidx.car.app.model.ActionStrip.Builder clearActions();
+ }
+
+ public class CarColor {
+ method public static androidx.car.app.model.CarColor createCustom(@ColorInt int, @ColorInt int);
+ method @ColorInt public int getColor();
+ method @ColorInt public int getColorDark();
+ method public int getType();
+ field public static final androidx.car.app.model.CarColor BLUE;
+ field public static final androidx.car.app.model.CarColor DEFAULT;
+ field public static final androidx.car.app.model.CarColor GREEN;
+ field public static final androidx.car.app.model.CarColor PRIMARY;
+ field public static final androidx.car.app.model.CarColor RED;
+ field public static final androidx.car.app.model.CarColor SECONDARY;
+ field public static final int TYPE_BLUE = 6; // 0x6
+ field public static final int TYPE_CUSTOM = 0; // 0x0
+ field public static final int TYPE_DEFAULT = 1; // 0x1
+ field public static final int TYPE_GREEN = 5; // 0x5
+ field public static final int TYPE_PRIMARY = 2; // 0x2
+ field public static final int TYPE_RED = 4; // 0x4
+ field public static final int TYPE_SECONDARY = 3; // 0x3
+ field public static final int TYPE_YELLOW = 7; // 0x7
+ field public static final androidx.car.app.model.CarColor YELLOW;
+ }
+
+ public class CarIcon {
+ method public static androidx.car.app.model.CarIcon.Builder builder(androidx.core.graphics.drawable.IconCompat);
+ method public androidx.core.graphics.drawable.IconCompat? getIcon();
+ method public androidx.car.app.model.CarColor? getTint();
+ method public int getType();
+ method public androidx.car.app.model.CarIcon.Builder newBuilder();
+ method public static androidx.car.app.model.CarIcon of(androidx.core.graphics.drawable.IconCompat);
+ field public static final androidx.car.app.model.CarIcon ALERT;
+ field public static final androidx.car.app.model.CarIcon APP_ICON;
+ field public static final androidx.car.app.model.CarIcon BACK;
+ field public static final androidx.car.app.model.CarIcon ERROR;
+ field public static final int TYPE_ALERT = 4; // 0x4
+ field public static final int TYPE_APP = 5; // 0x5
+ field public static final int TYPE_BACK = 3; // 0x3
+ field public static final int TYPE_CUSTOM = 1; // 0x1
+ field public static final int TYPE_ERROR = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
+ field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
+ }
+
+ public static final class CarIcon.Builder {
+ method public androidx.car.app.model.CarIcon build();
+ method public androidx.car.app.model.CarIcon.Builder setIcon(androidx.car.app.model.CarIcon);
+ method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
+ }
+
+ public class CarIconSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon);
+ method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon, int);
+ method public int getAlignment();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public void updateDrawState(android.text.TextPaint?);
+ method public static int validateAlignment(int);
+ field public static final int ALIGN_BASELINE = 1; // 0x1
+ field public static final int ALIGN_BOTTOM = 0; // 0x0
+ field public static final int ALIGN_CENTER = 2; // 0x2
+ }
+
+ public class CarText {
+ ctor public CarText();
+ method public static androidx.car.app.model.CarText create(CharSequence);
+ method public java.util.List<androidx.car.app.model.CarText.SpanWrapper!> getSpans();
+ method public String? getText();
+ method public boolean isEmpty();
+ method public static boolean isNullOrEmpty(androidx.car.app.model.CarText?);
+ method public static String? toShortString(androidx.car.app.model.CarText?);
+ field public static final androidx.car.app.model.CarText EMPTY;
+ }
+
+ public static class CarText.SpanWrapper {
+ field @Keep public final int end;
+ field @Keep public final int flags;
+ field @Keep public final Object? span;
+ field @Keep public final int start;
+ }
+
+ public class DateTimeWithZone {
+ method public static androidx.car.app.model.DateTimeWithZone create(long, int, String);
+ method public static androidx.car.app.model.DateTimeWithZone create(long, java.util.TimeZone);
+ method @RequiresApi(26) public static androidx.car.app.model.DateTimeWithZone create(java.time.ZonedDateTime);
+ method public long getTimeSinceEpochMillis();
+ method public int getZoneOffsetSeconds();
+ method public String? getZoneShortName();
+ }
+
+ public final class Distance {
+ method public static androidx.car.app.model.Distance create(double, int);
+ method public double getDisplayDistance();
+ method public int getDisplayUnit();
+ field public static final int UNIT_FEET = 6; // 0x6
+ field public static final int UNIT_KILOMETERS = 2; // 0x2
+ field public static final int UNIT_KILOMETERS_P1 = 3; // 0x3
+ field public static final int UNIT_METERS = 1; // 0x1
+ field public static final int UNIT_MILES = 4; // 0x4
+ field public static final int UNIT_MILES_P1 = 5; // 0x5
+ field public static final int UNIT_YARDS = 7; // 0x7
+ }
+
+ public class DistanceSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.DistanceSpan create(androidx.car.app.model.Distance);
+ method public androidx.car.app.model.Distance getDistance();
+ method public void updateDrawState(android.text.TextPaint?);
+ }
+
+ public class DurationSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.DurationSpan create(long);
+ method @RequiresApi(26) public static androidx.car.app.model.DurationSpan create(java.time.Duration);
+ method public long getDurationSeconds();
+ method public void updateDrawState(android.text.TextPaint?);
+ }
+
+ public class ForegroundCarColorSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.ForegroundCarColorSpan create(androidx.car.app.model.CarColor);
+ method public androidx.car.app.model.CarColor getColor();
+ method public void updateDrawState(android.text.TextPaint);
+ }
+
+ public class GridItem implements androidx.car.app.model.Item {
+ method public static androidx.car.app.model.GridItem.Builder builder();
+ method public androidx.car.app.model.CarIcon getImage();
+ method public int getImageType();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public androidx.car.app.model.CarText? getText();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public androidx.car.app.model.Toggle? getToggle();
+ field public static final int IMAGE_TYPE_ICON = 1; // 0x1
+ field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+ }
+
+ public static final class GridItem.Builder {
+ method public androidx.car.app.model.GridItem build();
+ method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon);
+ method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
+ }
+
+ public final class GridTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.GridTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.CarIcon? getBackgroundImage();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getSingleList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class GridTemplate.Builder {
+ method public androidx.car.app.model.GridTemplate build();
+ method public androidx.car.app.model.GridTemplate.Builder clearAllLists();
+ method public androidx.car.app.model.GridTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.GridTemplate.Builder setBackgroundImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.GridTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.GridTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.GridTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+ method public androidx.car.app.model.GridTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public interface Item {
+ }
+
+ public final class ItemList {
+ method public static androidx.car.app.model.ItemList.Builder builder();
+ method public java.util.List<java.lang.Object!> getItems();
+ method public androidx.car.app.model.CarText? getNoItemsMessage();
+ method public int getSelectedIndex();
+ method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
+ }
+
+ public static final class ItemList.Builder {
+ ctor public ItemList.Builder();
+ method public androidx.car.app.model.ItemList.Builder addItem(androidx.car.app.model.Item);
+ method public androidx.car.app.model.ItemList build();
+ method public androidx.car.app.model.ItemList.Builder clearItems();
+ method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
+ method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+ method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+ method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
+ }
+
+ public static interface ItemList.OnItemVisibilityChangedListener {
+ method public void onItemVisibilityChanged(int, int);
+ }
+
+ public static interface ItemList.OnSelectedListener {
+ method public void onSelected(int);
+ }
+
+ public final class LatLng {
+ method public static androidx.car.app.model.LatLng create(double, double);
+ method public static androidx.car.app.model.LatLng create(android.location.Location);
+ method public double getLatitude();
+ method public double getLongitude();
+ }
+
+ public final class ListTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.ListTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public java.util.List<androidx.car.app.model.SectionedItemList!> getSectionLists();
+ method public androidx.car.app.model.ItemList? getSingleList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class ListTemplate.Builder {
+ method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
+ method public androidx.car.app.model.ListTemplate build();
+ method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
+ method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.ListTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+ method public androidx.car.app.model.ListTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class MessageTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
+ method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.CarText? getDebugMessage();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public androidx.car.app.model.CarText getMessage();
+ method public androidx.car.app.model.CarText? getTitle();
+ }
+
+ public static final class MessageTemplate.Builder {
+ method public androidx.car.app.model.MessageTemplate build();
+ method public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
+ method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
+ method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class Metadata {
+ method public static androidx.car.app.model.Metadata.Builder builder();
+ method public androidx.car.app.model.Place? getPlace();
+ method public androidx.car.app.model.Metadata.Builder newBuilder();
+ method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
+ field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
+ }
+
+ public static final class Metadata.Builder {
+ method public androidx.car.app.model.Metadata build();
+ method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
+ }
+
+ public interface OnClickListener {
+ method public void onClick();
+ }
+
+ public class OnClickListenerWrapper {
+ method public boolean isParkedOnly();
+ }
+
+ public final class Pane {
+ method public static androidx.car.app.model.Pane.Builder builder();
+ method public androidx.car.app.model.ActionList? getActionList();
+ method public java.util.List<java.lang.Object!> getRows();
+ method public boolean isLoading();
+ method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
+ }
+
+ public static final class Pane.Builder {
+ ctor public Pane.Builder();
+ method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
+ method public androidx.car.app.model.Pane build();
+ method public androidx.car.app.model.Pane.Builder clearRows();
+ method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method public androidx.car.app.model.Pane.Builder setLoading(boolean);
+ }
+
+ public final class PaneTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.PaneTemplate.Builder builder(androidx.car.app.model.Pane);
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.Pane getPane();
+ method public androidx.car.app.model.CarText? getTitle();
+ }
+
+ public static final class PaneTemplate.Builder {
+ method public androidx.car.app.model.PaneTemplate build();
+ method public androidx.car.app.model.PaneTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.PaneTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.PaneTemplate.Builder setPane(androidx.car.app.model.Pane);
+ method public androidx.car.app.model.PaneTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class ParkedOnlyOnClickListener implements androidx.car.app.model.OnClickListener {
+ method public static androidx.car.app.model.ParkedOnlyOnClickListener create(androidx.car.app.model.OnClickListener);
+ method public void onClick();
+ }
+
+ public class Place {
+ method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
+ method public androidx.car.app.model.LatLng getLatLng();
+ method public androidx.car.app.model.PlaceMarker? getMarker();
+ method public androidx.car.app.model.Place.Builder newBuilder();
+ }
+
+ public static final class Place.Builder {
+ method public androidx.car.app.model.Place build();
+ method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
+ method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
+ }
+
+ public final class PlaceListMapTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.PlaceListMapTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Place? getAnchor();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isCurrentLocationEnabled();
+ method public boolean isLoading();
+ }
+
+ public static final class PlaceListMapTemplate.Builder {
+ ctor public PlaceListMapTemplate.Builder();
+ method public androidx.car.app.model.PlaceListMapTemplate build();
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setAnchor(androidx.car.app.model.Place?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setCurrentLocationEnabled(boolean);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class PlaceMarker {
+ method public static androidx.car.app.model.PlaceMarker.Builder builder();
+ method public androidx.car.app.model.CarColor? getColor();
+ method public static androidx.car.app.model.PlaceMarker getDefault();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public int getIconType();
+ method public androidx.car.app.model.CarText? getLabel();
+ method public static boolean isDefaultMarker(androidx.car.app.model.PlaceMarker?);
+ field public static final int TYPE_ICON = 0; // 0x0
+ field public static final int TYPE_IMAGE = 1; // 0x1
+ }
+
+ public static final class PlaceMarker.Builder {
+ method public androidx.car.app.model.PlaceMarker build();
+ method public androidx.car.app.model.PlaceMarker.Builder setColor(androidx.car.app.model.CarColor?);
+ method public androidx.car.app.model.PlaceMarker.Builder setIcon(androidx.car.app.model.CarIcon?, int);
+ method public androidx.car.app.model.PlaceMarker.Builder setLabel(CharSequence?);
+ }
+
+ public class Row implements androidx.car.app.model.Item {
+ method public static androidx.car.app.model.Row.Builder builder();
+ method public int getFlags();
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.Metadata getMetadata();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public int getRowImageType();
+ method public java.util.List<androidx.car.app.model.CarText!> getTexts();
+ method public androidx.car.app.model.CarText getTitle();
+ method public androidx.car.app.model.Toggle? getToggle();
+ method public boolean isBrowsable();
+ method public androidx.car.app.model.Row row();
+ method public void yourBoat();
+ field public static final int IMAGE_TYPE_ICON = 4; // 0x4
+ field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+ field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+ field public static final int ROW_FLAG_NONE = 1; // 0x1
+ field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
+ field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
+ }
+
+ public static final class Row.Builder {
+ method public androidx.car.app.model.Row.Builder addText(CharSequence);
+ method public androidx.car.app.model.Row build();
+ method public androidx.car.app.model.Row.Builder clearText();
+ method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
+ method public androidx.car.app.model.Row.Builder setFlags(int);
+ method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
+ method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+ method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
+ method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
+ }
+
+ public final class SearchTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public String? getInitialSearchText();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public String? getSearchHint();
+ method public boolean isLoading();
+ method public boolean isShowKeyboardByDefault();
+ }
+
+ public static final class SearchTemplate.Builder {
+ method public androidx.car.app.model.SearchTemplate build();
+ method public androidx.car.app.model.SearchTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.SearchTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.SearchTemplate.Builder setInitialSearchText(String?);
+ method public androidx.car.app.model.SearchTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.model.SearchTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.SearchTemplate.Builder setSearchHint(String?);
+ method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
+ }
+
+ public class SectionedItemList {
+ method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
+ method public androidx.car.app.model.CarText getHeader();
+ method public androidx.car.app.model.ItemList getItemList();
+ }
+
+ public interface Template {
+ method public default void checkPermissions(android.content.Context);
+ method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
+ }
+
+ public final class TemplateInfo {
+ ctor public TemplateInfo(androidx.car.app.model.Template, String);
+ method public Class<? extends androidx.car.app.model.Template> getTemplateClass();
+ method public String getTemplateId();
+ }
+
+ public final class TemplateWrapper {
+ method public static androidx.car.app.model.TemplateWrapper copyOf(androidx.car.app.model.TemplateWrapper);
+ method public int getCurrentTaskStep();
+ method public String getId();
+ method public androidx.car.app.model.Template getTemplate();
+ method public java.util.List<androidx.car.app.model.TemplateInfo!>? getTemplateInfosForScreenStack();
+ method public boolean isRefresh();
+ method public void setCurrentTaskStep(int);
+ method public void setId(String);
+ method public void setRefresh(boolean);
+ method public void setTemplate(androidx.car.app.model.Template);
+ method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template);
+ method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template, String);
+ }
+
+ public class Toggle {
+ method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+ method public boolean isChecked();
+ }
+
+ public static final class Toggle.Builder {
+ method public androidx.car.app.model.Toggle build();
+ method public androidx.car.app.model.Toggle.Builder setChecked(boolean);
+ method public androidx.car.app.model.Toggle.Builder setCheckedChangeListener(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+ }
+
+ public static interface Toggle.OnCheckedChangeListener {
+ method public void onCheckedChange(boolean);
+ }
+
+}
+
+package androidx.car.app.model.constraints {
+
+ public class ActionsConstraints {
+ method @VisibleForTesting public static androidx.car.app.model.constraints.ActionsConstraints.Builder builder();
+ method public java.util.Set<java.lang.Integer!> getDisallowedActionTypes();
+ method public int getMaxActions();
+ method public int getMaxCustomTitles();
+ method public java.util.Set<java.lang.Integer!> getRequiredActionTypes();
+ method @VisibleForTesting public androidx.car.app.model.constraints.ActionsConstraints.Builder newBuilder();
+ method public void validateOrThrow(java.util.List<java.lang.Object!>);
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_HEADER;
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION;
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE;
+ }
+
+ @VisibleForTesting public static final class ActionsConstraints.Builder {
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder addDisallowedActionType(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder addRequiredActionType(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints build();
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxActions(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxCustomTitles(int);
+ }
+
+ public class CarColorConstraints {
+ method public void validateOrThrow(androidx.car.app.model.CarColor);
+ field public static final androidx.car.app.model.constraints.CarColorConstraints STANDARD_ONLY;
+ field public static final androidx.car.app.model.constraints.CarColorConstraints UNCONSTRAINED;
+ }
+
+ public class CarIconConstraints {
+ method public androidx.core.graphics.drawable.IconCompat checkSupportedIcon(androidx.core.graphics.drawable.IconCompat);
+ method public void validateOrThrow(androidx.car.app.model.CarIcon?);
+ field public static final androidx.car.app.model.constraints.CarIconConstraints DEFAULT;
+ field public static final androidx.car.app.model.constraints.CarIconConstraints UNCONSTRAINED;
+ }
+
+ public class RowConstraints {
+ method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
+ method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
+ method public int getFlagOverrides();
+ method public int getMaxActionsExclusive();
+ method public int getMaxTextLinesPerRow();
+ method public boolean isImageAllowed();
+ method public boolean isOnClickListenerAllowed();
+ method public boolean isToggleAllowed();
+ method public androidx.car.app.model.constraints.RowConstraints.Builder newBuilder();
+ method public void validateOrThrow(Object);
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_CONSERVATIVE;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_FULL_LIST;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_PANE;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_SIMPLE;
+ field public static final androidx.car.app.model.constraints.RowConstraints UNCONSTRAINED;
+ }
+
+ public static final class RowConstraints.Builder {
+ method public androidx.car.app.model.constraints.RowConstraints build();
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setOnClickListenerAllowed(boolean);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setToggleAllowed(boolean);
+ }
+
+ public class RowListConstraints {
+ method public static androidx.car.app.model.constraints.RowListConstraints.Builder builder();
+ method public int getMaxActions();
+ method public androidx.car.app.model.constraints.RowConstraints getRowConstraints();
+ method public int getRowListType();
+ method public boolean isAllowSelectableLists();
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder newBuilder();
+ method public void validateOrThrow(androidx.car.app.model.ItemList);
+ method public void validateOrThrow(java.util.List<androidx.car.app.model.SectionedItemList!>);
+ method public void validateOrThrow(androidx.car.app.model.Pane);
+ field public static final int DEFAULT_LIST = 0; // 0x0
+ field public static final int PANE = 1; // 0x1
+ field public static final int ROUTE_PREVIEW = 2; // 0x2
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_PANE;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE;
+ }
+
+ public static final class RowListConstraints.Builder {
+ method public androidx.car.app.model.constraints.RowListConstraints build();
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setAllowSelectableLists(boolean);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setMaxActions(int);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowConstraints(androidx.car.app.model.constraints.RowConstraints);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowListType(int);
+ }
+
+}
+
+package androidx.car.app.navigation {
+
+ public class NavigationManager {
+ method @MainThread public void navigationEnded();
+ method @MainThread public void navigationStarted();
+ method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+ method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
+ }
+
+ public interface NavigationManagerListener {
+ method public void onAutoDriveEnabled();
+ method public void stopNavigation();
+ }
+
+}
+
+package androidx.car.app.navigation.model {
+
+ public final class Destination {
+ method public static androidx.car.app.navigation.model.Destination.Builder builder(CharSequence, CharSequence);
+ method public static androidx.car.app.navigation.model.Destination.Builder builder();
+ method public androidx.car.app.model.CarText? getAddress();
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.CarText? getName();
+ }
+
+ public static final class Destination.Builder {
+ method public androidx.car.app.navigation.model.Destination build();
+ method public androidx.car.app.navigation.model.Destination.Builder setAddress(CharSequence?);
+ method public androidx.car.app.navigation.model.Destination.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Destination.Builder setName(CharSequence?);
+ }
+
+ public final class Lane {
+ method public static androidx.car.app.navigation.model.Lane.Builder builder();
+ method public java.util.List<androidx.car.app.navigation.model.LaneDirection!> getDirections();
+ }
+
+ public static final class Lane.Builder {
+ ctor public Lane.Builder();
+ method public androidx.car.app.navigation.model.Lane.Builder addDirection(androidx.car.app.navigation.model.LaneDirection);
+ method public androidx.car.app.navigation.model.Lane build();
+ method public androidx.car.app.navigation.model.Lane.Builder clearDirections();
+ }
+
+ public final class LaneDirection {
+ method public static androidx.car.app.navigation.model.LaneDirection create(int, boolean);
+ method public int getShape();
+ method public boolean isHighlighted();
+ field public static final int SHAPE_NORMAL_LEFT = 5; // 0x5
+ field public static final int SHAPE_NORMAL_RIGHT = 6; // 0x6
+ field public static final int SHAPE_SHARP_LEFT = 7; // 0x7
+ field public static final int SHAPE_SHARP_RIGHT = 8; // 0x8
+ field public static final int SHAPE_SLIGHT_LEFT = 3; // 0x3
+ field public static final int SHAPE_SLIGHT_RIGHT = 4; // 0x4
+ field public static final int SHAPE_STRAIGHT = 2; // 0x2
+ field public static final int SHAPE_UNKNOWN = 1; // 0x1
+ field public static final int SHAPE_U_TURN_LEFT = 9; // 0x9
+ field public static final int SHAPE_U_TURN_RIGHT = 10; // 0xa
+ }
+
+ public final class Maneuver {
+ method public static androidx.car.app.navigation.model.Maneuver.Builder builder(int);
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public int getRoundaboutExitAngle();
+ method public int getRoundaboutExitNumber();
+ method public int getType();
+ field public static final int TYPE_DEPART = 1; // 0x1
+ field public static final int TYPE_DESTINATION = 39; // 0x27
+ field public static final int TYPE_DESTINATION_LEFT = 41; // 0x29
+ field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
+ field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
+ field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+ field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+ field public static final int TYPE_FORK_LEFT = 25; // 0x19
+ field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
+ field public static final int TYPE_KEEP_LEFT = 3; // 0x3
+ field public static final int TYPE_KEEP_RIGHT = 4; // 0x4
+ field public static final int TYPE_MERGE_LEFT = 27; // 0x1b
+ field public static final int TYPE_MERGE_RIGHT = 28; // 0x1c
+ field public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29; // 0x1d
+ field public static final int TYPE_NAME_CHANGE = 2; // 0x2
+ field public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23; // 0x17
+ field public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24; // 0x18
+ field public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21; // 0x15
+ field public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22; // 0x16
+ field public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15; // 0xf
+ field public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16; // 0x10
+ field public static final int TYPE_ON_RAMP_SHARP_LEFT = 17; // 0x11
+ field public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18; // 0x12
+ field public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13; // 0xd
+ field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
+ field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
+ field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
+ field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
+ field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+ field public static final int TYPE_STRAIGHT = 36; // 0x24
+ field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
+ field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
+ field public static final int TYPE_TURN_SHARP_LEFT = 9; // 0x9
+ field public static final int TYPE_TURN_SHARP_RIGHT = 10; // 0xa
+ field public static final int TYPE_TURN_SLIGHT_LEFT = 5; // 0x5
+ field public static final int TYPE_TURN_SLIGHT_RIGHT = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_U_TURN_LEFT = 11; // 0xb
+ field public static final int TYPE_U_TURN_RIGHT = 12; // 0xc
+ }
+
+ public static final class Maneuver.Builder {
+ method public androidx.car.app.navigation.model.Maneuver build();
+ method public androidx.car.app.navigation.model.Maneuver.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitAngle(int);
+ method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitNumber(int);
+ }
+
+ public class MessageInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+ method public static androidx.car.app.navigation.model.MessageInfo.Builder builder(CharSequence);
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.CarText? getText();
+ method public androidx.car.app.model.CarText getTitle();
+ }
+
+ public static final class MessageInfo.Builder {
+ method public androidx.car.app.navigation.model.MessageInfo build();
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setText(CharSequence?);
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setTitle(CharSequence);
+ }
+
+ public class NavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.NavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip getActionStrip();
+ method public androidx.car.app.model.CarColor? getBackgroundColor();
+ method public androidx.car.app.navigation.model.TravelEstimate? getDestinationTravelEstimate();
+ method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? getNavigationInfo();
+ }
+
+ public static final class NavigationTemplate.Builder {
+ method public androidx.car.app.navigation.model.NavigationTemplate build();
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setBackgroundColor(androidx.car.app.model.CarColor?);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate?);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setNavigationInfo(androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo?);
+ }
+
+ public static interface NavigationTemplate.NavigationInfo {
+ }
+
+ public final class PlaceListNavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class PlaceListNavigationTemplate.Builder {
+ ctor public PlaceListNavigationTemplate.Builder();
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class RoutePreviewNavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.Action? getNavigateAction();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class RoutePreviewNavigationTemplate.Builder {
+ ctor public RoutePreviewNavigationTemplate.Builder();
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class RoutingInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+ method public static androidx.car.app.navigation.model.RoutingInfo.Builder builder();
+ method public androidx.car.app.model.Distance? getCurrentDistance();
+ method public androidx.car.app.navigation.model.Step? getCurrentStep();
+ method public androidx.car.app.model.CarIcon? getJunctionImage();
+ method public androidx.car.app.navigation.model.Step? getNextStep();
+ method public boolean isLoading();
+ }
+
+ public static final class RoutingInfo.Builder {
+ method public androidx.car.app.navigation.model.RoutingInfo build();
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
+ }
+
+ public final class Step {
+ method public static androidx.car.app.navigation.model.Step.Builder builder(CharSequence);
+ method public androidx.car.app.model.CarText? getCue();
+ method public java.util.List<androidx.car.app.navigation.model.Lane!> getLanes();
+ method public androidx.car.app.model.CarIcon? getLanesImage();
+ method public androidx.car.app.navigation.model.Maneuver? getManeuver();
+ method public androidx.car.app.model.CarText? getRoad();
+ method public androidx.car.app.navigation.model.Step.Builder newBuilder();
+ }
+
+ public static final class Step.Builder {
+ method public androidx.car.app.navigation.model.Step.Builder addLane(androidx.car.app.navigation.model.Lane);
+ method public androidx.car.app.navigation.model.Step build();
+ method public androidx.car.app.navigation.model.Step.Builder clearLanes();
+ method public androidx.car.app.navigation.model.Step.Builder setCue(CharSequence);
+ method public androidx.car.app.navigation.model.Step.Builder setLanesImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Step.Builder setManeuver(androidx.car.app.navigation.model.Maneuver?);
+ method public androidx.car.app.navigation.model.Step.Builder setRoad(CharSequence);
+ }
+
+ public final class TravelEstimate {
+ method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+ method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+ method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+ method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+ method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
+ method public androidx.car.app.model.Distance getRemainingDistance();
+ method public androidx.car.app.model.CarColor getRemainingDistanceColor();
+ method public androidx.car.app.model.CarColor getRemainingTimeColor();
+ method public long getRemainingTimeSeconds();
+ }
+
+ public static final class TravelEstimate.Builder {
+ method public androidx.car.app.navigation.model.TravelEstimate build();
+ method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+ method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+ }
+
+ public final class Trip {
+ method public static androidx.car.app.navigation.model.Trip.Builder builder();
+ method public androidx.car.app.model.CarText? getCurrentRoad();
+ method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getDestinationTravelEstimates();
+ method public java.util.List<androidx.car.app.navigation.model.Destination!> getDestinations();
+ method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getStepTravelEstimates();
+ method public java.util.List<androidx.car.app.navigation.model.Step!> getSteps();
+ method public boolean isLoading();
+ }
+
+ public static final class Trip.Builder {
+ ctor public Trip.Builder();
+ method public androidx.car.app.navigation.model.Trip.Builder addDestination(androidx.car.app.navigation.model.Destination);
+ method public androidx.car.app.navigation.model.Trip.Builder addDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+ method public androidx.car.app.navigation.model.Trip.Builder addStep(androidx.car.app.navigation.model.Step?);
+ method public androidx.car.app.navigation.model.Trip.Builder addStepTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+ method public androidx.car.app.navigation.model.Trip build();
+ method public androidx.car.app.navigation.model.Trip.Builder clearDestinationTravelEstimates();
+ method public androidx.car.app.navigation.model.Trip.Builder clearDestinations();
+ method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
+ method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
+ method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
+ method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+ }
+
+}
+
package androidx.car.app.serialization {
public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1051,10 @@
package androidx.car.app.utils {
+ public interface Logger {
+ method public void log(String);
+ }
+
public class ThreadUtils {
method public static void checkMainThread();
method public static void runOnMain(Runnable);
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 8839e7a..bfa3df9 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -111,6 +111,7 @@
method public final androidx.lifecycle.Lifecycle getLifecycle();
method public String? getMarker();
method public final androidx.car.app.ScreenManager getScreenManager();
+ method public abstract androidx.car.app.model.Template getTemplate();
method public final void invalidate();
method public void setMarker(String?);
method public void setResult(Object?);
@@ -152,6 +153,885 @@
}
+package androidx.car.app.model {
+
+ public final class Action {
+ method public static androidx.car.app.model.Action.Builder builder();
+ method public androidx.car.app.model.CarColor getBackgroundColor();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public int getType();
+ method public boolean isStandard();
+ method public androidx.car.app.model.Action.Builder newBuilder();
+ method public static String typeToString(int);
+ field public static final androidx.car.app.model.Action APP_ICON;
+ field public static final androidx.car.app.model.Action BACK;
+ field public static final int TYPE_APP_ICON = 65538; // 0x10002
+ field public static final int TYPE_BACK = 65539; // 0x10003
+ field public static final int TYPE_CUSTOM = 1; // 0x1
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class Action.Builder {
+ method public androidx.car.app.model.Action build();
+ method public androidx.car.app.model.Action.Builder setBackgroundColor(androidx.car.app.model.CarColor);
+ method public androidx.car.app.model.Action.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.Action.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.Action.Builder setTitle(CharSequence?);
+ }
+
+ public class ActionList {
+ method public static androidx.car.app.model.ActionList create(java.util.List<androidx.car.app.model.Action!>);
+ method public java.util.List<androidx.car.app.model.Action!> getList();
+ }
+
+ public class ActionStrip {
+ method public static androidx.car.app.model.ActionStrip.Builder builder();
+ method public androidx.car.app.model.Action? getActionOfType(int);
+ method public java.util.List<java.lang.Object!> getActions();
+ }
+
+ public static final class ActionStrip.Builder {
+ ctor public ActionStrip.Builder();
+ method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
+ method public androidx.car.app.model.ActionStrip build();
+ method public androidx.car.app.model.ActionStrip.Builder clearActions();
+ }
+
+ public class CarColor {
+ method public static androidx.car.app.model.CarColor createCustom(@ColorInt int, @ColorInt int);
+ method @ColorInt public int getColor();
+ method @ColorInt public int getColorDark();
+ method public int getType();
+ field public static final androidx.car.app.model.CarColor BLUE;
+ field public static final androidx.car.app.model.CarColor DEFAULT;
+ field public static final androidx.car.app.model.CarColor GREEN;
+ field public static final androidx.car.app.model.CarColor PRIMARY;
+ field public static final androidx.car.app.model.CarColor RED;
+ field public static final androidx.car.app.model.CarColor SECONDARY;
+ field public static final int TYPE_BLUE = 6; // 0x6
+ field public static final int TYPE_CUSTOM = 0; // 0x0
+ field public static final int TYPE_DEFAULT = 1; // 0x1
+ field public static final int TYPE_GREEN = 5; // 0x5
+ field public static final int TYPE_PRIMARY = 2; // 0x2
+ field public static final int TYPE_RED = 4; // 0x4
+ field public static final int TYPE_SECONDARY = 3; // 0x3
+ field public static final int TYPE_YELLOW = 7; // 0x7
+ field public static final androidx.car.app.model.CarColor YELLOW;
+ }
+
+ public class CarIcon {
+ method public static androidx.car.app.model.CarIcon.Builder builder(androidx.core.graphics.drawable.IconCompat);
+ method public androidx.core.graphics.drawable.IconCompat? getIcon();
+ method public androidx.car.app.model.CarColor? getTint();
+ method public int getType();
+ method public androidx.car.app.model.CarIcon.Builder newBuilder();
+ method public static androidx.car.app.model.CarIcon of(androidx.core.graphics.drawable.IconCompat);
+ field public static final androidx.car.app.model.CarIcon ALERT;
+ field public static final androidx.car.app.model.CarIcon APP_ICON;
+ field public static final androidx.car.app.model.CarIcon BACK;
+ field public static final androidx.car.app.model.CarIcon ERROR;
+ field public static final int TYPE_ALERT = 4; // 0x4
+ field public static final int TYPE_APP = 5; // 0x5
+ field public static final int TYPE_BACK = 3; // 0x3
+ field public static final int TYPE_CUSTOM = 1; // 0x1
+ field public static final int TYPE_ERROR = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
+ field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
+ }
+
+ public static final class CarIcon.Builder {
+ method public androidx.car.app.model.CarIcon build();
+ method public androidx.car.app.model.CarIcon.Builder setIcon(androidx.car.app.model.CarIcon);
+ method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
+ }
+
+ public class CarIconSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon);
+ method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon, int);
+ method public int getAlignment();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public void updateDrawState(android.text.TextPaint?);
+ method public static int validateAlignment(int);
+ field public static final int ALIGN_BASELINE = 1; // 0x1
+ field public static final int ALIGN_BOTTOM = 0; // 0x0
+ field public static final int ALIGN_CENTER = 2; // 0x2
+ }
+
+ public class CarText {
+ ctor public CarText();
+ method public static androidx.car.app.model.CarText create(CharSequence);
+ method public java.util.List<androidx.car.app.model.CarText.SpanWrapper!> getSpans();
+ method public String? getText();
+ method public boolean isEmpty();
+ method public static boolean isNullOrEmpty(androidx.car.app.model.CarText?);
+ method public static String? toShortString(androidx.car.app.model.CarText?);
+ field public static final androidx.car.app.model.CarText EMPTY;
+ }
+
+ public static class CarText.SpanWrapper {
+ field @Keep public final int end;
+ field @Keep public final int flags;
+ field @Keep public final Object? span;
+ field @Keep public final int start;
+ }
+
+ public class DateTimeWithZone {
+ method public static androidx.car.app.model.DateTimeWithZone create(long, int, String);
+ method public static androidx.car.app.model.DateTimeWithZone create(long, java.util.TimeZone);
+ method @RequiresApi(26) public static androidx.car.app.model.DateTimeWithZone create(java.time.ZonedDateTime);
+ method public long getTimeSinceEpochMillis();
+ method public int getZoneOffsetSeconds();
+ method public String? getZoneShortName();
+ }
+
+ public final class Distance {
+ method public static androidx.car.app.model.Distance create(double, int);
+ method public double getDisplayDistance();
+ method public int getDisplayUnit();
+ field public static final int UNIT_FEET = 6; // 0x6
+ field public static final int UNIT_KILOMETERS = 2; // 0x2
+ field public static final int UNIT_KILOMETERS_P1 = 3; // 0x3
+ field public static final int UNIT_METERS = 1; // 0x1
+ field public static final int UNIT_MILES = 4; // 0x4
+ field public static final int UNIT_MILES_P1 = 5; // 0x5
+ field public static final int UNIT_YARDS = 7; // 0x7
+ }
+
+ public class DistanceSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.DistanceSpan create(androidx.car.app.model.Distance);
+ method public androidx.car.app.model.Distance getDistance();
+ method public void updateDrawState(android.text.TextPaint?);
+ }
+
+ public class DurationSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.DurationSpan create(long);
+ method @RequiresApi(26) public static androidx.car.app.model.DurationSpan create(java.time.Duration);
+ method public long getDurationSeconds();
+ method public void updateDrawState(android.text.TextPaint?);
+ }
+
+ public class ForegroundCarColorSpan extends android.text.style.CharacterStyle {
+ method public static androidx.car.app.model.ForegroundCarColorSpan create(androidx.car.app.model.CarColor);
+ method public androidx.car.app.model.CarColor getColor();
+ method public void updateDrawState(android.text.TextPaint);
+ }
+
+ public class GridItem implements androidx.car.app.model.Item {
+ method public static androidx.car.app.model.GridItem.Builder builder();
+ method public androidx.car.app.model.CarIcon getImage();
+ method public int getImageType();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public androidx.car.app.model.CarText? getText();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public androidx.car.app.model.Toggle? getToggle();
+ field public static final int IMAGE_TYPE_ICON = 1; // 0x1
+ field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+ }
+
+ public static final class GridItem.Builder {
+ method public androidx.car.app.model.GridItem build();
+ method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon);
+ method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
+ }
+
+ public final class GridTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.GridTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.CarIcon? getBackgroundImage();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getSingleList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class GridTemplate.Builder {
+ method public androidx.car.app.model.GridTemplate build();
+ method public androidx.car.app.model.GridTemplate.Builder clearAllLists();
+ method public androidx.car.app.model.GridTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.GridTemplate.Builder setBackgroundImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.GridTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.GridTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.GridTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+ method public androidx.car.app.model.GridTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public interface Item {
+ }
+
+ public final class ItemList {
+ method public static androidx.car.app.model.ItemList.Builder builder();
+ method public java.util.List<java.lang.Object!> getItems();
+ method public androidx.car.app.model.CarText? getNoItemsMessage();
+ method public int getSelectedIndex();
+ method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
+ }
+
+ public static final class ItemList.Builder {
+ ctor public ItemList.Builder();
+ method public androidx.car.app.model.ItemList.Builder addItem(androidx.car.app.model.Item);
+ method public androidx.car.app.model.ItemList build();
+ method public androidx.car.app.model.ItemList.Builder clearItems();
+ method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
+ method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+ method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+ method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
+ }
+
+ public static interface ItemList.OnItemVisibilityChangedListener {
+ method public void onItemVisibilityChanged(int, int);
+ }
+
+ public static interface ItemList.OnSelectedListener {
+ method public void onSelected(int);
+ }
+
+ public final class LatLng {
+ method public static androidx.car.app.model.LatLng create(double, double);
+ method public static androidx.car.app.model.LatLng create(android.location.Location);
+ method public double getLatitude();
+ method public double getLongitude();
+ }
+
+ public final class ListTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.ListTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public java.util.List<androidx.car.app.model.SectionedItemList!> getSectionLists();
+ method public androidx.car.app.model.ItemList? getSingleList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class ListTemplate.Builder {
+ method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
+ method public androidx.car.app.model.ListTemplate build();
+ method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
+ method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.ListTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+ method public androidx.car.app.model.ListTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class MessageTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
+ method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.CarText? getDebugMessage();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public androidx.car.app.model.CarText getMessage();
+ method public androidx.car.app.model.CarText? getTitle();
+ }
+
+ public static final class MessageTemplate.Builder {
+ method public androidx.car.app.model.MessageTemplate build();
+ method public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
+ method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
+ method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class Metadata {
+ method public static androidx.car.app.model.Metadata.Builder builder();
+ method public androidx.car.app.model.Place? getPlace();
+ method public androidx.car.app.model.Metadata.Builder newBuilder();
+ method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
+ field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
+ }
+
+ public static final class Metadata.Builder {
+ method public androidx.car.app.model.Metadata build();
+ method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
+ }
+
+ public interface OnClickListener {
+ method public void onClick();
+ }
+
+ public class OnClickListenerWrapper {
+ method public boolean isParkedOnly();
+ }
+
+ public final class Pane {
+ method public static androidx.car.app.model.Pane.Builder builder();
+ method public androidx.car.app.model.ActionList? getActionList();
+ method public java.util.List<java.lang.Object!> getRows();
+ method public boolean isLoading();
+ method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
+ }
+
+ public static final class Pane.Builder {
+ ctor public Pane.Builder();
+ method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
+ method public androidx.car.app.model.Pane build();
+ method public androidx.car.app.model.Pane.Builder clearRows();
+ method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method public androidx.car.app.model.Pane.Builder setLoading(boolean);
+ }
+
+ public final class PaneTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.PaneTemplate.Builder builder(androidx.car.app.model.Pane);
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.Pane getPane();
+ method public androidx.car.app.model.CarText? getTitle();
+ }
+
+ public static final class PaneTemplate.Builder {
+ method public androidx.car.app.model.PaneTemplate build();
+ method public androidx.car.app.model.PaneTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.PaneTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.PaneTemplate.Builder setPane(androidx.car.app.model.Pane);
+ method public androidx.car.app.model.PaneTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class ParkedOnlyOnClickListener implements androidx.car.app.model.OnClickListener {
+ method public static androidx.car.app.model.ParkedOnlyOnClickListener create(androidx.car.app.model.OnClickListener);
+ method public void onClick();
+ }
+
+ public class Place {
+ method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
+ method public androidx.car.app.model.LatLng getLatLng();
+ method public androidx.car.app.model.PlaceMarker? getMarker();
+ method public androidx.car.app.model.Place.Builder newBuilder();
+ }
+
+ public static final class Place.Builder {
+ method public androidx.car.app.model.Place build();
+ method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
+ method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
+ }
+
+ public final class PlaceListMapTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.PlaceListMapTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Place? getAnchor();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isCurrentLocationEnabled();
+ method public boolean isLoading();
+ }
+
+ public static final class PlaceListMapTemplate.Builder {
+ ctor public PlaceListMapTemplate.Builder();
+ method public androidx.car.app.model.PlaceListMapTemplate build();
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setAnchor(androidx.car.app.model.Place?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setCurrentLocationEnabled(boolean);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.PlaceListMapTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class PlaceMarker {
+ method public static androidx.car.app.model.PlaceMarker.Builder builder();
+ method public androidx.car.app.model.CarColor? getColor();
+ method public static androidx.car.app.model.PlaceMarker getDefault();
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public int getIconType();
+ method public androidx.car.app.model.CarText? getLabel();
+ method public static boolean isDefaultMarker(androidx.car.app.model.PlaceMarker?);
+ field public static final int TYPE_ICON = 0; // 0x0
+ field public static final int TYPE_IMAGE = 1; // 0x1
+ }
+
+ public static final class PlaceMarker.Builder {
+ method public androidx.car.app.model.PlaceMarker build();
+ method public androidx.car.app.model.PlaceMarker.Builder setColor(androidx.car.app.model.CarColor?);
+ method public androidx.car.app.model.PlaceMarker.Builder setIcon(androidx.car.app.model.CarIcon?, int);
+ method public androidx.car.app.model.PlaceMarker.Builder setLabel(CharSequence?);
+ }
+
+ public class Row implements androidx.car.app.model.Item {
+ method public static androidx.car.app.model.Row.Builder builder();
+ method public int getFlags();
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.Metadata getMetadata();
+ method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+ method public int getRowImageType();
+ method public java.util.List<androidx.car.app.model.CarText!> getTexts();
+ method public androidx.car.app.model.CarText getTitle();
+ method public androidx.car.app.model.Toggle? getToggle();
+ method public boolean isBrowsable();
+ method public androidx.car.app.model.Row row();
+ method public void yourBoat();
+ field public static final int IMAGE_TYPE_ICON = 4; // 0x4
+ field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+ field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+ field public static final int ROW_FLAG_NONE = 1; // 0x1
+ field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
+ field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
+ }
+
+ public static final class Row.Builder {
+ method public androidx.car.app.model.Row.Builder addText(CharSequence);
+ method public androidx.car.app.model.Row build();
+ method public androidx.car.app.model.Row.Builder clearText();
+ method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
+ method public androidx.car.app.model.Row.Builder setFlags(int);
+ method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
+ method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+ method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+ method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
+ method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
+ }
+
+ public final class SearchTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public String? getInitialSearchText();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public String? getSearchHint();
+ method public boolean isLoading();
+ method public boolean isShowKeyboardByDefault();
+ }
+
+ public static final class SearchTemplate.Builder {
+ method public androidx.car.app.model.SearchTemplate build();
+ method public androidx.car.app.model.SearchTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.model.SearchTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.model.SearchTemplate.Builder setInitialSearchText(String?);
+ method public androidx.car.app.model.SearchTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.model.SearchTemplate.Builder setLoading(boolean);
+ method public androidx.car.app.model.SearchTemplate.Builder setSearchHint(String?);
+ method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
+ }
+
+ public class SectionedItemList {
+ method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
+ method public androidx.car.app.model.CarText getHeader();
+ method public androidx.car.app.model.ItemList getItemList();
+ }
+
+ public interface Template {
+ method public default void checkPermissions(android.content.Context);
+ method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
+ }
+
+ public final class TemplateInfo {
+ ctor public TemplateInfo(androidx.car.app.model.Template, String);
+ method public Class<? extends androidx.car.app.model.Template> getTemplateClass();
+ method public String getTemplateId();
+ }
+
+ public final class TemplateWrapper {
+ method public static androidx.car.app.model.TemplateWrapper copyOf(androidx.car.app.model.TemplateWrapper);
+ method public int getCurrentTaskStep();
+ method public String getId();
+ method public androidx.car.app.model.Template getTemplate();
+ method public java.util.List<androidx.car.app.model.TemplateInfo!>? getTemplateInfosForScreenStack();
+ method public boolean isRefresh();
+ method public void setCurrentTaskStep(int);
+ method public void setId(String);
+ method public void setRefresh(boolean);
+ method public void setTemplate(androidx.car.app.model.Template);
+ method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template);
+ method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template, String);
+ }
+
+ public class Toggle {
+ method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+ method public boolean isChecked();
+ }
+
+ public static final class Toggle.Builder {
+ method public androidx.car.app.model.Toggle build();
+ method public androidx.car.app.model.Toggle.Builder setChecked(boolean);
+ method public androidx.car.app.model.Toggle.Builder setCheckedChangeListener(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+ }
+
+ public static interface Toggle.OnCheckedChangeListener {
+ method public void onCheckedChange(boolean);
+ }
+
+}
+
+package androidx.car.app.model.constraints {
+
+ public class ActionsConstraints {
+ method @VisibleForTesting public static androidx.car.app.model.constraints.ActionsConstraints.Builder builder();
+ method public java.util.Set<java.lang.Integer!> getDisallowedActionTypes();
+ method public int getMaxActions();
+ method public int getMaxCustomTitles();
+ method public java.util.Set<java.lang.Integer!> getRequiredActionTypes();
+ method @VisibleForTesting public androidx.car.app.model.constraints.ActionsConstraints.Builder newBuilder();
+ method public void validateOrThrow(java.util.List<java.lang.Object!>);
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_HEADER;
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION;
+ field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE;
+ }
+
+ @VisibleForTesting public static final class ActionsConstraints.Builder {
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder addDisallowedActionType(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder addRequiredActionType(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints build();
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxActions(int);
+ method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxCustomTitles(int);
+ }
+
+ public class CarColorConstraints {
+ method public void validateOrThrow(androidx.car.app.model.CarColor);
+ field public static final androidx.car.app.model.constraints.CarColorConstraints STANDARD_ONLY;
+ field public static final androidx.car.app.model.constraints.CarColorConstraints UNCONSTRAINED;
+ }
+
+ public class CarIconConstraints {
+ method public androidx.core.graphics.drawable.IconCompat checkSupportedIcon(androidx.core.graphics.drawable.IconCompat);
+ method public void validateOrThrow(androidx.car.app.model.CarIcon?);
+ field public static final androidx.car.app.model.constraints.CarIconConstraints DEFAULT;
+ field public static final androidx.car.app.model.constraints.CarIconConstraints UNCONSTRAINED;
+ }
+
+ public class RowConstraints {
+ method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
+ method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
+ method public int getFlagOverrides();
+ method public int getMaxActionsExclusive();
+ method public int getMaxTextLinesPerRow();
+ method public boolean isImageAllowed();
+ method public boolean isOnClickListenerAllowed();
+ method public boolean isToggleAllowed();
+ method public androidx.car.app.model.constraints.RowConstraints.Builder newBuilder();
+ method public void validateOrThrow(Object);
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_CONSERVATIVE;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_FULL_LIST;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_PANE;
+ field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_SIMPLE;
+ field public static final androidx.car.app.model.constraints.RowConstraints UNCONSTRAINED;
+ }
+
+ public static final class RowConstraints.Builder {
+ method public androidx.car.app.model.constraints.RowConstraints build();
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setOnClickListenerAllowed(boolean);
+ method public androidx.car.app.model.constraints.RowConstraints.Builder setToggleAllowed(boolean);
+ }
+
+ public class RowListConstraints {
+ method public static androidx.car.app.model.constraints.RowListConstraints.Builder builder();
+ method public int getMaxActions();
+ method public androidx.car.app.model.constraints.RowConstraints getRowConstraints();
+ method public int getRowListType();
+ method public boolean isAllowSelectableLists();
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder newBuilder();
+ method public void validateOrThrow(androidx.car.app.model.ItemList);
+ method public void validateOrThrow(java.util.List<androidx.car.app.model.SectionedItemList!>);
+ method public void validateOrThrow(androidx.car.app.model.Pane);
+ field public static final int DEFAULT_LIST = 0; // 0x0
+ field public static final int PANE = 1; // 0x1
+ field public static final int ROUTE_PREVIEW = 2; // 0x2
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_PANE;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+ field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE;
+ }
+
+ public static final class RowListConstraints.Builder {
+ method public androidx.car.app.model.constraints.RowListConstraints build();
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setAllowSelectableLists(boolean);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setMaxActions(int);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowConstraints(androidx.car.app.model.constraints.RowConstraints);
+ method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowListType(int);
+ }
+
+}
+
+package androidx.car.app.navigation {
+
+ public class NavigationManager {
+ method @MainThread public void navigationEnded();
+ method @MainThread public void navigationStarted();
+ method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+ method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
+ }
+
+ public interface NavigationManagerListener {
+ method public void onAutoDriveEnabled();
+ method public void stopNavigation();
+ }
+
+}
+
+package androidx.car.app.navigation.model {
+
+ public final class Destination {
+ method public static androidx.car.app.navigation.model.Destination.Builder builder(CharSequence, CharSequence);
+ method public static androidx.car.app.navigation.model.Destination.Builder builder();
+ method public androidx.car.app.model.CarText? getAddress();
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.CarText? getName();
+ }
+
+ public static final class Destination.Builder {
+ method public androidx.car.app.navigation.model.Destination build();
+ method public androidx.car.app.navigation.model.Destination.Builder setAddress(CharSequence?);
+ method public androidx.car.app.navigation.model.Destination.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Destination.Builder setName(CharSequence?);
+ }
+
+ public final class Lane {
+ method public static androidx.car.app.navigation.model.Lane.Builder builder();
+ method public java.util.List<androidx.car.app.navigation.model.LaneDirection!> getDirections();
+ }
+
+ public static final class Lane.Builder {
+ ctor public Lane.Builder();
+ method public androidx.car.app.navigation.model.Lane.Builder addDirection(androidx.car.app.navigation.model.LaneDirection);
+ method public androidx.car.app.navigation.model.Lane build();
+ method public androidx.car.app.navigation.model.Lane.Builder clearDirections();
+ }
+
+ public final class LaneDirection {
+ method public static androidx.car.app.navigation.model.LaneDirection create(int, boolean);
+ method public int getShape();
+ method public boolean isHighlighted();
+ field public static final int SHAPE_NORMAL_LEFT = 5; // 0x5
+ field public static final int SHAPE_NORMAL_RIGHT = 6; // 0x6
+ field public static final int SHAPE_SHARP_LEFT = 7; // 0x7
+ field public static final int SHAPE_SHARP_RIGHT = 8; // 0x8
+ field public static final int SHAPE_SLIGHT_LEFT = 3; // 0x3
+ field public static final int SHAPE_SLIGHT_RIGHT = 4; // 0x4
+ field public static final int SHAPE_STRAIGHT = 2; // 0x2
+ field public static final int SHAPE_UNKNOWN = 1; // 0x1
+ field public static final int SHAPE_U_TURN_LEFT = 9; // 0x9
+ field public static final int SHAPE_U_TURN_RIGHT = 10; // 0xa
+ }
+
+ public final class Maneuver {
+ method public static androidx.car.app.navigation.model.Maneuver.Builder builder(int);
+ method public androidx.car.app.model.CarIcon? getIcon();
+ method public int getRoundaboutExitAngle();
+ method public int getRoundaboutExitNumber();
+ method public int getType();
+ field public static final int TYPE_DEPART = 1; // 0x1
+ field public static final int TYPE_DESTINATION = 39; // 0x27
+ field public static final int TYPE_DESTINATION_LEFT = 41; // 0x29
+ field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
+ field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
+ field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+ field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+ field public static final int TYPE_FORK_LEFT = 25; // 0x19
+ field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
+ field public static final int TYPE_KEEP_LEFT = 3; // 0x3
+ field public static final int TYPE_KEEP_RIGHT = 4; // 0x4
+ field public static final int TYPE_MERGE_LEFT = 27; // 0x1b
+ field public static final int TYPE_MERGE_RIGHT = 28; // 0x1c
+ field public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29; // 0x1d
+ field public static final int TYPE_NAME_CHANGE = 2; // 0x2
+ field public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23; // 0x17
+ field public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24; // 0x18
+ field public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21; // 0x15
+ field public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22; // 0x16
+ field public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15; // 0xf
+ field public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16; // 0x10
+ field public static final int TYPE_ON_RAMP_SHARP_LEFT = 17; // 0x11
+ field public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18; // 0x12
+ field public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13; // 0xd
+ field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
+ field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
+ field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
+ field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
+ field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
+ field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+ field public static final int TYPE_STRAIGHT = 36; // 0x24
+ field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
+ field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
+ field public static final int TYPE_TURN_SHARP_LEFT = 9; // 0x9
+ field public static final int TYPE_TURN_SHARP_RIGHT = 10; // 0xa
+ field public static final int TYPE_TURN_SLIGHT_LEFT = 5; // 0x5
+ field public static final int TYPE_TURN_SLIGHT_RIGHT = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_U_TURN_LEFT = 11; // 0xb
+ field public static final int TYPE_U_TURN_RIGHT = 12; // 0xc
+ }
+
+ public static final class Maneuver.Builder {
+ method public androidx.car.app.navigation.model.Maneuver build();
+ method public androidx.car.app.navigation.model.Maneuver.Builder setIcon(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitAngle(int);
+ method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitNumber(int);
+ }
+
+ public class MessageInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+ method public static androidx.car.app.navigation.model.MessageInfo.Builder builder(CharSequence);
+ method public androidx.car.app.model.CarIcon? getImage();
+ method public androidx.car.app.model.CarText? getText();
+ method public androidx.car.app.model.CarText getTitle();
+ }
+
+ public static final class MessageInfo.Builder {
+ method public androidx.car.app.navigation.model.MessageInfo build();
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setText(CharSequence?);
+ method public androidx.car.app.navigation.model.MessageInfo.Builder setTitle(CharSequence);
+ }
+
+ public class NavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.NavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip getActionStrip();
+ method public androidx.car.app.model.CarColor? getBackgroundColor();
+ method public androidx.car.app.navigation.model.TravelEstimate? getDestinationTravelEstimate();
+ method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? getNavigationInfo();
+ }
+
+ public static final class NavigationTemplate.Builder {
+ method public androidx.car.app.navigation.model.NavigationTemplate build();
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setBackgroundColor(androidx.car.app.model.CarColor?);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate?);
+ method public androidx.car.app.navigation.model.NavigationTemplate.Builder setNavigationInfo(androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo?);
+ }
+
+ public static interface NavigationTemplate.NavigationInfo {
+ }
+
+ public final class PlaceListNavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class PlaceListNavigationTemplate.Builder {
+ ctor public PlaceListNavigationTemplate.Builder();
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public final class RoutePreviewNavigationTemplate implements androidx.car.app.model.Template {
+ method public static androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder builder();
+ method public androidx.car.app.model.ActionStrip? getActionStrip();
+ method public androidx.car.app.model.Action? getHeaderAction();
+ method public androidx.car.app.model.ItemList? getItemList();
+ method public androidx.car.app.model.Action? getNavigateAction();
+ method public androidx.car.app.model.CarText? getTitle();
+ method public boolean isLoading();
+ }
+
+ public static final class RoutePreviewNavigationTemplate.Builder {
+ ctor public RoutePreviewNavigationTemplate.Builder();
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+ method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
+ }
+
+ public class RoutingInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+ method public static androidx.car.app.navigation.model.RoutingInfo.Builder builder();
+ method public androidx.car.app.model.Distance? getCurrentDistance();
+ method public androidx.car.app.navigation.model.Step? getCurrentStep();
+ method public androidx.car.app.model.CarIcon? getJunctionImage();
+ method public androidx.car.app.navigation.model.Step? getNextStep();
+ method public boolean isLoading();
+ }
+
+ public static final class RoutingInfo.Builder {
+ method public androidx.car.app.navigation.model.RoutingInfo build();
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
+ }
+
+ public final class Step {
+ method public static androidx.car.app.navigation.model.Step.Builder builder(CharSequence);
+ method public androidx.car.app.model.CarText? getCue();
+ method public java.util.List<androidx.car.app.navigation.model.Lane!> getLanes();
+ method public androidx.car.app.model.CarIcon? getLanesImage();
+ method public androidx.car.app.navigation.model.Maneuver? getManeuver();
+ method public androidx.car.app.model.CarText? getRoad();
+ method public androidx.car.app.navigation.model.Step.Builder newBuilder();
+ }
+
+ public static final class Step.Builder {
+ method public androidx.car.app.navigation.model.Step.Builder addLane(androidx.car.app.navigation.model.Lane);
+ method public androidx.car.app.navigation.model.Step build();
+ method public androidx.car.app.navigation.model.Step.Builder clearLanes();
+ method public androidx.car.app.navigation.model.Step.Builder setCue(CharSequence);
+ method public androidx.car.app.navigation.model.Step.Builder setLanesImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.Step.Builder setManeuver(androidx.car.app.navigation.model.Maneuver?);
+ method public androidx.car.app.navigation.model.Step.Builder setRoad(CharSequence);
+ }
+
+ public final class TravelEstimate {
+ method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+ method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+ method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+ method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+ method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
+ method public androidx.car.app.model.Distance getRemainingDistance();
+ method public androidx.car.app.model.CarColor getRemainingDistanceColor();
+ method public androidx.car.app.model.CarColor getRemainingTimeColor();
+ method public long getRemainingTimeSeconds();
+ }
+
+ public static final class TravelEstimate.Builder {
+ method public androidx.car.app.navigation.model.TravelEstimate build();
+ method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+ method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+ }
+
+ public final class Trip {
+ method public static androidx.car.app.navigation.model.Trip.Builder builder();
+ method public androidx.car.app.model.CarText? getCurrentRoad();
+ method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getDestinationTravelEstimates();
+ method public java.util.List<androidx.car.app.navigation.model.Destination!> getDestinations();
+ method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getStepTravelEstimates();
+ method public java.util.List<androidx.car.app.navigation.model.Step!> getSteps();
+ method public boolean isLoading();
+ }
+
+ public static final class Trip.Builder {
+ ctor public Trip.Builder();
+ method public androidx.car.app.navigation.model.Trip.Builder addDestination(androidx.car.app.navigation.model.Destination);
+ method public androidx.car.app.navigation.model.Trip.Builder addDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+ method public androidx.car.app.navigation.model.Trip.Builder addStep(androidx.car.app.navigation.model.Step?);
+ method public androidx.car.app.navigation.model.Trip.Builder addStepTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+ method public androidx.car.app.navigation.model.Trip build();
+ method public androidx.car.app.navigation.model.Trip.Builder clearDestinationTravelEstimates();
+ method public androidx.car.app.navigation.model.Trip.Builder clearDestinations();
+ method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
+ method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
+ method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
+ method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+ }
+
+}
+
package androidx.car.app.serialization {
public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1051,10 @@
package androidx.car.app.utils {
+ public interface Logger {
+ method public void log(String);
+ }
+
public class ThreadUtils {
method public static void checkMainThread();
method public static void runOnMain(Runnable);
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index 1b0f2e5..16e0e08 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import androidx.build.LibraryGroups
import androidx.build.LibraryType
-import androidx.build.LibraryVersions
-import androidx.build.Publish
+
+import static androidx.build.dependencies.DependenciesKt.*
plugins {
id("AndroidXPlugin")
@@ -26,13 +27,38 @@
dependencies {
implementation "androidx.activity:activity:1.1.0"
implementation "androidx.annotation:annotation:1.1.0"
+ implementation "androidx.core:core:1.3.0"
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
+
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ // TODO(shiufai): We need this for assertThrows. Point back to the AndroidX shared version if
+ // it is ever upgraded.
+ androidTestImplementation("junit:junit:4.13")
+ androidTestImplementation(TRUTH)
+ androidTestImplementation(MOCKITO_ANDROID)
+ androidTestImplementation ROBOLECTRIC, {
+ // The following are excluded as they are resulting in duplicated class conflicts with
+ // the junit dependency above.
+ exclude group: "org.apache.maven.wagon"
+ exclude group: "org.apache.maven"
+ }
}
android {
defaultConfig {
minSdkVersion 21
+ multiDexEnabled = true
+ }
+ lintOptions {
+ // We have a bunch of builder/inner classes where the outer classes access the private
+ // fields/constructors directly.
+ disable("SyntheticAccessor")
+ // We rely on keeping a bunch of private variables in the library for serialization.
+ disable("BanKeepAnnotation")
}
buildFeatures {
aidl = true
@@ -40,6 +66,8 @@
buildTypes.all {
consumerProguardFiles 'proguard-rules.pro'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
androidx {
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/car/app/app/src/androidTest/AndroidManifest.xml
index ce015a5..d84e190 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/car/app/app/src/androidTest/AndroidManifest.xml
@@ -15,6 +15,6 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.car.app.test">
+ package="androidx.car.app">
</manifest>
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestData.java b/car/app/app/src/androidTest/java/androidx/car/app/TestData.java
new file mode 100644
index 0000000..2017796
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/TestData.java
@@ -0,0 +1,32 @@
+/*
+ * 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 androidx.car.app.model.LatLng;
+import androidx.car.app.model.Place;
+
+/** A grab bag of fake data shared by tests. */
+public final class TestData {
+ public static final LatLng GOOGLE_KIR = LatLng.create(47.6696482, -122.19950278);
+ public static final LatLng GOOGLE_BVE = LatLng.create(47.6204588, -122.1918818);
+
+ public static final Place PLACE_KIR = Place.builder(GOOGLE_KIR).build();
+ public static final Place PLACE_BVE = Place.builder(GOOGLE_BVE).build();
+
+ private TestData() {
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java b/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
new file mode 100644
index 0000000..78d3eb4
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
@@ -0,0 +1,166 @@
+/*
+ * 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.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.text.SpannableString;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.SectionedItemList;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.ZonedDateTime;
+import java.time.format.TextStyle;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/** A grab bag of utility methods intended only for tests. */
+public class TestUtils {
+ /** Helper functions in here only. */
+ private TestUtils() {
+ }
+
+ /**
+ * Returns a {@link DateTimeWithZone} instance from a date string and a time zone id.
+ *
+ * @param dateTimeString The string in ISO format, for example "2020-04-14T15:57:00".
+ * @param zoneIdString An Olson DB time zone identifier, for example "US/Pacific".
+ */
+ public static DateTimeWithZone createDateTimeWithZone(
+ String dateTimeString, String zoneIdString) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ TimeZone timeZone = TimeZone.getTimeZone(zoneIdString);
+ dateFormat.setTimeZone(timeZone);
+ Date date;
+ try {
+ date = dateFormat.parse(dateTimeString);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Failed to parse string: " + dateTimeString, e);
+ }
+ if (date == null) {
+ throw new IllegalArgumentException("Failed to parse string: " + dateTimeString);
+ }
+ return DateTimeWithZone.create(date.getTime(), timeZone);
+ }
+
+ /** Returns a default {@link Action} instance. */
+ public static Action createAction(@Nullable String title, @Nullable CarIcon icon) {
+ return Action.builder().setTitle(title).setIcon(icon).setOnClickListener(() -> {
+ }).build();
+ }
+
+ /** Returns an {@link ItemList} with the given number of rows and selectable state. */
+ public static ItemList createItemList(int rowCount, boolean isSelectable) {
+ return createItemListWithDistanceSpan(rowCount, isSelectable, null);
+ }
+
+ /**
+ * Returns an {@link ItemList} with the given selectable state and number of rows populated with
+ * the given {@link DistanceSpan}.
+ */
+ public static ItemList createItemListWithDistanceSpan(
+ int rowCount, boolean isSelectable, @Nullable DistanceSpan distanceSpan) {
+ ItemList.Builder builder = ItemList.builder();
+ for (int i = 0; i < rowCount; ++i) {
+ Row.Builder rowBuilder = Row.builder();
+ if (distanceSpan != null) {
+ SpannableString title = new SpannableString(" title " + i);
+ title.setSpan(distanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ rowBuilder.setTitle(title);
+ } else {
+ rowBuilder.setTitle("title " + i);
+ }
+ builder.addItem(rowBuilder.build());
+ }
+
+ if (isSelectable) {
+ builder.setSelectable(index -> {
+ });
+ }
+
+ return builder.build();
+ }
+
+ /** Returns a {@link Pane} with the given number of rows and actions */
+ public static Pane createPane(int rowCount, int actionCount) {
+ Pane.Builder builder = Pane.builder();
+ for (int i = 0; i < rowCount; ++i) {
+ builder.addRow(Row.builder().setTitle("title " + i).build());
+ }
+
+ List<Action> actions = new ArrayList<>();
+ for (int i = 0; i < actionCount; i++) {
+ actions.add(createAction("action " + i, null));
+ }
+ builder.setActions(actions);
+
+ return builder.build();
+ }
+
+ /** Returns a list of {@link SectionedItemList} with the given parameters. */
+ public static List<SectionedItemList> createSections(
+ int sectionCount, int rowCountPerSection, boolean isSelectable) {
+ List<SectionedItemList> sections = new ArrayList<>();
+
+ for (int i = 0; i < sectionCount; i++) {
+ sections.add(
+ SectionedItemList.create(
+ createItemList(rowCountPerSection, isSelectable),
+ CarText.create("Section " + i)));
+ }
+
+ return sections;
+ }
+
+ /** Returns an {@link ItemList} consisting of {@link GridItem}s */
+ public static ItemList getGridItemList(int itemCount) {
+ ItemList.Builder builder = ItemList.builder();
+ while (itemCount-- > 0) {
+ builder.addItem(GridItem.builder().setTitle("Title").setImage(BACK).build());
+ }
+ return builder.build();
+ }
+
+ @RequiresApi(26)
+ public static void assertDateTimeWithZoneEquals(
+ ZonedDateTime zonedDateTime, DateTimeWithZone dateTimeWithZone) {
+ assertThat(dateTimeWithZone.getZoneShortName())
+ .isEqualTo(zonedDateTime.getZone().getDisplayName(TextStyle.SHORT,
+ Locale.getDefault()));
+ assertThat(dateTimeWithZone.getZoneOffsetSeconds())
+ .isEqualTo(dateTimeWithZone.getZoneOffsetSeconds());
+ assertThat(dateTimeWithZone.getTimeSinceEpochMillis())
+ .isEqualTo(dateTimeWithZone.getTimeSinceEpochMillis());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
new file mode 100644
index 0000000..e6efb98
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ActionStrip}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionStripTest {
+ @Test
+ public void createEmpty_throws() {
+ assertThrows(IllegalStateException.class, () -> ActionStrip.builder().build());
+ }
+
+ @Test
+ public void addDuplicatedTypes_throws() {
+ Action action1 = Action.BACK;
+ Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
+ }).build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActionStrip.builder().addAction(action1).addAction(action1));
+
+ // Duplicated custom types will not throw.
+ ActionStrip.builder().addAction(action1).addAction(action2).addAction(action2).build();
+ }
+
+ @Test
+ public void createActions() {
+ Action action1 = Action.BACK;
+ Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
+ }).build();
+ ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+
+ assertThat(list.getActions()).hasSize(2);
+ assertThat(action1).isEqualTo(list.getActions().get(0));
+ assertThat(action2).isEqualTo(list.getActions().get(1));
+ }
+
+ @Test
+ public void clearActions() {
+ Action action1 = Action.BACK;
+ Action action2 = Action.APP_ICON;
+ ActionStrip list =
+ ActionStrip.builder()
+ .addAction(action1)
+ .addAction(action2)
+ .clearActions()
+ .addAction(action2)
+ .build();
+ assertThat(list.getActions()).hasSize(1);
+ }
+
+ @Test
+ public void getActionOfType() {
+ Action action1 = Action.BACK;
+ Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
+ }).build();
+ ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+
+ assertThat(list.getActionOfType(Action.TYPE_BACK)).isEqualTo(action1);
+ assertThat(list.getActionOfType(Action.TYPE_CUSTOM)).isEqualTo(action2);
+ }
+
+ @Test
+ public void equals() {
+ Action action1 = Action.BACK;
+ Action action2 = Action.APP_ICON;
+ ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+ ActionStrip list2 = ActionStrip.builder().addAction(action1).addAction(action2).build();
+
+ assertThat(list2).isEqualTo(list);
+ }
+
+ @Test
+ public void notEquals() {
+ Action action1 = Action.BACK;
+ Action action2 = Action.APP_ICON;
+ ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+ ActionStrip list2 = ActionStrip.builder().addAction(action2).build();
+
+ assertThat(list).isNotEqualTo(list2);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
new file mode 100644
index 0000000..7ac962a
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link Action}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private IOnDoneCallback.Stub mMockOnDoneCallback;
+
+ @Test
+ public void create_throws_noTitleOrIcon() {
+ assertThrows(
+ IllegalStateException.class, () -> Action.builder().setOnClickListener(() -> {
+ }).build());
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.builder().setOnClickListener(() -> {
+ }).setTitle("").build());
+ }
+
+ @Test
+ public void create_throws_invalid_carIcon() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+
+ assertThrows(IllegalArgumentException.class, () -> Action.builder().setIcon(carIcon));
+ }
+
+ @Test
+ public void create_throws_customBackgroundColor() {
+ OnClickListener >
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Action.builder()
+ .setTitle("foo")
+ .setOnClickListener(onClickListener)
+ .setBackgroundColor(CarColor.createCustom(0xdead, 0xbeef))
+ .build());
+ }
+
+ @Test
+ public void create_noTitleDefault() {
+ OnClickListener >
+ Action action =
+ Action.builder()
+ .setIcon(
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .setOnClickListener(onClickListener)
+ .build();
+ assertThat(action.getTitle()).isNull();
+ }
+
+ @Test
+ public void create_noIconDefault() {
+ OnClickListener >
+ Action action = Action.builder().setTitle("foo").setOnClickListener(
+ onClickListener).build();
+ assertThat(action.getIcon()).isNull();
+ }
+
+ @Test
+ public void create_noBackgroundColorDefault() {
+ OnClickListener >
+ Action action = Action.builder().setTitle("foo").setOnClickListener(
+ onClickListener).build();
+ assertThat(action.getBackgroundColor()).isEqualTo(CarColor.DEFAULT);
+ }
+
+ @Test
+ public void createInstance() throws RemoteException {
+ OnClickListener >
+ IconCompat icon =
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ String title = "foo";
+ Action action =
+ Action.builder()
+ .setTitle(title)
+ .setIcon(CarIcon.of(icon))
+ .setBackgroundColor(CarColor.BLUE)
+ .setOnClickListener(onClickListener)
+ .build();
+ assertThat(icon).isEqualTo(action.getIcon().getIcon());
+ assertThat(CarText.create(title)).isEqualTo(action.getTitle());
+ assertThat(CarColor.BLUE).isEqualTo(action.getBackgroundColor());
+ // TODO(shiufai): revisit the following as the test is not running on the main looper
+ // thread, and thus the verify is failing.
+// action.getOnClickListener().getListener().onClick(mockOnDoneCallback);
+// verify(onClickListener).onClick();
+ }
+
+ @Test
+ public void create_invalidSetOnBackThrows() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.BACK.newBuilder().setOnClickListener(() -> {
+ }).build());
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.BACK.newBuilder().setTitle("BACK").build());
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.BACK.newBuilder().setIcon(CarIcon.ALERT).build());
+ }
+
+ @Test
+ public void create_invalidSetOnAppIconThrows() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.APP_ICON.newBuilder().setOnClickListener(() -> {
+ }).build());
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.APP_ICON.newBuilder().setTitle("APP").build());
+ assertThrows(
+ IllegalStateException.class,
+ () -> Action.APP_ICON.newBuilder().setIcon(CarIcon.ALERT).build());
+ }
+
+ @Test
+ public void equals() {
+ String title = "foo";
+ CarIcon icon = CarIcon.ALERT;
+
+ Action action1 =
+ Action.builder().setOnClickListener(() -> {
+ }).setTitle(title).setIcon(icon).build();
+ Action action2 =
+ Action.builder().setOnClickListener(() -> {
+ }).setTitle(title).setIcon(icon).build();
+
+ assertThat(action2).isEqualTo(action1);
+ }
+
+ @Test
+ public void notEquals_nonMatchingTitle() {
+ String title = "foo";
+ Action action1 = Action.builder().setOnClickListener(() -> {
+ }).setTitle(title).build();
+ Action action2 = Action.builder().setOnClickListener(() -> {
+ }).setTitle("not foo").build();
+
+ assertThat(action2).isNotEqualTo(action1);
+ }
+
+ @Test
+ public void notEquals_nonMatchingIcon() {
+ String title = "foo";
+ CarIcon icon1 = CarIcon.ALERT;
+ CarIcon icon2 = CarIcon.APP_ICON;
+
+ Action action1 =
+ Action.builder().setOnClickListener(() -> {
+ }).setTitle(title).setIcon(icon1).build();
+ Action action2 =
+ Action.builder().setOnClickListener(() -> {
+ }).setTitle(title).setIcon(icon2).build();
+
+ assertThat(action2).isNotEqualTo(action1);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
new file mode 100644
index 0000000..2439237
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link CarIconSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CarIconSpanTest {
+ private IconCompat mIcon;
+
+ @Before
+ public void setup() {
+ mIcon =
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ }
+
+ @Test
+ public void constructor() {
+ CarIcon carIcon = CarIcon.of(mIcon);
+ CarIconSpan span = CarIconSpan.create(carIcon);
+
+ assertThat(span.getIcon()).isEqualTo(carIcon);
+ }
+
+ @Test
+ public void constructor_invalidCarIcon_throws() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+ assertThrows(IllegalArgumentException.class, () -> CarIconSpan.create(carIcon));
+ }
+
+ @Test
+ public void equals() {
+ CarIcon carIcon = CarIcon.of(mIcon);
+ CarIconSpan span1 = CarIconSpan.create(carIcon);
+ CarIconSpan span2 = CarIconSpan.create(carIcon);
+
+ assertThat(span2).isEqualTo(span1);
+ }
+
+ @Test
+ public void notEquals() {
+ CarIcon carIcon = CarIcon.of(mIcon);
+ CarIconSpan span1 = CarIconSpan.create(carIcon);
+ CarIconSpan span2 = CarIconSpan.create(CarIcon.ALERT);
+
+ assertThat(span2).isNotEqualTo(span1);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
new file mode 100644
index 0000000..1152c7e
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.car.app.model.CarColor.BLUE;
+import static androidx.car.app.model.CarColor.DEFAULT;
+import static androidx.car.app.model.CarColor.GREEN;
+import static androidx.car.app.model.CarIcon.BACK;
+import static androidx.car.app.model.CarIcon.TYPE_BACK;
+import static androidx.car.app.model.CarIcon.TYPE_CUSTOM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/** Tests for {@link CarIcon}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CarIconTest {
+ private IconCompat mIcon;
+
+ @Before
+ public void setup() {
+ mIcon =
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ }
+
+ @Test
+ public void of() {
+ CarIcon carIcon = CarIcon.of(mIcon);
+
+ assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+ assertThat(carIcon.getTint()).isNull();
+ assertThat(carIcon.getIcon()).isEqualTo(mIcon);
+ }
+
+ @Test
+ public void build_withTint() {
+ CarIcon carIcon = CarIcon.builder(mIcon).setTint(BLUE).build();
+
+ assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+ assertThat(carIcon.getTint()).isEqualTo(BLUE);
+ assertThat(carIcon.getIcon()).isEqualTo(mIcon);
+ }
+
+ @Test
+ public void build_noTint() {
+ CarIcon carIcon = CarIcon.builder(mIcon).build();
+
+ assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+ assertThat(carIcon.getTint()).isNull();
+ assertThat(mIcon).isEqualTo(carIcon.getIcon());
+ }
+
+ @Test
+ public void newBuilder_fromStandard() {
+ CarIcon carIcon = BACK.newBuilder().setTint(GREEN).build();
+
+ assertThat(carIcon.getType()).isEqualTo(TYPE_BACK);
+ assertThat(carIcon.getTint()).isEqualTo(GREEN);
+ assertThat(carIcon.getIcon()).isEqualTo(BACK.getIcon());
+ }
+
+ @Test
+ public void standard_defaultTint() {
+ assertThat(BACK.getTint()).isEqualTo(DEFAULT);
+ }
+
+ // TODO(shiufai): Add content uri equality test once we support content URI.
+ @Test
+ public void icon_from_uri() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+
+ assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+ assertThat(carIcon.getTint()).isNull();
+ assertThat(carIcon.getIcon().getType()).isEqualTo(IconCompat.TYPE_URI);
+ }
+
+ @Test
+ public void custom_icon_unsupported_scheme() {
+ // Create an icon URI with "file://" scheme.
+ Uri iconUri = Uri.fromFile(new File("foo/bar"));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CarIcon.builder(IconCompat.createWithContentUri(iconUri)));
+ }
+
+ @Test
+ public void custom_icon_unsupported_types() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CarIcon.builder(IconCompat.createWithAdaptiveBitmapContentUri("foo/bar")));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CarIcon.builder(IconCompat.createWithData(new byte[0], 1, 1)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ CarIcon.builder(
+ IconCompat.createWithAdaptiveBitmap(
+ Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8))));
+ }
+
+ @Test
+ public void equals() {
+ assertThat(BACK.equals(BACK)).isTrue();
+ CarIcon carIcon = CarIcon.of(mIcon);
+
+ assertThat(
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1)))
+ .isEqualTo(carIcon);
+ }
+
+ @Test
+ public void notEquals() {
+ assertThat(BACK.newBuilder().setTint(GREEN).build()).isNotEqualTo(BACK);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
new file mode 100644
index 0000000..1844d51
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.car.app.TestUtils.assertDateTimeWithZoneEquals;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/** Tests for {@link DateTimeWithZone}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DateTimeWithZoneTest {
+ @Test
+ @SuppressWarnings("JdkObsolete")
+ public void create() {
+ GregorianCalendar calendar = new GregorianCalendar(2020, 4, 15, 2, 57, 0);
+ Date date = calendar.getTime();
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+ long timeSinceEpochMillis = date.getTime();
+ long timeZoneOffsetSeconds = MILLISECONDS.toSeconds(
+ timeZone.getOffset(timeSinceEpochMillis));
+ String zoneShortName = "PST";
+
+ DateTimeWithZone dateTimeWithZone =
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ zoneShortName);
+
+ assertThat(dateTimeWithZone.getTimeSinceEpochMillis()).isEqualTo(timeSinceEpochMillis);
+ assertThat(dateTimeWithZone.getZoneOffsetSeconds()).isEqualTo(timeZoneOffsetSeconds);
+ assertThat(dateTimeWithZone.getZoneShortName()).isEqualTo(zoneShortName);
+ }
+
+ @Test
+ public void create_argumentChecks() {
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+
+ long timeSinceEpochMillis = 123;
+ long timeZoneOffsetSeconds = MILLISECONDS.toSeconds(
+ timeZone.getOffset(timeSinceEpochMillis));
+ String zoneShortName = "PST";
+
+ // Negative time.
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ DateTimeWithZone.create(-1, (int) timeZoneOffsetSeconds, zoneShortName);
+ });
+
+ // Offset out of range.
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ DateTimeWithZone.create(timeSinceEpochMillis, 18 * 60 * 60 + 1, zoneShortName);
+ });
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ DateTimeWithZone.create(timeSinceEpochMillis, -18 * 60 * 60 - 1, zoneShortName);
+ });
+
+ // Null short name.
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ null);
+ });
+
+ // Empty short name.
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds, "");
+ });
+ }
+
+ @Test
+ @SuppressWarnings("JdkObsolete")
+ public void create_withTimeZone() {
+ GregorianCalendar calendar = new GregorianCalendar(2020, 4, 15, 2, 57, 0);
+ Date date = calendar.getTime();
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+ long timeSinceEpochMillis = date.getTime();
+
+ DateTimeWithZone dateTimeWithZone = DateTimeWithZone.create(timeSinceEpochMillis, timeZone);
+
+ long timeZoneOffsetSeconds = MILLISECONDS.toSeconds(
+ timeZone.getOffset(timeSinceEpochMillis));
+ String zoneShortName = "PST";
+
+ assertThat(dateTimeWithZone.getZoneOffsetSeconds()).isEqualTo(timeZoneOffsetSeconds);
+ assertThat(dateTimeWithZone.getTimeSinceEpochMillis()).isEqualTo(timeSinceEpochMillis);
+ assertThat(dateTimeWithZone.getZoneShortName()).isEqualTo(zoneShortName);
+ }
+
+ @Test
+ public void create_withTimeZone_argumentChecks() {
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+
+ // Negative time.
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ DateTimeWithZone.create(-1, timeZone);
+ });
+
+ // Null time zone.
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ DateTimeWithZone.create(123, null);
+ });
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ public void create_withZonedDateTime() {
+ ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
+ DateTimeWithZone dateTimeWithZone = DateTimeWithZone.create(zonedDateTime);
+
+ assertDateTimeWithZoneEquals(zonedDateTime, dateTimeWithZone);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ public void create_withZonedDateTime_argumentChecks() {
+ // Null date time.
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ DateTimeWithZone.create(null);
+ });
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ public void equals() {
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+ long timeSinceEpochMillis = System.currentTimeMillis();
+ long timeZoneOffsetSeconds =
+ Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+ String zoneShortName = "PST";
+
+ DateTimeWithZone dateTimeWithZone =
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ zoneShortName);
+
+ assertThat(dateTimeWithZone)
+ .isEqualTo(
+ DateTimeWithZone.create(
+ timeSinceEpochMillis, (int) timeZoneOffsetSeconds, zoneShortName));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ public void notEquals_differentTimeSinceEpoch() {
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+ long timeSinceEpochMillis = System.currentTimeMillis();
+ long timeZoneOffsetSeconds =
+ Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+ String zoneShortName = "PST";
+
+ DateTimeWithZone dateTimeWithZone =
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ zoneShortName);
+
+ assertThat(dateTimeWithZone)
+ .isNotEqualTo(
+ DateTimeWithZone.create(
+ timeSinceEpochMillis + 1, (int) timeZoneOffsetSeconds,
+ zoneShortName));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ public void notEquals_differentTimeZoneOffsetSeconds() {
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+ long timeSinceEpochMillis = System.currentTimeMillis();
+ long timeZoneOffsetSeconds =
+ Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+ String zoneShortName = "PST";
+
+ DateTimeWithZone dateTimeWithZone =
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ zoneShortName);
+
+ assertThat(dateTimeWithZone)
+ .isNotEqualTo(
+ DateTimeWithZone.create(
+ timeSinceEpochMillis, (int) timeZoneOffsetSeconds + 1,
+ zoneShortName));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ public void notEquals_differentTimeZone() {
+ TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+ long timeSinceEpochMillis = System.currentTimeMillis();
+ long timeZoneOffsetSeconds =
+ Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+ String zoneShortName = "PST";
+
+ DateTimeWithZone dateTimeWithZone =
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ zoneShortName);
+
+ assertThat(dateTimeWithZone)
+ .isNotEqualTo(
+ DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ "UTC"));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
new file mode 100644
index 0000000..a05bed7
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link DistanceSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DistanceSpanTest {
+ private final Distance mDistance =
+ Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS);
+
+ @Test
+ public void constructor() {
+ DistanceSpan span = DistanceSpan.create(mDistance);
+ assertThat(span.getDistance()).isEqualTo(mDistance);
+ }
+
+ @Test
+ public void equals() {
+ DistanceSpan span = DistanceSpan.create(mDistance);
+ assertThat(span).isEqualTo(DistanceSpan.create(mDistance));
+ }
+
+ @Test
+ public void notEquals() {
+ DistanceSpan span = DistanceSpan.create(mDistance);
+ assertThat(span)
+ .isNotEqualTo(
+ DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS)));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
new file mode 100644
index 0000000..91c7028
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.car.app.model.Distance.UNIT_KILOMETERS;
+import static androidx.car.app.model.Distance.UNIT_METERS;
+import static androidx.car.app.model.Distance.UNIT_YARDS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Distance}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DistanceTest {
+
+ private static final double DISPLAY_DISTANCE = 1.2d;
+ private static final int DISPLAY_UNIT = UNIT_KILOMETERS;
+ private static final double DELTA = 0.00001;
+
+ @Test
+ public void createInstance_negativeMeter() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Distance.create(/* displayDistance= */ -1, UNIT_METERS));
+ }
+
+ @Test
+ public void createInstance() {
+ Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+ assertThat(distance.getDisplayDistance()).isWithin(DELTA).of(DISPLAY_DISTANCE);
+ assertThat(distance.getDisplayUnit()).isEqualTo(DISPLAY_UNIT);
+ }
+
+ @Test
+ public void equals() {
+ Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+
+ assertThat(Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT)).isEqualTo(distance);
+ }
+
+ @Test
+ public void notEquals_differentDisplayValue() {
+ Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+
+ assertThat(Distance.create(DISPLAY_DISTANCE + 1, DISPLAY_UNIT)).isNotEqualTo(distance);
+ }
+
+ @Test
+ public void notEquals_differentDisplayUnit() {
+ Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+
+ assertThat(Distance.create(DISPLAY_DISTANCE, UNIT_YARDS)).isNotEqualTo(distance);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
new file mode 100644
index 0000000..e43f2e1
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link DurationSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DurationSpanTest {
+ @Test
+ public void constructor() {
+ DurationSpan span = DurationSpan.create(1);
+ assertThat(span.getDurationSeconds()).isEqualTo(1);
+ }
+
+ @Test
+ public void equals() {
+ DurationSpan span = DurationSpan.create(1);
+ assertThat(span).isEqualTo(DurationSpan.create(1));
+ }
+
+ @Test
+ public void notEquals() {
+ DurationSpan span = DurationSpan.create(1);
+ assertThat(span).isNotEqualTo(DurationSpan.create(2));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
new file mode 100644
index 0000000..4f320f0
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.car.app.model.CarColor.BLUE;
+import static androidx.car.app.model.CarColor.GREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link CarIconSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ForegroundCarColorSpanTest {
+ @Test
+ public void constructor() {
+ ForegroundCarColorSpan span = ForegroundCarColorSpan.create(BLUE);
+
+ assertThat(span.getColor()).isEqualTo(BLUE);
+ }
+
+ @Test
+ public void equals() {
+ ForegroundCarColorSpan span = ForegroundCarColorSpan.create(BLUE);
+ assertThat(ForegroundCarColorSpan.create(BLUE)).isEqualTo(span);
+ }
+
+ @Test
+ public void notEquals() {
+ ForegroundCarColorSpan span = ForegroundCarColorSpan.create(BLUE);
+ assertThat(ForegroundCarColorSpan.create(GREEN)).isNotEqualTo(span);
+ }
+
+ @Test
+ public void customColorDisallowed() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ForegroundCarColorSpan.create(CarColor.createCustom(0xdead, 0xbeef)));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
new file mode 100644
index 0000000..7fe88b4
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link GridItem}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GridItemTest {
+
+ @Test
+ public void create_defaultValues() {
+ GridItem gridItem = GridItem.builder().setImage(BACK).build();
+
+ assertThat(BACK).isEqualTo(gridItem.getImage());
+ assertThat(gridItem.getImageType()).isEqualTo(GridItem.IMAGE_TYPE_LARGE);
+ assertThat(gridItem.getTitle()).isNull();
+ assertThat(gridItem.getText()).isNull();
+ }
+
+ @Test
+ public void title_charSequence() {
+ String title = "foo";
+ GridItem gridItem = GridItem.builder().setTitle(title).setImage(BACK).build();
+
+ assertThat(CarText.create(title)).isEqualTo(gridItem.getTitle());
+ }
+
+ @Test
+ public void text_charSequence() {
+ String text = "foo";
+ GridItem gridItem = GridItem.builder().setTitle("title").setText(text).setImage(
+ BACK).build();
+
+ assertThat(CarText.create(text)).isEqualTo(gridItem.getText());
+ }
+
+ @Test
+ public void textWithoutTitle_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> GridItem.builder().setText("text").setImage(BACK).build());
+ }
+
+ @Test
+ public void create_noImage_throwsException() {
+ assertThrows(IllegalStateException.class, () -> GridItem.builder().setTitle("foo").build());
+ }
+
+ @Test
+ public void equals() {
+ String title = "title";
+ String text = "text";
+ GridItem gridItem = GridItem.builder().setTitle(title).setText(text).setImage(BACK).build();
+
+ assertThat(GridItem.builder().setTitle(title).setText(text).setImage(BACK).build())
+ .isEqualTo(gridItem);
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ String title = "title";
+ GridItem gridItem = GridItem.builder().setTitle(title).setImage(BACK).build();
+
+ assertThat(GridItem.builder().setTitle("foo").setImage(BACK).build()).isNotEqualTo(
+ gridItem);
+ }
+
+ @Test
+ public void notEquals_differentText() {
+ String title = "title";
+ String text = "text";
+ GridItem gridItem = GridItem.builder().setTitle(title).setText(text).setImage(BACK).build();
+
+ assertThat(GridItem.builder().setTitle(title).setText("foo").setImage(BACK).build())
+ .isNotEqualTo(gridItem);
+ }
+
+ @Test
+ public void notEquals_differentImage() {
+ GridItem gridItem = GridItem.builder().setImage(BACK).build();
+
+ assertThat(GridItem.builder().setImage(ALERT).build()).isNotEqualTo(gridItem);
+ }
+
+ @Test
+ public void notEquals_differentToggle() {
+ Toggle toggle1 = Toggle.builder(isChecked -> {
+ }).setChecked(true).build();
+ Toggle toggle2 = Toggle.builder(isChecked -> {
+ }).setChecked(false).build();
+ GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle1).build();
+
+ assertThat(GridItem.builder().setImage(BACK).setToggle(toggle2).build()).isNotEqualTo(
+ gridItem);
+ }
+
+// TODO(shiufai): revisit the following as the test is not running on the main looper thread, and
+// thus the verify is failing.
+// @Test
+// public void clickListener() throws RemoteException {
+// OnClickListener >
+// GridItem gridItem =
+// GridItem.builder().setImage(BACK).setOnClickListener(onClickListener).build();
+// gridItem.getOnClickListener().getListener().onClick(mock(IOnDoneCallback.class));
+// verify(onClickListener).onClick();
+// }
+
+ @Test
+ public void setToggle() {
+ Toggle toggle = Toggle.builder(isChecked -> {
+ }).build();
+ GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle).build();
+ assertThat(toggle).isEqualTo(gridItem.getToggle());
+ }
+
+ @Test
+ public void setOnClickListenerAndToggle_throws() {
+ Toggle toggle = Toggle.builder(isChecked -> {
+ }).build();
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ GridItem.builder()
+ .setImage(BACK)
+ .setOnClickListener(() -> {
+ })
+ .setToggle(toggle)
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
new file mode 100644
index 0000000..ef3832b
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.utils.Logger;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link GridTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GridTemplateTest {
+ private final Logger mLogger = message -> {
+ };
+
+ @Test
+ public void createInstance_emptyList_notLoading_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> GridTemplate.builder().setTitle("Title").build());
+
+ // Positive case
+ GridTemplate.builder().setTitle("Title").setLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_isLoading_hasList_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .setSingleList(TestUtils.getGridItemList(2))
+ .build());
+ }
+
+ @Test
+ public void createInstance_noHeaderTitleOrAction_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> GridTemplate.builder().setSingleList(TestUtils.getGridItemList(2)).build());
+
+ // Positive cases.
+ GridTemplate.builder().setTitle("Title").setSingleList(
+ TestUtils.getGridItemList(2)).build();
+ GridTemplate.builder()
+ .setHeaderAction(Action.BACK)
+ .setSingleList(TestUtils.getGridItemList(2))
+ .build();
+ }
+
+ @Test
+ public void createInstance_setSingleList() {
+ ItemList list = TestUtils.getGridItemList(2);
+ GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+ assertThat(template.getSingleList()).isEqualTo(list);
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ GridTemplate.builder()
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void createInstance_setHeaderAction() {
+ GridTemplate template =
+ GridTemplate.builder()
+ .setSingleList(TestUtils.getGridItemList(2))
+ .setHeaderAction(Action.BACK)
+ .build();
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ }
+
+ @Test
+ public void createInstance_setActionStrip() {
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ GridTemplate template =
+ GridTemplate.builder()
+ .setSingleList(TestUtils.getGridItemList(2))
+ .setTitle("Title")
+ .setActionStrip(actionStrip)
+ .build();
+ assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+ }
+
+ @Test
+ public void createInstance_setBackground() {
+ GridTemplate template =
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .setBackgroundImage(BACK)
+ .build();
+ assertThat(template.getBackgroundImage()).isEqualTo(BACK);
+ }
+
+ @Test
+ public void validate_fromLoadingState_isRefresh() {
+ GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
+ ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+ GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Going from loading state to new content is allowed.
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder().setTitle("Title").setLoading(true).build(),
+ mLogger))
+ .isTrue();
+ }
+
+ @Test
+ public void validate_mutableProperties_isRefresh() {
+ GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
+ ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+ GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Ensure a template is a refresh of itself.
+ assertThat(template.isRefresh(template, mLogger)).isTrue();
+
+ // Allowed mutable states.
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder()
+ .addItem(gridItem.setOnClickListener(() -> {
+ }).setImage(BACK).build())
+ .build())
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build(),
+ mLogger))
+ .isTrue();
+ }
+
+ @Test
+ public void validate_titleUpdate_isNotRefresh() {
+ ItemList list = ItemList.builder().build();
+ GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Title updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder().setSingleList(list).setTitle("Title2").build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_gridItemImageAndTextUpdates_isNotRefresh() {
+ GridItem.Builder gridItem =
+ GridItem.builder().setImage(BACK).setTitle("Title1").setText("Text1");
+ ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+ GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Ensure a template is a refresh of itself.
+ assertThat(template.isRefresh(template, mLogger)).isTrue();
+
+ // Image updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(
+ gridItem.setImage(ALERT).build()).build())
+ .build(),
+ mLogger))
+ .isFalse();
+
+ // Text updates are disallowed
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(
+ gridItem.setTitle("Title2").build()).build())
+ .build(),
+ mLogger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(
+ gridItem.setText("Text2").build()).build())
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_newGridItem_isNotRefresh() {
+ GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
+ ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+ GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Additional grid items are disallowed.
+ assertThat(
+ template.isRefresh(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder()
+ .addItem(gridItem.build())
+ .addItem(gridItem.build())
+ .build())
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_toLoadingState_isNotRefresh() {
+ // Going from content to loading state is disallowed.
+ assertThat(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .build()
+ .isRefresh(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(ItemList.builder().build())
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void resetList_clearsSingleList() {
+ GridTemplate.Builder builder =
+ GridTemplate.builder()
+ .setSingleList(TestUtils.getGridItemList(2))
+ .setHeaderAction(Action.BACK);
+
+ assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+ }
+
+ @Test
+ public void equals() {
+ ItemList itemList = ItemList.builder().build();
+ String title = "title";
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+ GridTemplate template =
+ GridTemplate.builder()
+ .setSingleList(itemList)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ GridTemplate.builder()
+ .setSingleList(itemList)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentItemList() {
+ ItemList itemList = ItemList.builder().build();
+
+ GridTemplate template =
+ GridTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ GridTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(
+ GridItem.builder().setImage(BACK).build()).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ ItemList itemList = ItemList.builder().build();
+
+ GridTemplate template =
+ GridTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ GridTemplate.builder()
+ .setSingleList(itemList)
+ .setHeaderAction(Action.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ ItemList itemList = ItemList.builder().build();
+ String title = "title";
+
+ GridTemplate template = GridTemplate.builder().setSingleList(itemList).setTitle(
+ title).build();
+
+ assertThat(template)
+ .isNotEqualTo(GridTemplate.builder().setSingleList(itemList).setTitle(
+ "foo").build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ ItemList itemList = ItemList.builder().build();
+ String title = "title";
+
+ GridTemplate template =
+ GridTemplate.builder()
+ .setSingleList(itemList)
+ .setTitle(title)
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ GridTemplate.builder()
+ .setSingleList(itemList)
+ .setTitle(title)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
new file mode 100644
index 0000000..056128a
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
@@ -0,0 +1,571 @@
+/*
+ * 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.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import android.os.RemoteException;
+import android.text.SpannableString;
+
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.model.ItemList.OnItemVisibilityChangedListener;
+import androidx.car.app.model.ItemList.OnSelectedListener;
+import androidx.car.app.utils.Logger;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+/** Tests for {@link ItemListTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ItemListTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private IOnDoneCallback.Stub mMockOnDoneCallback;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void createEmpty() {
+ ItemList list = ItemList.builder().build();
+ assertThat(list.getItems()).isEqualTo(Collections.emptyList());
+ }
+
+ @Test
+ public void createRows() {
+ Row row1 = Row.builder().setTitle("Row1").build();
+ Row row2 = Row.builder().setTitle("Row2").build();
+ ItemList list = ItemList.builder().addItem(row1).addItem(row2).build();
+
+ assertThat(list.getItems()).hasSize(2);
+ assertThat(list.getItems().get(0)).isEqualTo(row1);
+ assertThat(list.getItems().get(1)).isEqualTo(row2);
+ }
+
+ @Test
+ public void createGridItems() {
+ GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
+ GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
+ ItemList list = ItemList.builder().addItem(gridItem1).addItem(gridItem2).build();
+
+ assertThat(list.getItems()).containsExactly(gridItem1, gridItem2).inOrder();
+ }
+
+ @Test
+ public void clearRows() {
+ Row row1 = Row.builder().setTitle("Row1").build();
+ Row row2 = Row.builder().setTitle("Row2").build();
+ ItemList list = ItemList.builder().addItem(row1).addItem(row2).clearItems().build();
+
+ assertThat(list.getItems()).isEmpty();
+ }
+
+ @Test
+ public void clearGridItems() {
+ GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
+ GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
+ ItemList list = ItemList.builder().addItem(gridItem1).addItem(
+ gridItem2).clearItems().build();
+
+ assertThat(list.getItems()).isEmpty();
+ }
+
+ @Test
+ public void setSelectedable_emptyList_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> ItemList.builder().setSelectable(selectedIndex -> {
+ }).build());
+ }
+
+ @Test
+ public void setSelectedIndex_greaterThanListSize_throws() {
+ Row row1 = Row.builder().setTitle("Row1").build();
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ ItemList.builder()
+ .addItem(row1)
+ .setSelectable(selectedIndex -> {
+ })
+ .setSelectedIndex(2)
+ .build());
+ }
+
+ @Test
+ public void setSelectable() throws RemoteException {
+ OnSelectedListener mockListener = mock(OnSelectedListener.class);
+ ItemList itemList =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("title").build())
+ .setSelectable(mockListener)
+ .build();
+
+ // TODO(shiufai): revisit the following as the test is not running on the main looper
+ // thread, and thus the verify is failing.
+// itemList.getOnSelectedListener().onSelected(0, mockOnDoneCallback);
+// verify(mockListener).onSelected(eq(0));
+ }
+
+ @Test
+ public void setSelectable_disallowOnClickListenerInRows() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("foo").setOnClickListener(() -> {
+ }).build())
+ .setSelectable((index) -> {
+ })
+ .build());
+
+ // Positive test.
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("foo").build())
+ .setSelectable((index) -> {
+ })
+ .build();
+ }
+
+ @Test
+ public void setSelectable_disallowToggleInRow() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ ItemList.builder()
+ .addItem(Row.builder().setToggle(Toggle.builder(isChecked -> {
+ }).build()).build())
+ .setSelectable((index) -> {
+ })
+ .build());
+ }
+
+ @Test
+ public void setOnItemVisibilityChangeListener_triggerListener() throws RemoteException {
+ OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
+ ItemList list =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("1").build())
+ .setOnItemsVisibilityChangeListener(listener)
+ .build();
+
+ // TODO(shiufai): revisit the following as the test is not running on the main looper
+ // thread, and thus the verify is failing.
+// list.getOnItemsVisibilityChangeListener().onItemVisibilityChanged(0, 1,
+// mockOnDoneCallback);
+// ArgumentCaptor<Integer> startIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+// ArgumentCaptor<Integer> endIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+// verify(listener).onItemVisibilityChanged(startIndexCaptor.capture(),
+// endIndexCaptor.capture());
+// assertThat(startIndexCaptor.getValue()).isEqualTo(0);
+// assertThat(endIndexCaptor.getValue()).isEqualTo(1);
+ }
+
+ @Test
+ public void validateRows_isRefresh() {
+ Logger logger = message -> {
+ };
+ Row.Builder row = Row.builder().setTitle("Title1");
+ ItemList listWithRows = ItemList.builder().addItem(row.build()).build();
+
+ // Text updates are disallowed.
+ ItemList listWithDifferentTitle =
+ ItemList.builder().addItem(row.setTitle("Title2").build()).build();
+ ItemList listWithDifferentText =
+ ItemList.builder().addItem(row.addText("Text").build()).build();
+ assertThat(listWithDifferentTitle.isRefresh(listWithRows, logger)).isFalse();
+ assertThat(listWithDifferentText.isRefresh(listWithRows, logger)).isFalse();
+
+ // Additional rows are disallowed.
+ ItemList listWithTwoRows = ItemList.builder().addItem(row.build()).addItem(
+ row.build()).build();
+ assertThat(listWithTwoRows.isRefresh(listWithRows, logger)).isFalse();
+ }
+
+ @Test
+ public void validateGridItems_isRefresh() {
+ Logger logger = message -> {
+ };
+ GridItem.Builder gridItem = GridItem.builder().setImage(BACK).setTitle("Title1");
+ ItemList listWithGridItems = ItemList.builder().addItem(gridItem.build()).build();
+
+ // Text updates are disallowed.
+ ItemList listWithDifferentTitle =
+ ItemList.builder().addItem(gridItem.setTitle("Title2").build()).build();
+ ItemList listWithDifferentText =
+ ItemList.builder().addItem(gridItem.setText("Text").build()).build();
+ assertThat(listWithDifferentTitle.isRefresh(listWithGridItems, logger)).isFalse();
+ assertThat(listWithDifferentText.isRefresh(listWithGridItems, logger)).isFalse();
+
+ // Image updates are disallowed.
+ ItemList listWithDifferentImage =
+ ItemList.builder().addItem(gridItem.setImage(ALERT).build()).build();
+ assertThat(listWithDifferentImage.isRefresh(listWithGridItems, logger)).isFalse();
+
+ // Additional grid items are disallowed.
+ ItemList listWithTwoGridItems =
+ ItemList.builder().addItem(gridItem.build()).addItem(gridItem.build()).build();
+ assertThat(listWithTwoGridItems.isRefresh(listWithGridItems, logger)).isFalse();
+ }
+
+ @Test
+ public void validateRows_isRefresh_differentSpansAreIgnored() {
+ Logger logger = message -> {
+ };
+ SpannableString textWithDistanceSpan = new SpannableString("Text");
+ textWithDistanceSpan.setSpan(
+ DistanceSpan.create(Distance.create(1000, Distance.UNIT_KILOMETERS)),
+ /* start= */ 0,
+ /* end= */ 1,
+ /* flags= */ 0);
+ SpannableString textWithDurationSpan = new SpannableString("Text");
+ textWithDurationSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+
+ ItemList list1 =
+ ItemList.builder()
+ .addItem(
+ Row.builder().setTitle(textWithDistanceSpan).addText(
+ textWithDurationSpan).build())
+ .build();
+ ItemList list2 =
+ ItemList.builder()
+ .addItem(
+ Row.builder().setTitle(textWithDurationSpan).addText(
+ textWithDistanceSpan).build())
+ .build();
+ ItemList list3 =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("Text2").addText("Text2").build())
+ .build();
+
+ assertThat(list2.isRefresh(list1, logger)).isTrue();
+ assertThat(list3.isRefresh(list1, logger)).isFalse();
+ }
+
+ @Test
+ public void validateRows_isRefresh_differentToggleStatesAllowTextUpdates() {
+ Logger logger = message -> {
+ };
+ Toggle -> {
+ }).setChecked(true).build();
+ Toggle offToggle = Toggle.builder(isChecked -> {
+ }).setChecked(false).build();
+
+ ItemList listWithOnToggle =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("Title1").setToggle(onToggle).build())
+ .build();
+ ItemList listWithOffToggle =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("Title1").setToggle(offToggle).build())
+ .build();
+ ItemList listWithoutToggle =
+ ItemList.builder().addItem(Row.builder().setTitle("Title2").build()).build();
+ ItemList listWithOffToggleDifferentText =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("Title2").addText("Text").setToggle(
+ offToggle).build())
+ .build();
+ ItemList listWithOnToggleDifferentText =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("Title2").setToggle(onToggle).build())
+ .build();
+
+ // Going from toggle to no toggle is not a refresh.
+ assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
+
+ // Going from on toggle to off toggle, or vice versa, is always a refresh
+ assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentText, logger)).isTrue();
+ assertThat(listWithOffToggleDifferentText.isRefresh(listWithOnToggle, logger)).isTrue();
+
+ // If toggle state is the same, then text changes are not considered a refresh.
+ assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentText, logger)).isFalse();
+ assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentText, logger)).isFalse();
+ }
+
+ @Test
+ public void validateGridItems_isRefresh_differentToggleStatesAllowTextUpdates() {
+ Logger logger = message -> {
+ };
+ Toggle -> {
+ }).setChecked(true).build();
+ Toggle offToggle = Toggle.builder(isChecked -> {
+ }).setChecked(false).build();
+
+ ItemList listWithOnToggle =
+ ItemList.builder()
+ .addItem(
+ GridItem.builder().setImage(BACK).setTitle("Title1").setToggle(
+ onToggle).build())
+ .build();
+ ItemList listWithOffToggle =
+ ItemList.builder()
+ .addItem(
+ GridItem.builder().setImage(BACK).setTitle("Title1").setToggle(
+ offToggle).build())
+ .build();
+ ItemList listWithoutToggle =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title2").build())
+ .build();
+ ItemList listWithOffToggleDifferentText =
+ ItemList.builder()
+ .addItem(
+ GridItem.builder()
+ .setImage(BACK)
+ .setTitle("Title2")
+ .setText("Text")
+ .setToggle(offToggle)
+ .build())
+ .build();
+ ItemList listWithOnToggleDifferentText =
+ ItemList.builder()
+ .addItem(
+ GridItem.builder().setImage(BACK).setTitle("Title2").setToggle(
+ onToggle).build())
+ .build();
+
+ // Going from toggle to no toggle is not a refresh.
+ assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
+
+ // Going from on toggle to off toggle, or vice versa, is always a refresh
+ assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentText, logger)).isTrue();
+ assertThat(listWithOffToggleDifferentText.isRefresh(listWithOnToggle, logger)).isTrue();
+
+ // If toggle state is the same, then text changes are not considered a refresh.
+ assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentText, logger)).isFalse();
+ assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentText, logger)).isFalse();
+ }
+
+ @Test
+ public void validateGridItems_isRefresh_differentToggleStatesAllowImageUpdates() {
+ Logger logger = message -> {
+ };
+ Toggle -> {
+ }).setChecked(true).build();
+ Toggle offToggle = Toggle.builder(isChecked -> {
+ }).setChecked(false).build();
+
+ ItemList listWithOnToggle =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setToggle(onToggle).build())
+ .build();
+ ItemList listWithOffToggle =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setToggle(offToggle).build())
+ .build();
+ ItemList listWithoutToggle =
+ ItemList.builder().addItem(GridItem.builder().setImage(ALERT).build()).build();
+ ItemList listWithOffToggleDifferentImage =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(ALERT).setToggle(offToggle).build())
+ .build();
+ ItemList listWithOnToggleDifferentImage =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(ALERT).setToggle(onToggle).build())
+ .build();
+
+ // Going from toggle to no toggle is not a refresh.
+ assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
+
+ // Going from on toggle to off toggle, or vice versa, is always a refresh
+ assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentImage, logger)).isTrue();
+ assertThat(listWithOffToggleDifferentImage.isRefresh(listWithOnToggle, logger)).isTrue();
+
+ // If toggle state is the same, then image changes are not considered a refresh.
+ assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentImage, logger)).isFalse();
+ assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentImage, logger)).isFalse();
+ }
+
+ @Test
+ public void validateGridItems_isRefresh_differentSelectedIndexAllowTextUpdates() {
+ Logger logger = message -> {
+ };
+ OnSelectedListener >
+
+ ItemList listWithItem0Selected =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title11").build())
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title12").build())
+ .setSelectable(onSelectedListener)
+ .setSelectedIndex(0)
+ .build();
+ ItemList listWithItem1Selected =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
+ .setSelectable(onSelectedListener)
+ .setSelectedIndex(1)
+ .build();
+ ItemList listWithItem0SelectedDifferentText =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
+ .setSelectable(onSelectedListener)
+ .setSelectedIndex(0)
+ .build();
+ ItemList listWithoutOnSelectedListener =
+ ItemList.builder()
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
+ .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
+ .build();
+
+ // Selecting item 1 from item 0, or vice versa, is always a refresh.
+ assertThat(listWithItem0Selected.isRefresh(listWithItem1Selected, logger)).isTrue();
+ assertThat(listWithItem1Selected.isRefresh(listWithItem0Selected, logger)).isTrue();
+
+ // If item selection is the same, it is not considered a refresh
+ assertThat(listWithItem0Selected.isRefresh(listWithItem0SelectedDifferentText, logger))
+ .isFalse();
+
+ // If one of the ItemList doesn't have a selectable state, it is not a refresh.
+ assertThat(
+ listWithItem0Selected.isRefresh(listWithoutOnSelectedListener, logger)).isFalse();
+ }
+
+ @Test
+ public void equals_itemListWithRows() {
+ Row row = Row.builder().setTitle("Title").build();
+ ItemList itemList =
+ ItemList.builder()
+ .setSelectable((index) -> {
+ })
+ .setNoItemsMessage("no items")
+ .setSelectedIndex(0)
+ .setOnItemsVisibilityChangeListener((start, end) -> {
+ })
+ .addItem(row)
+ .build();
+ assertThat(itemList)
+ .isEqualTo(
+ ItemList.builder()
+ .setSelectable((index) -> {
+ })
+ .setNoItemsMessage("no items")
+ .setSelectedIndex(0)
+ .setOnItemsVisibilityChangeListener((start, end) -> {
+ })
+ .addItem(row)
+ .build());
+ }
+
+ @Test
+ public void equals_itemListWithGridItems() {
+ GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
+ ItemList itemList =
+ ItemList.builder()
+ .setSelectable((index) -> {
+ })
+ .setNoItemsMessage("no items")
+ .setSelectedIndex(0)
+ .setOnItemsVisibilityChangeListener((start, end) -> {
+ })
+ .addItem(gridItem)
+ .build();
+ assertThat(itemList)
+ .isEqualTo(
+ ItemList.builder()
+ .setSelectable((index) -> {
+ })
+ .setNoItemsMessage("no items")
+ .setSelectedIndex(0)
+ .setOnItemsVisibilityChangeListener((start, end) -> {
+ })
+ .addItem(gridItem)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentNoItemsMessage() {
+ ItemList itemList = ItemList.builder().setNoItemsMessage("no items").build();
+ assertThat(itemList).isNotEqualTo(ItemList.builder().setNoItemsMessage("YO").build());
+ }
+
+ @Test
+ public void notEquals_differentSelectedIndex() {
+ Row row = Row.builder().setTitle("Title").build();
+ ItemList itemList =
+ ItemList.builder().setSelectable((index) -> {
+ }).addItem(row).addItem(row).build();
+ assertThat(itemList)
+ .isNotEqualTo(
+ ItemList.builder()
+ .setSelectable((index) -> {
+ })
+ .setSelectedIndex(1)
+ .addItem(row)
+ .addItem(row)
+ .build());
+ }
+
+ @Test
+ public void notEquals_missingSelectedListener() {
+ Row row = Row.builder().setTitle("Title").build();
+ ItemList itemList =
+ ItemList.builder().setSelectable((index) -> {
+ }).addItem(row).addItem(row).build();
+ assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).addItem(row).build());
+ }
+
+ @Test
+ public void notEquals_missingVisibilityChangedListener() {
+ Row row = Row.builder().setTitle("Title").build();
+ ItemList itemList =
+ ItemList.builder()
+ .setOnItemsVisibilityChangeListener((start, end) -> {
+ })
+ .addItem(row)
+ .addItem(row)
+ .build();
+ assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).addItem(row).build());
+ }
+
+ @Test
+ public void notEquals_differentRows() {
+ Row row = Row.builder().setTitle("Title").build();
+ ItemList itemList = ItemList.builder().addItem(row).addItem(row).build();
+ assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).build());
+ }
+
+ @Test
+ public void notEquals_differentGridItems() {
+ GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
+ ItemList itemList = ItemList.builder().addItem(gridItem).addItem(gridItem).build();
+ assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(gridItem).build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
new file mode 100644
index 0000000..c170d65
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link LatLng}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LatLngTest {
+ @Test
+ public void createInstance() {
+ LatLng location = LatLng.create(123.f, 456.f);
+ assertThat(location.getLatitude()).isWithin(0.001).of(123.f);
+ assertThat(location.getLongitude()).isWithin(0.001).of(456.f);
+ }
+
+ @Test
+ public void equals() {
+ LatLng latLng = LatLng.create(123.45, 987.65);
+
+ assertThat(LatLng.create(123.45, 987.65)).isEqualTo(latLng);
+ }
+
+ @Test
+ public void notEquals_differentLat() {
+ LatLng latLng = LatLng.create(123.45, 987.65);
+
+ assertThat(LatLng.create(123.449999999, 987.65)).isNotEqualTo(latLng);
+ assertThat(LatLng.create(123.450000001, 987.65)).isNotEqualTo(latLng);
+ }
+
+ @Test
+ public void notEquals_differentLng() {
+ LatLng latLng = LatLng.create(123.45, 987.65);
+
+ assertThat(LatLng.create(123.45, 987.64999999999)).isNotEqualTo(latLng);
+ assertThat(LatLng.create(123.45, 987.65000000001)).isNotEqualTo(latLng);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
new file mode 100644
index 0000000..fd05fa8c
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
@@ -0,0 +1,531 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ListTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ListTemplateTest {
+ private final Logger mLogger = message -> {
+ };
+
+ @Test
+ public void createInstance_emptyList_notLoading_Throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> ListTemplate.builder().setTitle("Title").build());
+
+ // Positive case
+ ListTemplate.builder().setTitle("Title").setLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_isLoading_hasList_Throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .setSingleList(getList())
+ .build());
+ }
+
+ @Test
+ public void addEmptyList_throws() {
+ ItemList emptyList = ItemList.builder().build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ListTemplate.builder().setTitle("Title").addList(emptyList,
+ "header").build());
+ }
+
+ @Test
+ public void addList_emptyHeader_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ListTemplate.builder().setTitle("Title").addList(getList(), "").build());
+ }
+
+ @Test
+ public void resetList_clearsSingleList() {
+ ListTemplate.Builder builder =
+ ListTemplate.builder().setTitle("Title").setSingleList(getList());
+ assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+ }
+
+ @Test
+ public void resetList_clearsMultipleLists() {
+ ListTemplate.Builder builder =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .addList(getList(), "header1")
+ .addList(getList(), "header2");
+ assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+ }
+
+ @Test
+ public void addList_withVisibilityListener_throws() {
+ ItemList list =
+ ItemList.builder()
+ .addItem(Row.builder().setTitle("Title").build())
+ .setOnItemsVisibilityChangeListener((start, end) -> {
+ })
+ .build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ListTemplate.builder().setTitle("Title").addList(list, "header").build());
+ }
+
+ @Test
+ public void addList_moreThanMaxTexts_throws() {
+ Row rowExceedsMaxTexts =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+ "text3").build();
+ Row rowMeetingMaxTexts =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(rowExceedsMaxTexts).build())
+ .build());
+
+ // Positive case.
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+ .build();
+ }
+
+ @Test
+ public void createInstance_noHeaderTitleOrAction_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> ListTemplate.builder().setSingleList(getList()).build());
+
+ // Positive cases/.
+ ListTemplate.builder().setTitle("Title").setSingleList(getList()).build();
+ ListTemplate.builder().setHeaderAction(Action.BACK).setSingleList(getList()).build();
+ }
+
+ @Test
+ public void createInstance_setSingleList() {
+ ItemList list = getList();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+ assertThat(template.getSingleList()).isEqualTo(list);
+ assertThat(template.getSectionLists()).isEmpty();
+ }
+
+ @Test
+ public void createInstance_addList() {
+ ItemList list1 = getList();
+ ItemList list2 = getList();
+ ListTemplate template =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .addList(list1, "header1")
+ .addList(list2, "header2")
+ .build();
+ assertThat(template.getSingleList()).isNull();
+ assertThat(template.getSectionLists()).hasSize(2);
+ assertThat(template.getSectionLists().get(0).getItemList()).isEqualTo(list1);
+ assertThat(template.getSectionLists().get(0).getHeader().getText()).isEqualTo("header1");
+ assertThat(template.getSectionLists().get(1).getItemList()).isEqualTo(list2);
+ assertThat(template.getSectionLists().get(1).getHeader().getText()).isEqualTo("header2");
+ }
+
+ @Test
+ public void setSingleList_clearLists() {
+ ItemList list1 = getList();
+ ItemList list2 = getList();
+ ItemList list3 = getList();
+ ListTemplate template =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .addList(list1, "header1")
+ .addList(list2, "header2")
+ .setSingleList(list3)
+ .build();
+ assertThat(template.getSingleList()).isEqualTo(list3);
+ assertThat(template.getSectionLists()).isEmpty();
+ }
+
+ @Test
+ public void addList_clearSingleList() {
+ ItemList list1 = getList();
+ ItemList list2 = getList();
+ ItemList list3 = getList();
+ ListTemplate template =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(list1)
+ .addList(list2, "header1")
+ .addList(list3, "header2")
+ .build();
+ assertThat(template.getSingleList()).isNull();
+ assertThat(template.getSectionLists()).hasSize(2);
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ListTemplate.builder()
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void createInstance_setHeaderAction() {
+ ListTemplate template =
+ ListTemplate.builder().setSingleList(getList()).setHeaderAction(
+ Action.BACK).build();
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ }
+
+ @Test
+ public void createInstance_setActionStrip() {
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ ListTemplate template =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(getList())
+ .setActionStrip(actionStrip)
+ .build();
+ assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+ }
+
+ @Test
+ public void validate_fromLoadingState_isRefresh() {
+ Row.Builder row = Row.builder().setTitle("Row1");
+ ItemList list = ItemList.builder().addItem(row.build()).build();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Going from loading state to new content is allowed.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder().setTitle("Title").setLoading(true).build(),
+ mLogger))
+ .isTrue();
+ }
+
+ @Test
+ public void validate_mutableProperties_isRefresh() {
+ Row.Builder row = Row.builder().setTitle("Row1");
+ ItemList list = ItemList.builder().addItem(row.build()).build();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Ensure a template is a refresh of itself.
+ assertThat(template.isRefresh(template, mLogger)).isTrue();
+
+ // Allowed mutable states.
+ SpannableString stringWithSpan = new SpannableString("Row1");
+ stringWithSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+ IconCompat icon = IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder()
+ .addItem(
+ row.setOnClickListener(() -> {
+ })
+ .setBrowsable(true)
+ .setTitle(stringWithSpan)
+ .setImage(CarIcon.of(icon))
+ .build())
+ .build())
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build(),
+ mLogger))
+ .isTrue();
+ }
+
+ @Test
+ public void validate_multipleListMutableProperties_isRefresh() {
+ Row row = Row.builder().setTitle("Row1").build();
+ Row refreshRow =
+ Row.builder()
+ .setTitle("Row1")
+ .setOnClickListener(() -> {
+ })
+ .setBrowsable(true)
+ .setImage(
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .build();
+ ItemList list = ItemList.builder().addItem(row).build();
+ ListTemplate template =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .addList(list, "header1")
+ .addList(list, "header2")
+ .build();
+
+ // Sublist refreshes are allowed as long as headers and number of sections remain the same.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .addList(ItemList.builder().addItem(refreshRow).build(), "header1")
+ .addList(ItemList.builder().addItem(refreshRow).build(), "header2")
+ .build(),
+ mLogger))
+ .isTrue();
+ }
+
+ @Test
+ public void validate_titleUpdate_isNotRefresh() {
+ ItemList list = ItemList.builder().build();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Title updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder().setSingleList(list).setTitle("Title2").build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_rowTextUpdate_isNotRefresh() {
+ Row.Builder row = Row.builder().setTitle("Row1");
+ ItemList list = ItemList.builder().addItem(row.build()).build();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Text updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(ItemList.builder().addItem(
+ row.setTitle("Row2").build()).build())
+ .build(),
+ mLogger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(ItemList.builder().addItem(
+ row.addText("Text").build()).build())
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_newRow_isNotRefresh() {
+ Row.Builder row = Row.builder().setTitle("Row1");
+ ItemList list = ItemList.builder().addItem(row.build()).build();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Additional rows are disallowed.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(row.build()).addItem(
+ row.build()).build())
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_multipleList_headerChange_isNotRefresh() {
+ Row.Builder row = Row.builder().setTitle("Row1");
+ ItemList list = ItemList.builder().addItem(row.build()).build();
+ ListTemplate template =
+ ListTemplate.builder().setTitle("Title").addList(list, "header1").build();
+
+ // Addition of lists are disallowed.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder().setTitle("Title").addList(list, "header2").build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_newList_isNotRefresh() {
+ Row.Builder row = Row.builder().setTitle("Row1");
+ ItemList list = ItemList.builder().addItem(row.build()).build();
+ ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+ list).build();
+
+ // Addition of lists are disallowed.
+ assertThat(
+ template.isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .addList(list, "header1")
+ .addList(list, "header2")
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void validate_toLoadingState_isNotRefresh() {
+ // Going from content to loading state is disallowed.
+ assertThat(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .build()
+ .isRefresh(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(ItemList.builder().build())
+ .build(),
+ mLogger))
+ .isFalse();
+ }
+
+ @Test
+ public void equals() {
+ ItemList itemList = ItemList.builder().build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ String title = "title";
+
+ ListTemplate template =
+ ListTemplate.builder()
+ .setSingleList(itemList)
+ .setActionStrip(actionStrip)
+ .setHeaderAction(Action.BACK)
+ .setTitle(title)
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ ListTemplate.builder()
+ .setSingleList(itemList)
+ .setActionStrip(actionStrip)
+ .setHeaderAction(Action.BACK)
+ .setTitle(title)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentItemList() {
+ ItemList itemList = ItemList.builder().build();
+
+ ListTemplate template =
+ ListTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(
+ ItemList.builder().addItem(
+ Row.builder().setTitle("Title").build()).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ ItemList itemList = ItemList.builder().build();
+
+ ListTemplate template =
+ ListTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ ListTemplate.builder()
+ .setSingleList(itemList)
+ .setHeaderAction(Action.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ ItemList itemList = ItemList.builder().build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+ ListTemplate template =
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(itemList)
+ .setActionStrip(actionStrip)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ ListTemplate.builder()
+ .setTitle("Title")
+ .setSingleList(itemList)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ ItemList itemList = ItemList.builder().build();
+ String title = "title";
+
+ ListTemplate template = ListTemplate.builder().setSingleList(itemList).setTitle(
+ title).build();
+
+ assertThat(template)
+ .isNotEqualTo(ListTemplate.builder().setSingleList(itemList).setTitle(
+ "yo").build());
+ }
+
+ private static ItemList getList() {
+ Row row1 = Row.builder().setTitle("Bananas").build();
+ Row row2 = Row.builder().setTitle("Oranges").build();
+ return ItemList.builder().addItem(row1).addItem(row2).build();
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
new file mode 100644
index 0000000..4784044
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MessageTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MessageTemplateTest {
+
+ private final String mTitle = "header";
+ private final String mDebugMessage = "debugMessage";
+ private final Throwable mCause = new IllegalStateException("bad");
+ private final String mMessage = "foo";
+ private final Action mAction = Action.BACK;
+ private final CarIcon mIcon = CarIcon.ALERT;
+
+ @Test
+ public void emptyMessage_throws() {
+ assertThrows(
+ IllegalStateException.class, () -> MessageTemplate.builder("").setTitle(
+ mTitle).build());
+ }
+
+ @Test
+ public void invalidCarIcon_throws() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> MessageTemplate.builder("hello").setTitle(mTitle).setIcon(carIcon));
+ }
+
+ @Test
+ public void noHeaderTitleOrAction_throws() {
+ assertThrows(IllegalStateException.class, () -> MessageTemplate.builder(mMessage).build());
+
+ // Positive cases.
+ MessageTemplate.builder(mMessage).setTitle(mTitle).build();
+ MessageTemplate.builder(mMessage).setHeaderAction(mAction).build();
+ }
+
+ @Test
+ public void createDefault_valuesAreNull() {
+ MessageTemplate template = MessageTemplate.builder(mMessage).setTitle(mTitle).build();
+ assertThat(template.getMessage().toString()).isEqualTo(mMessage);
+ assertThat(template.getTitle().getText()).isEqualTo("header");
+ assertThat(template.getIcon()).isNull();
+ assertThat(template.getHeaderAction()).isNull();
+ assertThat(template.getActionList()).isNull();
+ assertThat(template.getDebugMessage()).isNull();
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ MessageTemplate.builder(mMessage)
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void createWithContents_hasProperValuesSet() {
+ Throwable exception = new IllegalStateException();
+ CarIcon icon = BACK;
+ Action action = Action.builder().setOnClickListener(() -> {
+ }).setTitle("foo").build();
+
+ MessageTemplate template =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setHeaderAction(Action.BACK)
+ .setDebugCause(exception)
+ .setIcon(icon)
+ .setActions(ImmutableList.of(action))
+ .build();
+
+ assertThat(template.getMessage().toString()).isEqualTo(mMessage);
+ assertThat(template.getTitle().toString()).isEqualTo(mTitle);
+ assertThat(template.getDebugMessage().toString()).isEqualTo(
+ Log.getStackTraceString(exception));
+ assertThat(template.getIcon()).isEqualTo(icon);
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ assertThat(template.getActionList().getList()).containsExactly(action);
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+ MessageTemplate template = MessageTemplate.builder(mMessage).setTitle(mTitle).build();
+
+ assertThat(template.isRefresh(template, logger)).isTrue();
+
+ // Allowed mutable fields: icon, action strip and actions.
+ Action action = Action.builder().setOnClickListener(() -> {
+ }).setTitle("foo").build();
+ assertThat(
+ template.isRefresh(
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setIcon(
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .setHeaderAction(Action.BACK)
+ .setActions(ImmutableList.of(action))
+ .build(),
+ logger))
+ .isTrue();
+
+ // Text changes are disallowed.
+ assertThat(
+ template.isRefresh(MessageTemplate.builder("Message2").setTitle(mTitle).build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ MessageTemplate.builder(mMessage).setTitle(mTitle).setDebugMessage(
+ "Debug").build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugCause(new IllegalArgumentException("Exception"))
+ .build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ MessageTemplate.builder(mMessage).setTitle("Header2").build(), logger))
+ .isFalse();
+ }
+
+ @Test
+ public void equals() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setHeaderAction(Action.BACK)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setHeaderAction(Action.BACK)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentDebugMessage() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage("yo")
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentCause() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(new IllegalStateException("something else bad"))
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentMessage() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setMessage(mMessage)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder("bar")
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setHeaderAction(Action.BACK)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setHeaderAction(Action.APP_ICON)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentActions() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction, mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentIcon() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(CarIcon.ERROR)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ MessageTemplate template1 =
+ MessageTemplate.builder(mMessage)
+ .setTitle(mTitle)
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+ MessageTemplate template2 =
+ MessageTemplate.builder(mMessage)
+ .setTitle("Header2")
+ .setDebugMessage(mDebugMessage)
+ .setDebugCause(mCause)
+ .setActions(ImmutableList.of(mAction))
+ .setIcon(mIcon)
+ .build();
+
+ assertThat(template1).isNotEqualTo(template2);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
new file mode 100644
index 0000000..a67ff2d
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for the {@link Metadata} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MetadataTest {
+ @Test
+ public void setAndGetPlace() {
+ Place place = Place.builder(
+ LatLng.create(/* latitude= */ 123, /* longitude= */ 456)).build();
+ Metadata metadata = Metadata.ofPlace(place);
+ assertThat(metadata.getPlace()).isEqualTo(place);
+
+ metadata = Metadata.builder().build();
+ assertThat(metadata.getPlace()).isNull();
+ }
+
+ @Test
+ public void equals() {
+ Place place = Place.builder(
+ LatLng.create(/* latitude= */ 123, /* longitude= */ 456)).build();
+ Metadata metadata = Metadata.builder().setPlace(place).build();
+
+ assertThat(Metadata.builder().setPlace(place).build()).isEqualTo(metadata);
+ }
+
+ @Test
+ public void notEquals_differentPlace() {
+ Place place = Place.builder(
+ LatLng.create(/* latitude= */ 123, /* longitude= */ 456)).build();
+ Metadata metadata = Metadata.builder().setPlace(place).build();
+
+ Place place2 = Place.builder(
+ LatLng.create(/* latitude= */ 456, /* longitude= */ 789)).build();
+
+ assertThat(Metadata.builder().setPlace(place2).build()).isNotEqualTo(metadata);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
new file mode 100644
index 0000000..4a82bff
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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 org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceListMapTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ModelUtilsTest {
+ @Test
+ public void validateAllNonBrowsableRowsHaveDistances() {
+ DistanceSpan span =
+ DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+ SpannableString stringWithDistance = new SpannableString("Test");
+ stringWithDistance.setSpan(span, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ SpannableString stringWithInvalidDistance = new SpannableString("Test");
+ // 0-length span is not allowed.
+ stringWithInvalidDistance.setSpan(span, /* start= */ 0, /* end= */ 0, /* flags= */ 0);
+
+ Row rowWithDistance = Row.builder().setTitle(stringWithDistance).build();
+ Row rowWithDistance2 = Row.builder().setTitle("Title").addText(stringWithDistance).build();
+ Row rowWithInvalidDistance = Row.builder().setTitle(stringWithInvalidDistance).build();
+ Row rowWithoutDistance = Row.builder().setTitle("Test").build();
+ Row browsableRowWithoutPlace =
+ Row.builder().setTitle("Test").setBrowsable(true).setOnClickListener(() -> {
+ }).build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(rowWithDistance, rowWithInvalidDistance)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(rowWithDistance, rowWithoutDistance)));
+
+ // Positive cases
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(ImmutableList.of());
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(ImmutableList.of(rowWithDistance));
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(rowWithDistance, rowWithDistance2));
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(rowWithDistance, browsableRowWithoutPlace));
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(browsableRowWithoutPlace));
+ }
+
+ @Test
+ public void validateAllRowsHaveDurationsOrDistances() {
+ DistanceSpan distanceSpan =
+ DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+ DurationSpan durationSpan = DurationSpan.create(1);
+
+ SpannableString stringWithDistance = new SpannableString("Test");
+ stringWithDistance.setSpan(distanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+
+ SpannableString stringWithDuration = new SpannableString("Test");
+ stringWithDuration.setSpan(durationSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+
+ SpannableString stringWithInvalidDuration = new SpannableString("Test");
+ // 0-length span is not allowed.
+ stringWithInvalidDuration.setSpan(durationSpan, /* start= */ 0, /* end= */ 0, /* flags= */
+ 0);
+
+ Row rowWithDistance = Row.builder().setTitle(stringWithDistance).build();
+ Row rowWithDuration = Row.builder().setTitle(stringWithDuration).build();
+ Row rowWithDuration2 = Row.builder().setTitle("Title").addText(stringWithDuration).build();
+ Row rowWithInvalidDuration = Row.builder().setTitle(stringWithInvalidDuration).build();
+ Row plainRow = Row.builder().setTitle("Test").build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(
+ ImmutableList.of(rowWithDuration, rowWithInvalidDuration)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(
+ ImmutableList.of(rowWithDuration, plainRow)));
+
+ // Positive cases.
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of());
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of(rowWithDistance));
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of(rowWithDuration));
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(
+ ImmutableList.of(rowWithDuration, rowWithDuration2));
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(
+ ImmutableList.of(rowWithDuration, rowWithDistance));
+ }
+
+ @Test
+ public void validateAllRowsHaveOnlySmallSizedImages() {
+ CarIcon carIcon =
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ Row rowWithNoImage = Row.builder().setTitle("title1").build();
+ Row rowWithSmallImage =
+ Row.builder().setTitle("title2").setImage(carIcon, Row.IMAGE_TYPE_SMALL).build();
+ Row rowWithLargeImage =
+ Row.builder().setTitle("title3").setImage(carIcon, Row.IMAGE_TYPE_LARGE).build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ModelUtils.validateAllRowsHaveOnlySmallImages(
+ ImmutableList.of(rowWithLargeImage)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ModelUtils.validateAllRowsHaveOnlySmallImages(
+ ImmutableList.of(rowWithNoImage, rowWithLargeImage)));
+
+ // Positive cases
+ ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of());
+ ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of(rowWithNoImage));
+ ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of(rowWithSmallImage));
+ ModelUtils.validateAllRowsHaveOnlySmallImages(
+ ImmutableList.of(rowWithNoImage, rowWithSmallImage));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
new file mode 100644
index 0000000..eb80ae2
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/** Tests for {@link OnClickListenerWrapper}. */
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.RemoteException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class OnClickListenerWrapperTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ OnClickListener mMockOnClickListener;
+
+ @Test
+ public void create() throws RemoteException {
+ OnClickListenerWrapper wrapper = OnClickListenerWrapper.create(mMockOnClickListener);
+ assertThat(wrapper.isParkedOnly()).isFalse();
+
+ // TODO(shiufai): revisit the following as the test is not running on the main looper
+ // thread, and thus the verify is failing.
+// wrapper.getListener().onClick(mock(IOnDoneCallback.class));
+// verify(mockOnClickListener).onClick();
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
new file mode 100644
index 0000000..6275cd6
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PaneTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PaneTemplateTest {
+
+ @Test
+ public void pane_moreThanMaxActions_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PaneTemplate.builder(TestUtils.createPane(2, 3)).setTitle("Title").build());
+
+ // Positive cases.
+ PaneTemplate.builder(TestUtils.createPane(2, 2)).setTitle("Title").build();
+ }
+
+ @Test
+ public void pane_moreThanMaxTexts_throws() {
+ Row rowExceedsMaxTexts =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+ "text3").build();
+ Row rowMeetingMaxTexts =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PaneTemplate.builder(Pane.builder().addRow(rowExceedsMaxTexts).build())
+ .setTitle("Title")
+ .build());
+
+ // Positive cases.
+ PaneTemplate.builder(Pane.builder().addRow(rowMeetingMaxTexts).build())
+ .setTitle("Title")
+ .build();
+ }
+
+ @Test
+ public void pane_toggleOrClickListener_throws() {
+ Row rowWithToggle =
+ Row.builder().setTitle("Title").setToggle(Toggle.builder(isChecked -> {
+ }).build()).build();
+ Row rowWithClickListener = Row.builder().setTitle("Title").setOnClickListener(() -> {
+ }).build();
+ Row rowMeetingRestrictions =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PaneTemplate.builder(Pane.builder().addRow(rowWithToggle).build())
+ .setTitle("Title")
+ .build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PaneTemplate.builder(Pane.builder().addRow(rowWithClickListener).build())
+ .setTitle("Title")
+ .build());
+
+ // Positive cases.
+ PaneTemplate.builder(Pane.builder().addRow(rowMeetingRestrictions).build())
+ .setTitle("Title")
+ .build();
+ }
+
+ @Test
+ public void createInstance_noHeaderTitleOrAction_throws() {
+ assertThrows(IllegalStateException.class, () -> PaneTemplate.builder(getPane()).build());
+
+ // Positive cases.
+ PaneTemplate.builder(getPane()).setTitle("Title").build();
+ PaneTemplate.builder(getPane()).setHeaderAction(Action.BACK).build();
+ }
+
+ @Test
+ public void createInstance_setPane() {
+ Pane pane = getPane();
+ PaneTemplate template = PaneTemplate.builder(pane).setTitle("Title").build();
+ assertThat(template.getPane()).isEqualTo(pane);
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PaneTemplate.builder(getPane())
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void createInstance_setHeaderAction() {
+ PaneTemplate template = PaneTemplate.builder(getPane()).setHeaderAction(
+ Action.BACK).build();
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ }
+
+ @Test
+ public void createInstance_setActionStrip() {
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ PaneTemplate template =
+ PaneTemplate.builder(getPane()).setTitle("Title").setActionStrip(
+ actionStrip).build();
+ assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+ Row.Builder row = Row.builder().setTitle("Row1");
+ PaneTemplate template =
+ PaneTemplate.builder(Pane.builder().addRow(row.build()).build()).setTitle(
+ "Title").build();
+
+ assertThat(template.isRefresh(template, logger)).isTrue();
+
+ // Going from loading state to new content is allowed.
+ assertThat(
+ template.isRefresh(
+ PaneTemplate.builder(Pane.builder().setLoading(true).build())
+ .setTitle("Title")
+ .build(),
+ logger))
+ .isTrue();
+
+ // Other allowed mutable states.
+ SpannableString stringWithSpan = new SpannableString("Row1");
+ stringWithSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+ IconCompat icon = IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ assertThat(
+ template.isRefresh(
+ PaneTemplate.builder(
+ Pane.builder()
+ .addRow(
+ row.setImage(CarIcon.of(icon))
+ .setTitle(stringWithSpan)
+ .build())
+ .build())
+ .setTitle("Title")
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build(),
+ logger))
+ .isTrue();
+
+ // Title updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ PaneTemplate.builder(Pane.builder().addRow(row.build()).build())
+ .setTitle("Title2")
+ .build(),
+ logger))
+ .isFalse();
+
+ // Text updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ PaneTemplate.builder(
+ Pane.builder().addRow(row.setTitle("Row2").build()).build())
+ .setTitle("Title")
+ .build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ PaneTemplate.builder(
+ Pane.builder().addRow(row.addText("Text").build()).build())
+ .setTitle("Title")
+ .build(),
+ logger))
+ .isFalse();
+
+ // Additional rows are disallowed.
+ assertThat(
+ template.isRefresh(
+ PaneTemplate.builder(Pane.builder().addRow(row.build()).addRow(
+ row.build()).build())
+ .setTitle("Title")
+ .build(),
+ logger))
+ .isFalse();
+
+ // Going from content to loading state is disallowed.
+ assertThat(
+ PaneTemplate.builder(Pane.builder().setLoading(true).build())
+ .setTitle("Title")
+ .build()
+ .isRefresh(template, logger))
+ .isFalse();
+ }
+
+ @Test
+ public void equals() {
+ Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ String title = "foo";
+
+ PaneTemplate template =
+ PaneTemplate.builder(pane)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ PaneTemplate.builder(pane)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentPane() {
+ Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ String title = "foo";
+
+ PaneTemplate template =
+ PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(title).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PaneTemplate.builder(
+ Pane.builder().addRow(
+ Row.builder().setTitle("Title2").build()).build())
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+
+ PaneTemplate template = PaneTemplate.builder(pane).setHeaderAction(Action.BACK).build();
+
+ assertThat(template)
+ .isNotEqualTo(PaneTemplate.builder(pane).setHeaderAction(Action.APP_ICON).build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ String title = "foo";
+
+ PaneTemplate template =
+ PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(title).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PaneTemplate.builder(pane)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .setTitle(title)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ String title = "foo";
+
+ PaneTemplate template =
+ PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(title).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(
+ "bar").build());
+ }
+
+ private static Pane getPane() {
+ Row row1 = Row.builder().setTitle("Bananas").build();
+ Row row2 = Row.builder().setTitle("Oranges").build();
+ return Pane.builder().addRow(row1).addRow(row2).build();
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
new file mode 100644
index 0000000..2531bd6
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.utils.Logger;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Tests for {@link Pane}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PaneTest {
+ @Test
+ public void createEmptyRows_throws() {
+ assertThrows(IllegalStateException.class, () -> Pane.builder().build());
+
+ // Positive case
+ Pane.builder().setLoading(true).build();
+ }
+
+ @Test
+ public void isLoading_withRows_throws() {
+ Row row = createRow(1);
+ assertThrows(
+ IllegalStateException.class, () -> Pane.builder().addRow(row).setLoading(
+ true).build());
+
+ // Positive case
+ Pane.builder().addRow(row).build();
+ }
+
+ @Test
+ public void addRow() {
+ Row row = createRow(1);
+ Pane pane = Pane.builder().addRow(row).build();
+ assertThat(pane.getRows()).containsExactly(row);
+ }
+
+ @Test
+ public void clearRows() {
+ Row row = createRow(1);
+ Pane pane = Pane.builder().addRow(row).addRow(row).clearRows().addRow(row).build();
+ assertThat(pane.getRows()).hasSize(1);
+ }
+
+ @Test
+ public void addRow_multiple() {
+ Row row1 = createRow(1);
+ Row row2 = createRow(2);
+ Row row3 = createRow(3);
+ Pane pane = Pane.builder().addRow(row1).addRow(row2).addRow(row3).build();
+ assertThat(pane.getRows()).containsExactly(row1, row2, row3);
+ }
+
+ @Test
+ public void setActions() {
+ Action action1 = createAction(1);
+ Action action2 = createAction(2);
+ List<Action> actions = Arrays.asList(action1, action2);
+ Pane pane =
+ Pane.builder().addRow(Row.builder().setTitle("Title").build()).setActions(
+ actions).build();
+ assertActions(pane.getActionList(), actions);
+ }
+
+ @Test
+ public void setActions_throwsIfNullAction() {
+ Action action1 = createAction(1);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Pane.builder().setActions(Arrays.asList(action1, null)).build());
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+ Row.Builder row = Row.builder().setTitle("Title1");
+
+ Pane.Builder builder = Pane.builder().setLoading(true);
+ Pane pane = builder.build();
+ assertThat(pane.isRefresh(builder.build(), logger)).isTrue();
+
+ // Going from loading state to new content is allowed.
+ Pane paneWithRows = Pane.builder().addRow(row.build()).build();
+ assertThat(paneWithRows.isRefresh(pane, logger)).isTrue();
+
+ // Text updates are disallowed.
+ Pane paneWithDifferentTitle = Pane.builder().addRow(row.setTitle("Title2").build()).build();
+ Pane paneWithDifferentText = Pane.builder().addRow(row.addText("Text").build()).build();
+ assertThat(paneWithDifferentTitle.isRefresh(paneWithRows, logger)).isFalse();
+ assertThat(paneWithDifferentText.isRefresh(paneWithRows, logger)).isFalse();
+
+ // Additional rows are disallowed.
+ Pane paneWithTwoRows = Pane.builder().addRow(row.build()).addRow(row.build()).build();
+ assertThat(paneWithTwoRows.isRefresh(paneWithRows, logger)).isFalse();
+
+ // Going from content to loading state is disallowed.
+ assertThat(pane.isRefresh(paneWithRows, logger)).isFalse();
+ }
+
+ @Test
+ public void validate_isRefresh_differentSpansAreIgnored() {
+ Logger logger = message -> {
+ };
+ SpannableString textWithDistanceSpan = new SpannableString("Text");
+ textWithDistanceSpan.setSpan(
+ DistanceSpan.create(Distance.create(1000, Distance.UNIT_KILOMETERS)),
+ /* start= */ 0,
+ /* end= */ 1,
+ /* flags= */ 0);
+ SpannableString textWithDurationSpan = new SpannableString("Text");
+ textWithDurationSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+
+ Pane pane1 =
+ Pane.builder()
+ .addRow(
+ Row.builder().setTitle(textWithDistanceSpan).addText(
+ textWithDurationSpan).build())
+ .build();
+ Pane pane2 =
+ Pane.builder()
+ .addRow(
+ Row.builder().setTitle(textWithDurationSpan).addText(
+ textWithDistanceSpan).build())
+ .build();
+ Pane pane3 =
+ Pane.builder().addRow(Row.builder().setTitle("Text2").addText(
+ "Text2").build()).build();
+
+ assertThat(pane2.isRefresh(pane1, logger)).isTrue();
+ assertThat(pane3.isRefresh(pane1, logger)).isFalse();
+ }
+
+ @Test
+ public void equals() {
+ Pane pane =
+ Pane.builder()
+ .setLoading(false)
+ .setActions(ImmutableList.of(Action.APP_ICON, Action.BACK))
+ .addRow(Row.builder().setTitle("Title").build())
+ .build();
+
+ assertThat(pane)
+ .isEqualTo(
+ Pane.builder()
+ .setLoading(false)
+ .setActions(ImmutableList.of(Action.APP_ICON, Action.BACK))
+ .addRow(Row.builder().setTitle("Title").build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentLoading() {
+ Pane pane =
+ Pane.builder().setLoading(false).addRow(
+ Row.builder().setTitle("Title").build()).build();
+
+ assertThat(pane).isNotEqualTo(Pane.builder().setLoading(true).build());
+ }
+
+ @Test
+ public void notEquals_differentActionsAdded() {
+ Row row = Row.builder().setTitle("Title").build();
+ Pane pane =
+ Pane.builder()
+ .addRow(row)
+ .setActions(ImmutableList.of(Action.APP_ICON, Action.BACK))
+ .build();
+
+ assertThat(pane)
+ .isNotEqualTo(
+ Pane.builder().addRow(row).setActions(
+ ImmutableList.of(Action.APP_ICON)).build());
+ }
+
+ @Test
+ public void notEquals_differentRow() {
+ Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+
+ assertThat(pane)
+ .isNotEqualTo(
+ Pane.builder()
+ .addRow(Row.builder().setTitle("Title").setOnClickListener(() -> {
+ }).build())
+ .build());
+ }
+
+ private static Row createRow(int suffix) {
+ return Row.builder().setTitle("The title " + suffix).addText(
+ "The subtitle " + suffix).build();
+ }
+
+ private static Action createAction(int suffix) {
+ return Action.builder().setTitle("Action " + suffix).setOnClickListener(() -> {
+ }).build();
+ }
+
+ private static void assertActions(Object obj, List<Action> expectedActions) {
+ ActionList actionList = (ActionList) obj;
+ assertThat(actionList.getList()).containsExactlyElementsIn(expectedActions);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
new file mode 100644
index 0000000..c4076f6
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+/** Tests for {@link OnClickListenerWrapper}. */
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.RemoteException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ParkedOnlyOnClickListenerTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ OnClickListener mMockOnClickListener;
+
+ @Test
+ public void create() throws RemoteException {
+ ParkedOnlyOnClickListener parkedOnlyOnClickListener =
+ ParkedOnlyOnClickListener.create(mMockOnClickListener);
+ OnClickListenerWrapper wrapper = OnClickListenerWrapper.create(parkedOnlyOnClickListener);
+
+ assertThat(wrapper.isParkedOnly()).isTrue();
+ // TODO(shiufai): revisit the following as the test is not running on the main looper
+ // thread, and thus the verify is failing.
+// wrapper.getListener().onClick(mock(IOnDoneCallback.class));
+// verify(mockOnClickListener).onClick();
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
new file mode 100644
index 0000000..16257d0
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
@@ -0,0 +1,620 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceListMapTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceListMapTemplateTest {
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final DistanceSpan mDistanceSpan =
+ DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+
+ @Test
+ public void createInstance_emptyList_notLoading_Throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PlaceListMapTemplate.builder().setTitle("Title").build());
+
+ // Positive case
+ PlaceListMapTemplate.builder().setTitle("Title").setLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_isLoading_hasList_Throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .setItemList(ItemList.builder().build())
+ .build());
+ }
+
+ @Test
+ public void addList_selectable_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, true,
+ mDistanceSpan))
+ .build());
+
+ // Positive cases.
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .build();
+ }
+
+ @Test
+ public void addList_moreThanMaxTexts_throws() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row rowExceedsMaxTexts =
+ Row.builder().setTitle(title).addText("text1").addText("text2").addText(
+ "text3").build();
+ Row rowMeetingMaxTexts =
+ Row.builder().setTitle(title).addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowExceedsMaxTexts).build())
+ .build());
+
+ // Positive cases.
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+ .build();
+ }
+
+ @Test
+ public void addList_hasToggle_throws() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row rowWithToggle =
+ Row.builder().setTitle(title).setToggle(Toggle.builder(isChecked -> {
+ }).build()).build();
+ Row rowMeetingRestrictions =
+ Row.builder().setTitle(title).addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowWithToggle).build())
+ .build());
+
+ // Positive cases.
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowMeetingRestrictions).build())
+ .build();
+ }
+
+ @Test
+ public void createEmpty() {
+ ItemList itemList = ItemList.builder().build();
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder().setTitle("Title").setItemList(itemList).build();
+ assertThat(template.getItemList()).isEqualTo(itemList);
+ assertThat(template.getHeaderAction()).isNull();
+ assertThat(template.getActionStrip()).isNull();
+ assertThat(template.isCurrentLocationEnabled()).isFalse();
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void createInstance_setHeaderAction() {
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setItemList(ItemList.builder().build())
+ .setHeaderAction(Action.BACK)
+ .build();
+
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ }
+
+ @Test
+ public void createInstance_notAllRowHaveDistances() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row rowWithDistance = Row.builder().setTitle(title).build();
+ Row rowWithoutDistance = Row.builder().setTitle("Google Kir").build();
+ Row browsableRowWithoutDistance =
+ Row.builder()
+ .setTitle("Google Kir")
+ .setBrowsable(true)
+ .setOnClickListener(() -> {
+ })
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(rowWithDistance)
+ .addItem(rowWithoutDistance)
+ .build()));
+
+ // Positive cases
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowWithDistance).build())
+ .build();
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(rowWithDistance)
+ .addItem(browsableRowWithoutDistance)
+ .build())
+ .build();
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(browsableRowWithoutDistance).build())
+ .build();
+ }
+
+ @Test
+ public void createInstance_rowHasBothMarkerAndImages() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row row =
+ Row.builder()
+ .setTitle("Google Kir")
+ .setOnClickListener(() -> {
+ })
+ .setImage(CarIcon.ALERT)
+ .setMetadata(
+ Metadata.ofPlace(
+ Place.builder(LatLng.create(10.f, 10.f))
+ .setMarker(PlaceMarker.getDefault())
+ .build()))
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(row).build()));
+ }
+
+ @Test
+ public void createInstance_setItemList() {
+ ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder().setTitle("Title").setItemList(itemList).build();
+ assertThat(template.getItemList()).isEqualTo(itemList);
+ }
+
+ @Test
+ public void createInstance_setActionStrip() {
+ ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(itemList)
+ .setActionStrip(actionStrip)
+ .build();
+ assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+ }
+
+ @Test
+ public void createInstance_setTitle() {
+ ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+ String title = "title";
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setItemList(itemList)
+ .setTitle(title)
+ .setCurrentLocationEnabled(true)
+ .build();
+
+ assertThat(template.getTitle().getText()).isEqualTo(title);
+ }
+
+ @Test
+ public void createInstance_noHeaderTitleOrAction_throws() {
+ ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ PlaceListMapTemplate.builder()
+ .setItemList(itemList)
+ .setCurrentLocationEnabled(true)
+ .build());
+
+ // Positive cases.
+ PlaceListMapTemplate.builder().setTitle("Title").setItemList(itemList).build();
+ PlaceListMapTemplate.builder().setHeaderAction(Action.BACK).setItemList(itemList).build();
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, 0, 1, 0);
+ Row.Builder row =
+ Row.builder().setTitle(title).setBrowsable(true).setOnClickListener(() -> {
+ });
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(row.build()).build())
+ .build();
+
+ assertThat(template.isRefresh(template, logger)).isTrue();
+
+ // Going from loading state to new content is allowed.
+ assertThat(
+ template.isRefresh(
+ PlaceListMapTemplate.builder().setTitle("Title").setLoading(true).build(),
+ logger))
+ .isTrue();
+
+ // Other allowed mutable states.
+ SpannableString stringWithSpan = new SpannableString("Title");
+ stringWithSpan.setSpan(mDistanceSpan, 1, /* end= */ 2, /* flags= */ 0);
+ ItemList itemList = ItemList.builder()
+ .addItem(
+ row.setOnClickListener(() -> {
+ })
+ .setBrowsable(false)
+ .setTitle(stringWithSpan)
+ .setImage(
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .setMetadata(
+ Metadata.ofPlace(
+ Place.builder(
+ LatLng.create(
+ 1,
+ 1)).build()))
+ .build())
+ .build();
+ assertThat(
+ template.isRefresh(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(itemList)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build(),
+ logger))
+ .isTrue();
+
+ // Title updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ PlaceListMapTemplate.builder()
+ .setItemList(ItemList.builder().addItem(row.build()).build())
+ .setTitle("Title2")
+ .build(),
+ logger))
+ .isFalse();
+
+ // Text updates are disallowed.
+ SpannableString title2 = new SpannableString("Title2");
+ title2.setSpan(mDistanceSpan, 0, 1, 0);
+ assertThat(
+ template.isRefresh(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(
+ row.setTitle(title2).build()).build())
+ .build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(row.setTitle(title).addText(
+ "Text").build())
+ .build())
+ .build(),
+ logger))
+ .isFalse();
+
+ // Additional rows are disallowed.
+ assertThat(
+ template.isRefresh(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder().addItem(row.build()).addItem(
+ row.build()).build())
+ .build(),
+ logger))
+ .isFalse();
+
+ // Going from content to loading state is disallowed.
+ assertThat(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setLoading(true)
+ .build()
+ .isRefresh(template, logger))
+ .isFalse();
+ }
+
+ @Test
+ public void equals() {
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ String title = "foo";
+ Place place =
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("A").build())
+ .build();
+
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .setAnchor(place)
+ .setCurrentLocationEnabled(true)
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ PlaceListMapTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setTitle(title)
+ .setAnchor(place)
+ .setCurrentLocationEnabled(true)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentList() {
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setHeaderAction(Action.BACK)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListMapTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setHeaderAction(Action.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setActionStrip(actionStrip)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setTitle("foo")
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListMapTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setTitle("bar")
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentAnchor() {
+ Place place1 =
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("A").build())
+ .build();
+
+ Place place2 =
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("B").build())
+ .build();
+
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setAnchor(place1)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setAnchor(place2)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentLocationEnabled() {
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setCurrentLocationEnabled(true)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setCurrentLocationEnabled(false)
+ .build());
+ }
+
+// TODO(shiufai): the following shadow is resulting in a ClasscastException.
+// Further investigation is needed.
+// @Test
+// public void checkPermissions_hasPermissions() {
+// PlaceListMapTemplate template =
+// PlaceListMapTemplate.builder()
+// .setTitle("Title")
+// .setItemList(
+// TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+// .setCurrentLocationEnabled(true)
+// .build();
+//
+// PackageManager packageManager = mContext.getPackageManager();
+// PackageInfo pi = new PackageInfo();
+// pi.packageName = mContext.getPackageName();
+// pi.versionCode = 1;
+// pi.requestedPermissions = new String[]{permission.ACCESS_FINE_LOCATION};
+//
+// shadowOf(packageManager).installPackage(pi);
+//
+// // Expect that it does not throw
+// template.checkPermissions(context);
+// }
+
+// TODO(shiufai): the following shadow is resulting in a ClasscastException.
+// Further investigation is needed.
+// @Test
+// public void checkPermissions_doesNotHaveFineLocationPermission() {
+// PlaceListMapTemplate template =
+// PlaceListMapTemplate.builder()
+// .setTitle("Title")
+// .setItemList(
+// TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+// .setCurrentLocationEnabled(true)
+// .build();
+//
+// PackageManager packageManager = mContext.getPackageManager();
+// PackageInfo pi = new PackageInfo();
+// pi.packageName = mContext.getPackageName();
+// pi.versionCode = 1;
+//
+// shadowOf(packageManager).installPackage(pi);
+// assertThrows(SecurityException.class, () -> template.checkPermissions(context));
+// }
+
+ @Test
+ public void checkPermissions_doesNotHavePermissions() {
+ PlaceListMapTemplate template =
+ PlaceListMapTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setCurrentLocationEnabled(true)
+ .build();
+
+ assertThrows(SecurityException.class, () -> template.checkPermissions(mContext));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
new file mode 100644
index 0000000..3e7cc36
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceMarker}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceMarkerTest {
+
+ @Test
+ public void create_throws_invalidLabelLength() {
+ assertThrows(IllegalArgumentException.class,
+ () -> PlaceMarker.builder().setLabel("Blah").build());
+ }
+
+ @Test
+ public void setColor_withImageTypeIcon_throws() {
+ CarIcon icon =
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ PlaceMarker.builder()
+ .setIcon(icon, PlaceMarker.TYPE_IMAGE)
+ .setColor(CarColor.SECONDARY)
+ .build());
+ }
+
+ @Test
+ public void create_throws_invalidCarIcon() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PlaceMarker.builder().setIcon(carIcon, PlaceMarker.TYPE_IMAGE));
+ }
+
+ @Test
+ public void createInstance() {
+ CarIcon icon =
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ PlaceMarker marker1 =
+ PlaceMarker.builder()
+ .setIcon(icon, PlaceMarker.TYPE_ICON)
+ .setLabel("foo")
+ .setColor(CarColor.SECONDARY)
+ .build();
+ assertThat(marker1.getIcon()).isEqualTo(icon);
+ assertThat(marker1.getIconType()).isEqualTo(PlaceMarker.TYPE_ICON);
+ assertThat(marker1.getColor()).isEqualTo(CarColor.SECONDARY);
+ assertThat(marker1.getLabel().getText()).isEqualTo("foo");
+ }
+
+ @Test
+ public void isDefaultMarker() {
+ assertThat(PlaceMarker.isDefaultMarker(null)).isFalse();
+ assertThat(PlaceMarker.isDefaultMarker(PlaceMarker.builder().setLabel("foo").build()))
+ .isFalse();
+
+ assertThat(PlaceMarker.isDefaultMarker(PlaceMarker.getDefault())).isTrue();
+ assertThat(PlaceMarker.isDefaultMarker(PlaceMarker.builder().build())).isTrue();
+ }
+
+ @Test
+ public void equals() {
+ CarIcon carIcon =
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ PlaceMarker marker =
+ PlaceMarker.builder()
+ .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+ .setLabel("foo")
+ .setColor(CarColor.SECONDARY)
+ .build();
+
+ assertThat(
+ PlaceMarker.builder()
+ .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+ .setLabel("foo")
+ .setColor(CarColor.SECONDARY)
+ .build())
+ .isEqualTo(marker);
+ }
+
+ @Test
+ public void notEquals_differentIcon() {
+ PlaceMarker marker = PlaceMarker.builder().setIcon(BACK, PlaceMarker.TYPE_IMAGE).build();
+
+ assertThat(PlaceMarker.builder().setIcon(ALERT, PlaceMarker.TYPE_IMAGE).build())
+ .isNotEqualTo(marker);
+ }
+
+ @Test
+ public void notEquals_differentLabel() {
+ PlaceMarker marker = PlaceMarker.builder().setLabel("foo").build();
+
+ assertThat(PlaceMarker.builder().setLabel("bar").build()).isNotEqualTo(marker);
+ }
+
+ @Test
+ public void notEquals_differentBackgroundColor() {
+ PlaceMarker marker = PlaceMarker.builder().setColor(CarColor.SECONDARY).build();
+
+ assertThat(PlaceMarker.builder().setColor(CarColor.BLUE).build()).isNotEqualTo(marker);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
new file mode 100644
index 0000000..664dd26
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for the {@link Place} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceTest {
+ /** Tests basic setter and getter operations. */
+ @Test
+ public void setAndGet() {
+ Place place =
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("A").build())
+ .build();
+ assertThat(place.getLatLng()).isEqualTo(LatLng.create(123, 456));
+ assertThat(place.getMarker()).isEqualTo(PlaceMarker.builder().setLabel("A").build());
+ }
+
+ @Test
+ public void equals() {
+ Place place =
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("A").build())
+ .build();
+
+ assertThat(place)
+ .isEqualTo(
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("A").build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentLatLng() {
+ Place place = Place.builder(LatLng.create(123, 456)).build();
+
+ assertThat(place).isNotEqualTo(Place.builder(LatLng.create(1, 2)).build());
+ }
+
+ @Test
+ public void notEquals_differentMarker() {
+ Place place =
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("A").build())
+ .build();
+
+ assertThat(place)
+ .isNotEqualTo(
+ Place.builder(LatLng.create(123, 456))
+ .setMarker(PlaceMarker.builder().setLabel("B").build())
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
new file mode 100644
index 0000000..b818f03
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
@@ -0,0 +1,275 @@
+/*
+ * 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.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Row}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RowTest {
+ @Test
+ public void create_defaultValues() {
+ Row row = Row.builder().setTitle("Title").build();
+ assertThat(row.getTitle().getText()).isEqualTo("Title");
+ assertThat(row.getTexts()).isEmpty();
+ assertThat(row.getImage()).isNull();
+ assertThat(row.getOnClickListener()).isNull();
+ assertThat(row.isBrowsable()).isFalse();
+ assertThat(row.getMetadata()).isEqualTo(Metadata.EMPTY_METADATA);
+ assertThat(row.getRowImageType()).isEqualTo(Row.IMAGE_TYPE_SMALL);
+ }
+
+ @Test
+ public void title_charSequence() {
+ String title = "foo";
+ Row row = Row.builder().setTitle(title).build();
+ assertThat(CarText.create(title)).isEqualTo(row.getTitle());
+ }
+
+ @Test
+ public void title_carText() {
+ CarText title = CarText.create("foo");
+ Row row = Row.builder().setTitle(title).build();
+ assertThat(title).isEqualTo(row.getTitle());
+ }
+
+ @Test
+ public void text_charSequence() {
+ CarText text1 = CarText.create("foo");
+ CarText text2 = CarText.create("bar");
+ Row row = Row.builder().setTitle("Title").addText(text1).addText(text2).build();
+ assertThat(row.getTexts()).containsExactly(text1, text2);
+ }
+
+ @Test
+ public void text_carText() {
+ String text1 = "foo";
+ String text2 = "bar";
+ Row row = Row.builder().setTitle("Title").addText(text1).addText(text2).build();
+ assertThat(row.getTexts()).containsExactly(CarText.create(text1), CarText.create(text2));
+ }
+
+ @Test
+ public void setImage() {
+ CarIcon image1 = BACK;
+ Row row = Row.builder().setTitle("Title").setImage(image1).build();
+ assertThat(image1).isEqualTo(row.getImage());
+ }
+
+ @Test
+ public void setToggle() {
+ Toggle toggle1 = Toggle.builder(isChecked -> {
+ }).build();
+ Row row = Row.builder().setTitle("Title").setToggle(toggle1).build();
+ assertThat(toggle1).isEqualTo(row.getToggle());
+ }
+
+ @Test
+ public void setSectionHeader() {
+ Row row =
+ Row.builder().setFlags(Row.ROW_FLAG_SECTION_HEADER).setTitle(
+ "section header").build();
+ assertThat(row.getFlags() & Row.ROW_FLAG_SECTION_HEADER).isNotEqualTo(0);
+ }
+
+ @Test
+ public void setOnClickListenerAndToggle_throws() {
+ Toggle toggle1 = Toggle.builder(isChecked -> {
+ }).build();
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ Row.builder()
+ .setTitle("Title")
+ .setOnClickListener(() -> {
+ })
+ .setToggle(toggle1)
+ .build());
+ }
+
+// TODO(shiufai): revisit the following as the test is not running on the main looper thread, and
+// thus the verify is failing.
+// @Test
+// public void clickListener() throws RemoteException {
+// OnClickListener >
+// Row row = Row.builder().setTitle("Title").setOnClickListener(onClickListener).build();
+// row.getOnClickListener().getListener().onClick(mock(IOnDoneCallback.class));
+// verify(onClickListener).onClick();
+// }
+
+ @Test
+ public void setMetadata() {
+ Metadata metadata = Metadata.ofPlace(Place.builder(LatLng.create(1, 1)).build());
+
+ Row row = Row.builder().setTitle("Title").setMetadata(metadata).build();
+ assertThat(row.getMetadata()).isEqualTo(metadata);
+ }
+
+ @Test
+ public void setIsBrowsable_noListener_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> Row.builder().setTitle("Title").setBrowsable(true).build());
+
+ // Positive case.
+ Row.builder().setTitle("Title").setBrowsable(false).build();
+ }
+
+ @Test
+ public void setIsBrowsable_notExclusivelyTextOrImage_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ Row.builder()
+ .setTitle("Title")
+ .setBrowsable(true)
+ .setToggle(Toggle.builder(state -> {
+ }).build())
+ .build());
+
+ // Positive case.
+ Row.builder()
+ .setBrowsable(true)
+ .setOnClickListener(() -> {
+ })
+ .setTitle("Title")
+ .addText("Text")
+ .setImage(
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .build();
+ }
+
+ @Test
+ public void equals() {
+ String title = "title";
+
+ Row row =
+ Row.builder()
+ .setTitle(title)
+ .setImage(BACK)
+ .setOnClickListener(() -> {
+ })
+ .setBrowsable(false)
+ .setFlags(1)
+ .setMetadata(Metadata.EMPTY_METADATA)
+ .addText(title)
+ .build();
+
+ assertThat(
+ Row.builder()
+ .setTitle(title)
+ .setImage(BACK)
+ .setOnClickListener(() -> {
+ })
+ .setBrowsable(false)
+ .setFlags(1)
+ .setMetadata(Metadata.EMPTY_METADATA)
+ .addText(title)
+ .build())
+ .isEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ String title = "title";
+
+ Row row = Row.builder().setTitle(title).build();
+
+ assertThat(Row.builder().setTitle("foo").build()).isNotEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_differentImage() {
+ Row row = Row.builder().setTitle("Title").setImage(BACK).build();
+
+ assertThat(Row.builder().setTitle("Title").setImage(ALERT).build()).isNotEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_oneHasNoCallback() {
+ Row row = Row.builder().setTitle("Title").setOnClickListener(() -> {
+ }).build();
+
+ assertThat(Row.builder().setTitle("Title").build()).isNotEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_differentBrowsable() {
+ Row row =
+ Row.builder().setTitle("Title").setBrowsable(false).setOnClickListener(() -> {
+ }).build();
+
+ assertThat(
+ Row.builder()
+ .setTitle("Title")
+ .setBrowsable(true)
+ .setOnClickListener(() -> {
+ })
+ .build())
+ .isNotEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_differentFlags() {
+ Row row = Row.builder().setTitle("Title").setFlags(1).build();
+
+ assertThat(Row.builder().setTitle("Title").setFlags(2).build()).isNotEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_differentMetadata() {
+ Row row = Row.builder().setTitle("Title").setMetadata(Metadata.EMPTY_METADATA).build();
+
+ assertThat(
+ Row.builder()
+ .setTitle("Title")
+ .setMetadata(
+ Metadata.builder()
+ .setPlace(
+ Place.builder(LatLng.create(/* latitude= */
+ 1f, /* longitude= */ 1f))
+ .build())
+ .build())
+ .build())
+ .isNotEqualTo(row);
+ }
+
+ @Test
+ public void notEquals_differenText() {
+ Row row = Row.builder().setTitle("Title").addText("foo").build();
+
+ assertThat(Row.builder().setTitle("Title").addText("bar").build()).isNotEqualTo(row);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
new file mode 100644
index 0000000..cdc70fb
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.RemoteException;
+
+import androidx.car.app.SearchListener;
+import androidx.car.app.TestUtils;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link SearchTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SearchTemplateTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ SearchListener mMockSearchListener;
+
+ @Test
+ public void createInstance_isLoading_hasList_Throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ SearchTemplate.builder(mMockSearchListener)
+ .setLoading(true)
+ .setItemList(ItemList.builder().build())
+ .build());
+ }
+
+ @Test
+ public void addList_selectable_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(TestUtils.createItemList(6, true))
+ .build());
+
+ // Positive cases.
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(TestUtils.createItemList(6, false))
+ .build();
+ }
+
+ @Test
+ public void addList_moreThanMaxTexts_throws() {
+ Row rowExceedsMaxTexts =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+ "text3").build();
+ Row rowMeetingMaxTexts =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(ItemList.builder().addItem(rowExceedsMaxTexts).build())
+ .build());
+
+ // Positive cases.
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+ .build();
+ }
+
+ @Test
+ public void addList_hasToggle_throws() {
+ Row rowWithToggle =
+ Row.builder().setTitle("Title").setToggle(Toggle.builder(isChecked -> {
+ }).build()).build();
+ Row rowMeetingRestrictions =
+ Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(ItemList.builder().addItem(rowWithToggle).build())
+ .build());
+
+ // Positive cases.
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(ItemList.builder().addItem(rowMeetingRestrictions).build())
+ .build();
+ }
+
+ @Test
+ public void buildEmpty_nullValues() {
+ SearchTemplate searchTemplate = SearchTemplate.builder(mMockSearchListener).build();
+
+ assertThat(searchTemplate.getInitialSearchText()).isNull();
+ assertThat(searchTemplate.getSearchHint()).isNull();
+ assertThat(searchTemplate.getActionStrip()).isNull();
+ assertThat(searchTemplate.getHeaderAction()).isNull();
+ }
+
+ @Test
+ public void buildWithValues() throws RemoteException {
+ String initialSearchText = "searchTemplate for this!!";
+ String searchHint = "This is not a hint";
+ ItemList itemList = ItemList.builder().addItem(
+ Row.builder().setTitle("foo").build()).build();
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+ SearchTemplate searchTemplate =
+ SearchTemplate.builder(mMockSearchListener)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(actionStrip)
+ .setInitialSearchText(initialSearchText)
+ .setSearchHint(searchHint)
+ .setItemList(itemList)
+ .build();
+
+ assertThat(searchTemplate.getInitialSearchText()).isEqualTo(initialSearchText);
+ assertThat(searchTemplate.getSearchHint()).isEqualTo(searchHint);
+ assertThat(searchTemplate.getItemList()).isEqualTo(itemList);
+ assertThat(searchTemplate.getActionStrip()).isEqualTo(actionStrip);
+ assertThat(searchTemplate.getHeaderAction()).isEqualTo(Action.BACK);
+
+ // TODO(shiufai): revisit the following as the test is not running on the main looper
+ // thread, and thus the verify is failing.
+// String searchText = "foo";
+// searchTemplate.getSearchListener().onSearchSubmitted(searchText,
+// mock(IOnDoneCallback.class));
+// verify(mockSearchListener).onSearchSubmitted(searchText);
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ SearchTemplate.builder(mMockSearchListener)
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void equals() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .setInitialSearchText("foo")
+ .setSearchHint("hint")
+ .setShowKeyboardByDefault(false)
+ .setLoading(false)
+ .setItemList(ItemList.builder().build())
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ SearchTemplate.builder(mMockSearchListener)
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.BACK).build())
+ .setInitialSearchText("foo")
+ .setSearchHint("hint")
+ .setShowKeyboardByDefault(false)
+ .setLoading(false)
+ .setItemList(ItemList.builder().build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener).setHeaderAction(Action.BACK).build();
+ assertThat(template)
+ .isNotEqualTo(
+ SearchTemplate.builder(mMockSearchListener).setHeaderAction(
+ Action.APP_ICON).build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener)
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .build();
+ assertThat(template)
+ .isNotEqualTo(
+ SearchTemplate.builder(mMockSearchListener)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentInitialSearchText() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener).setInitialSearchText("foo").build();
+ assertThat(template)
+ .isNotEqualTo(
+ SearchTemplate.builder(mMockSearchListener).setInitialSearchText(
+ "bar").build());
+ }
+
+ @Test
+ public void notEquals_differentSearchHint() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener).setSearchHint("foo").build();
+ assertThat(template)
+ .isNotEqualTo(SearchTemplate.builder(mMockSearchListener).setSearchHint(
+ "bar").build());
+ }
+
+ @Test
+ public void notEquals_differentKeyboardEnabled() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener).setShowKeyboardByDefault(true).build();
+ assertThat(template)
+ .isNotEqualTo(
+ SearchTemplate.builder(mMockSearchListener).setShowKeyboardByDefault(
+ false).build());
+ }
+
+ @Test
+ public void notEquals_differentItemList() {
+ SearchTemplate template =
+ SearchTemplate.builder(mMockSearchListener).setItemList(
+ ItemList.builder().build()).build();
+ assertThat(template)
+ .isNotEqualTo(
+ SearchTemplate.builder(mMockSearchListener)
+ .setItemList(
+ ItemList.builder().addItem(
+ Row.builder().setTitle("Title").build()).build())
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
new file mode 100644
index 0000000..69f5124
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ItemListTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SectionedItemListTest {
+
+ @Test
+ public void createInstance() {
+ ItemList list = ItemList.builder().build();
+ CarText header = CarText.create("header");
+ SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+ assertThat(sectionList.getItemList()).isEqualTo(list);
+ assertThat(sectionList.getHeader()).isEqualTo(header);
+ }
+
+ @Test
+ public void equals() {
+ ItemList list = ItemList.builder().build();
+ CarText header = CarText.create("header");
+ SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+ ItemList list2 = ItemList.builder().build();
+ CarText header2 = CarText.create("header");
+ SectionedItemList sectionList2 = SectionedItemList.create(list2, header2);
+
+ assertThat(sectionList2).isEqualTo(sectionList);
+ }
+
+ @Test
+ public void notEquals_differentItemList() {
+ ItemList list = ItemList.builder().addItem(Row.builder().setTitle("Title").build()).build();
+ CarText header = CarText.create("header");
+ SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+ ItemList list2 = ItemList.builder().build();
+ CarText header2 = CarText.create("header");
+ SectionedItemList sectionList2 = SectionedItemList.create(list2, header2);
+
+ assertThat(sectionList2).isNotEqualTo(sectionList);
+ }
+
+ @Test
+ public void notEquals_differentHeader() {
+ ItemList list = ItemList.builder().build();
+ CarText header = CarText.create("header1");
+ SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+ ItemList list2 = ItemList.builder().build();
+ CarText header2 = CarText.create("header2");
+ SectionedItemList sectionList2 = SectionedItemList.create(list2, header2);
+
+ assertThat(sectionList2).isNotEqualTo(sectionList);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
new file mode 100644
index 0000000..3cbb446
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link TemplateWrapper}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TemplateWrapperTest {
+ @Test
+ public void createInstance() {
+ ListTemplate template =
+ ListTemplate.builder().setTitle("Title").setSingleList(
+ ItemList.builder().build()).build();
+ TemplateWrapper wrapper = TemplateWrapper.wrap(template);
+ assertThat(wrapper.getTemplate()).isEqualTo(template);
+
+ wrapper = TemplateWrapper.wrap(template, "1");
+ assertThat(wrapper.getTemplate()).isEqualTo(template);
+ assertThat(wrapper.getId()).isEqualTo("1");
+ }
+
+ @Test
+ public void createInstance_thenUpdate() {
+ ListTemplate template =
+ ListTemplate.builder().setTitle("Title").setSingleList(
+ ItemList.builder().build()).build();
+ ListTemplate template2 =
+ ListTemplate.builder().setTitle("Title").setSingleList(
+ ItemList.builder().build()).build();
+
+ TemplateWrapper wrapper = TemplateWrapper.wrap(template);
+ String id = wrapper.getId();
+ assertThat(wrapper.getTemplate()).isEqualTo(template);
+ assertThat(wrapper.getCurrentTaskStep()).isEqualTo(0);
+
+ wrapper.setTemplate(template2);
+ assertThat(wrapper.getTemplate()).isEqualTo(template2);
+ assertThat(wrapper.getCurrentTaskStep()).isEqualTo(0);
+ assertThat(wrapper.getId()).isEqualTo(id);
+
+ wrapper.setCurrentTaskStep(2);
+ assertThat(wrapper.getTemplate()).isEqualTo(template2);
+ assertThat(wrapper.getCurrentTaskStep()).isEqualTo(2);
+ assertThat(wrapper.getId()).isEqualTo(id);
+
+ wrapper.setRefresh(true);
+ assertThat(wrapper.isRefresh()).isTrue();
+ assertThat(wrapper.getTemplate()).isEqualTo(template2);
+ assertThat(wrapper.getCurrentTaskStep()).isEqualTo(2);
+ assertThat(wrapper.getId()).isEqualTo(id);
+
+ wrapper.setId("1");
+ assertThat(wrapper.getId()).isEqualTo("1");
+ }
+
+ @Test
+ public void copyOf() {
+ ListTemplate template =
+ ListTemplate.builder().setTitle("Title").setSingleList(
+ ItemList.builder().build()).build();
+ TemplateWrapper source = TemplateWrapper.wrap(template, "ID");
+ source.setCurrentTaskStep(45);
+ source.setRefresh(true);
+
+ TemplateWrapper dest = TemplateWrapper.copyOf(source);
+ assertThat(dest.getTemplate()).isEqualTo(template);
+ assertThat(dest.getCurrentTaskStep()).isEqualTo(45);
+ assertThat(dest.getId()).isEqualTo("ID");
+ assertThat(dest.isRefresh()).isTrue();
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
new file mode 100644
index 0000000..ab4d025
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.car.app.model.Toggle.OnCheckedChangeListener;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link Toggle}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ToggleTest {
+ @Rule
+ public MockitoRule mocks = MockitoJUnit.rule();
+
+ @Mock
+ OnCheckedChangeListener mMockOnCheckedChangeListener;
+
+ @Test
+ public void build_withValues_notCheckedByDefault() {
+ Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).build();
+ assertThat(toggle.isChecked()).isFalse();
+ }
+
+// TODO(shiufai): revisit the following as the test is not running on the main looper
+// thread, and thus the verify is failing.
+// @Test
+// public void build_checkedChange_sendsCheckedChangeCall() throws RemoteException {
+// Toggle toggle = Toggle.builder(mockOnCheckedChangeListener).setChecked(true).build();
+//
+// toggle.getOnCheckedChangeListener().onCheckedChange(false, mock(IOnDoneCallback.class));
+// verify(mockOnCheckedChangeListener).onCheckedChange(false);
+// }
+
+ @Test
+ public void equals() {
+ Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build();
+ assertThat(toggle)
+ .isEqualTo(Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build());
+ }
+
+ @Test
+ public void notEquals() {
+ Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build();
+ assertThat(toggle)
+ .isNotEqualTo(Toggle.builder(mMockOnCheckedChangeListener).setChecked(
+ false).build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
new file mode 100644
index 0000000..ab577b5
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.constraints;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+/** Tests for {@link ActionsConstraints}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionsConstraintsTest {
+ @Test
+ public void createEmpty() {
+ ActionsConstraints constraints = ActionsConstraints.builder().build();
+
+ assertThat(constraints.getMaxActions()).isEqualTo(Integer.MAX_VALUE);
+ assertThat(constraints.getRequiredActionTypes()).isEmpty();
+ }
+
+ @Test
+ public void create_requiredExceedsMaxAllowedActions() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ActionsConstraints.builder()
+ .setMaxActions(1)
+ .addRequiredActionType(Action.TYPE_BACK)
+ .addRequiredActionType(Action.TYPE_CUSTOM)
+ .build());
+ }
+
+ @Test
+ public void create_requiredAlsoDisallowed() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ActionsConstraints.builder()
+ .addRequiredActionType(Action.TYPE_BACK)
+ .addDisallowedActionType(Action.TYPE_BACK)
+ .build());
+ }
+
+ @Test
+ public void createConstraints() {
+ ActionsConstraints constraints =
+ ActionsConstraints.builder()
+ .setMaxActions(2)
+ .addRequiredActionType(Action.TYPE_CUSTOM)
+ .addDisallowedActionType(Action.TYPE_BACK)
+ .build();
+
+ assertThat(constraints.getMaxActions()).isEqualTo(2);
+ assertThat(constraints.getRequiredActionTypes()).containsExactly(Action.TYPE_CUSTOM);
+ assertThat(constraints.getDisallowedActionTypes()).containsExactly(Action.TYPE_BACK);
+ }
+
+ @Test
+ public void validateActions() {
+ ActionsConstraints constraints =
+ ActionsConstraints.builder()
+ .setMaxActions(2)
+ .setMaxCustomTitles(1)
+ .addRequiredActionType(Action.TYPE_CUSTOM)
+ .addDisallowedActionType(Action.TYPE_BACK)
+ .build();
+
+ CarIcon carIcon =
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ Action actionWithIcon = TestUtils.createAction(null, carIcon);
+ Action actionWithTitle = TestUtils.createAction("Title", carIcon);
+
+ // Positive case: instance that fits the 2-max-actions, only-1-has-title constraint.
+ constraints.validateOrThrow(
+ ActionStrip.builder()
+ .addAction(actionWithIcon)
+ .addAction(actionWithTitle)
+ .build()
+ .getActions());
+ // Positive case: empty list is okay when there are no required types
+ ActionsConstraints.builder().setMaxActions(2).build().validateOrThrow(
+ Collections.emptyList());
+
+ // Missing required type.
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ ActionStrip.builder().addAction(
+ Action.APP_ICON).build().getActions()));
+
+ // Disallowed type
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ ActionStrip.builder().addAction(Action.BACK).build().getActions()));
+
+ // Over max allowed actions
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ ActionStrip.builder()
+ .addAction(Action.APP_ICON)
+ .addAction(actionWithIcon)
+ .addAction(actionWithTitle)
+ .build()
+ .getActions()));
+
+ // Over max allowed actions with title
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ ActionStrip.builder()
+ .addAction(actionWithTitle)
+ .addAction(actionWithTitle)
+ .build()
+ .getActions()));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
new file mode 100644
index 0000000..9b4d96e
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.constraints;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RowConstraints}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RowConstraintsTest {
+ @Test
+ public void validate_clickListener() {
+ RowConstraints constraints = RowConstraints.builder().setOnClickListenerAllowed(
+ false).build();
+ RowConstraints allowConstraints =
+ RowConstraints.builder().setOnClickListenerAllowed(true).build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ Row.builder().setTitle("Title)").setOnClickListener(() -> {
+ }).build()));
+
+ // Positive cases
+ constraints.validateOrThrow(Row.builder().setTitle("Title").build());
+ allowConstraints.validateOrThrow(
+ Row.builder().setTitle("Title").setOnClickListener(() -> {
+ }).build());
+ }
+
+ @Test
+ public void validate_toggle() {
+ RowConstraints constraints = RowConstraints.builder().setToggleAllowed(false).build();
+ RowConstraints allowConstraints = RowConstraints.builder().setToggleAllowed(true).build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ Row.builder()
+ .setTitle("Title)")
+ .setToggle(Toggle.builder(isChecked -> {
+ }).build())
+ .build()));
+
+ // Positive cases
+ constraints.validateOrThrow(Row.builder().setTitle("Title").build());
+ allowConstraints.validateOrThrow(
+ Row.builder().setTitle("Title").setToggle(Toggle.builder(isChecked -> {
+ }).build()).build());
+ }
+
+ @Test
+ public void validate_images() {
+ RowConstraints constraints = RowConstraints.builder().setImageAllowed(false).build();
+ RowConstraints allowConstraints = RowConstraints.builder().setImageAllowed(true).build();
+ CarIcon carIcon =
+ CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ Row.builder().setTitle("Title)").setImage(carIcon).build()));
+
+ // Positive cases
+ constraints.validateOrThrow(Row.builder().setTitle("Title").build());
+ allowConstraints.validateOrThrow(Row.builder().setTitle("Title").setImage(carIcon).build());
+ }
+
+ @Test
+ public void validate_texts() {
+ RowConstraints constraints = RowConstraints.builder().setMaxTextLinesPerRow(2).build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ constraints.validateOrThrow(
+ Row.builder()
+ .setTitle("Title)")
+ .addText("text1")
+ .addText("text2")
+ .addText("text3")
+ .build()));
+
+ // Positive cases
+ constraints.validateOrThrow(
+ Row.builder().setTitle("Title").addText("text1").addText("text2").build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
new file mode 100644
index 0000000..56b5273
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.constraints;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RowListConstraints}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RowListConstraintsTest {
+ @Test
+ public void validate_itemList_noSelectable() {
+ RowListConstraints disallowConstraints =
+ RowListConstraints.builder()
+ .setRowListType(RowListConstraints.DEFAULT_LIST)
+ .setAllowSelectableLists(false)
+ .build();
+ RowListConstraints allowConstraints =
+ RowListConstraints.builder()
+ .setRowListType(RowListConstraints.DEFAULT_LIST)
+ .setAllowSelectableLists(true)
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> disallowConstraints.validateOrThrow(TestUtils.createItemList(5, true)));
+
+ // Positive case
+ disallowConstraints.validateOrThrow(TestUtils.createItemList(5, false));
+ allowConstraints.validateOrThrow(TestUtils.createItemList(5, true));
+ }
+
+ @Test
+ public void validate_sectionItemList_noSelectable() {
+ RowListConstraints disallowConstraints =
+ RowListConstraints.builder()
+ .setRowListType(RowListConstraints.DEFAULT_LIST)
+ .setAllowSelectableLists(false)
+ .build();
+ RowListConstraints allowConstraints =
+ RowListConstraints.builder()
+ .setRowListType(RowListConstraints.DEFAULT_LIST)
+ .setAllowSelectableLists(true)
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> disallowConstraints.validateOrThrow(TestUtils.createSections(2, 2, true)));
+
+ // Positive case
+ disallowConstraints.validateOrThrow(TestUtils.createSections(2, 2, false));
+ allowConstraints.validateOrThrow(TestUtils.createSections(2, 2, true));
+ }
+
+ @Test
+ public void validate_pane_maxActions() {
+ RowListConstraints constraints =
+ RowListConstraints.builder()
+ .setRowListType(RowListConstraints.DEFAULT_LIST)
+ .setMaxActions(2)
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> constraints.validateOrThrow(TestUtils.createPane(5, 3)));
+
+ // Positive case
+ constraints.validateOrThrow(TestUtils.createPane(5, 2));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
new file mode 100644
index 0000000..1d48c37
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.navigation;
+
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+
+import android.os.RemoteException;
+
+import androidx.car.app.HostDispatcher;
+import androidx.car.app.ICarHost;
+import androidx.car.app.model.Distance;
+import androidx.car.app.navigation.model.Destination;
+import androidx.car.app.navigation.model.Step;
+import androidx.car.app.navigation.model.TravelEstimate;
+import androidx.car.app.navigation.model.Trip;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.time.Duration;
+
+/** Tests for {@link NavigationManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavigationManagerTest {
+ @Mock
+ private ICarHost mMockCarHost;
+ @Mock
+ private INavigationHost.Stub mMockNavHost;
+ @Mock
+ private NavigationManagerListener mNavigationListener;
+
+ private final HostDispatcher mHostDispatcher = new HostDispatcher();
+ private NavigationManager mNavigationManager;
+
+ private final Destination mDestination =
+ Destination.builder().setName("Home").setAddress("123 State Street").build();
+ private final Step mStep = Step.builder("Straight Ahead").build();
+ private final TravelEstimate mStepTravelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS),
+ Duration.ofHours(1).getSeconds(),
+ createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"));
+ private final TravelEstimate mDestinationTravelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
+ Duration.ofHours(1).getSeconds(),
+ createDateTimeWithZone("2020-04-14T16:57:00", "US/Pacific"));
+ private static final String CURRENT_ROAD = "State St.";
+ private final Trip mTrip =
+ Trip.builder()
+ .addDestination(mDestination)
+ .addStep(mStep)
+ .addDestinationTravelEstimate(mDestinationTravelEstimate)
+ .addStepTravelEstimate(mStepTravelEstimate)
+ .setCurrentRoad(CURRENT_ROAD)
+ .build();
+
+ // TODO(rampara): Confirm that UiThreadTest annotation is required for test.
+// @Before
+// public void setUp() throws RemoteException {
+// MockitoAnnotations.initMocks(this);
+//
+// INavigationHost navHostStub =
+// new INavigationHost.Stub() {
+// @Override
+// public void updateTrip(Bundleable trip) throws RemoteException {
+// mMockNavHost.updateTrip(trip);
+// }
+//
+// @Override
+// public void navigationStarted() throws RemoteException {
+// mMockNavHost.navigationStarted();
+// }
+//
+// @Override
+// public void navigationEnded() throws RemoteException {
+// mMockNavHost.navigationEnded();
+// }
+// };
+// when(mMockCarHost.getHost(any())).thenReturn(navHostStub.asBinder());
+//
+// mHostDispatcher.setCarHost(mMockCarHost);
+//
+// mNavigationManager = NavigationManager.create(mHostDispatcher);
+// }
+
+// @Test
+// public void navigationStarted_sendState_navigationEnded() throws RemoteException {
+// InOrder inOrder = inOrder(mMockNavHost);
+//
+// mNavigationManager.setListener(mNavigationListener);
+// mNavigationManager.navigationStarted();
+// inOrder.verify(mMockNavHost).navigationStarted();
+//
+// mNavigationManager.updateTrip(mTrip);
+// inOrder.verify(mMockNavHost).updateTrip(any(Bundleable.class));
+//
+// mNavigationManager.navigationEnded();
+// inOrder.verify(mMockNavHost).navigationEnded();
+// }
+
+ @Test
+ public void navigationStarted_noListenerSet() throws RemoteException {
+// assertThrows(IllegalStateException.class, () -> mNavigationManager.navigationStarted());
+ }
+
+ // TODO(rampara): Confirm that UiThreadTest annotation is required for test.
+// @Test
+// public void navigationStarted_multiple() throws RemoteException {
+//
+// mNavigationManager.setListener(mNavigationListener);
+// mNavigationManager.navigationStarted();
+//
+// mNavigationManager.navigationStarted();
+// verify(mMockNavHost).navigationStarted();
+// }
+//
+// @Test
+// public void navgiationEnded_multiple_not_started() throws RemoteException {
+// mNavigationManager.navigationEnded();
+// mNavigationManager.navigationEnded();
+// mNavigationManager.navigationEnded();
+// verify(mMockNavHost, never()).navigationEnded();
+// }
+//
+// @Test
+// public void sendNavigationState_notStarted() throws RemoteException {
+// assertThrows(IllegalStateException.class, () -> mNavigationManager.updateTrip(mTrip));
+// }
+//
+// @Test
+// public void stopNavigation_notNavigating() throws RemoteException {
+// mNavigationManager.setListener(mNavigationListener);
+// mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
+// verify(mNavigationListener, never()).stopNavigation();
+// }
+//
+// @Test
+// public void stopNavigation_navigating_restart() throws RemoteException {
+// InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
+//
+// mNavigationManager.setListener(mNavigationListener);
+// mNavigationManager.navigationStarted();
+// inOrder.verify(mMockNavHost).navigationStarted();
+//
+// mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
+// inOrder.verify(mNavigationListener).stopNavigation();
+//
+// mNavigationManager.navigationStarted();
+// inOrder.verify(mMockNavHost).navigationStarted();
+// }
+//
+// @Test
+// public void onAutoDriveEnabled_callsListener() {
+// mNavigationManager.setListener(mNavigationListener);
+// mNavigationManager.onAutoDriveEnabled();
+//
+// verify(mNavigationListener).onAutoDriveEnabled();
+// }
+//
+// @Test
+// public void onAutoDriveEnabledBeforeRegisteringListener_callsListener() {
+// mNavigationManager.onAutoDriveEnabled();
+// mNavigationManager.setListener(mNavigationListener);
+//
+// verify(mNavigationListener).onAutoDriveEnabled();
+// }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
new file mode 100644
index 0000000..ba52c24
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Destination}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DestinationTest {
+
+ @Test
+ public void createInstance() {
+ String title = "Google BVE";
+ String address = "1120 112th Ave NE";
+
+ Destination destination = Destination.builder().setName(title).setAddress(address).build();
+
+ assertThat(destination.getName().getText()).isEqualTo(title);
+ assertThat(destination.getAddress().getText()).isEqualTo(address);
+ assertThat(destination.getImage()).isNull();
+ }
+
+ @Test
+ public void emptyNameAndAddress_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> Destination.builder().setName("").setAddress("").build());
+ }
+
+ @Test
+ public void emptyNameOrAddress_allowed() {
+ Destination destination = Destination.builder().setName("name").setAddress("").build();
+ assertThat(destination.getName().getText()).isEqualTo("name");
+ assertThat(destination.getAddress().getText()).isEmpty();
+
+ destination = Destination.builder().setName(null).setAddress("address").build();
+ assertThat(destination.getAddress().getText()).isEqualTo("address");
+ assertThat(destination.getName()).isNull();
+ }
+
+ @Test
+ public void invalidCarIcon_throws() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Destination.builder().setName("hello").setAddress("world").setImage(carIcon));
+ }
+
+ @Test
+ public void validate_hashcodeAndEquals() {
+ Destination destination1 = Destination.builder().setName("name").setAddress(
+ "address").build();
+ Destination destination2 = Destination.builder().setName("name").setAddress(
+ "address1").build();
+ Destination destination3 = Destination.builder().setName("name2").setAddress(
+ "address").build();
+ Destination destination4 = Destination.builder().setName("name").setAddress(
+ "address").build();
+
+ assertThat(destination1.hashCode()).isNotEqualTo(destination2.hashCode());
+ assertThat(destination1).isNotEqualTo(destination2);
+ assertThat(destination1.hashCode()).isNotEqualTo(destination3.hashCode());
+ assertThat(destination1).isNotEqualTo(destination3);
+ assertThat(destination1.hashCode()).isEqualTo(destination4.hashCode());
+ assertThat(destination1).isEqualTo(destination4);
+ }
+
+ @Test
+ public void equals() {
+ Destination destination = Destination.builder().setName("name").setAddress(
+ "address").build();
+
+ assertThat(destination)
+ .isEqualTo(Destination.builder().setName("name").setAddress("address").build());
+ }
+
+ @Test
+ public void notEquals_differentName() {
+ Destination destination = Destination.builder().setName("name").setAddress(
+ "address").build();
+
+ assertThat(destination)
+ .isNotEqualTo(Destination.builder().setName("Rafael").setAddress(
+ "address").build());
+ }
+
+ @Test
+ public void notEquals_differentAddress() {
+ Destination destination = Destination.builder().setName("name").setAddress(
+ "address").build();
+
+ assertThat(destination)
+ .isNotEqualTo(Destination.builder().setName("name").setAddress(
+ "123 main st.").build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
new file mode 100644
index 0000000..3f17b37
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_LEFT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link LaneDirection}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LaneDirectionTest {
+
+ @Test
+ public void createInstance() {
+ int shape = SHAPE_NORMAL_LEFT;
+ LaneDirection laneDirection = LaneDirection.create(shape, true);
+
+ assertThat(shape).isEqualTo(laneDirection.getShape());
+ assertThat(laneDirection.isHighlighted()).isTrue();
+ }
+
+ @Test
+ public void equals() {
+ LaneDirection laneDirection = LaneDirection.create(SHAPE_NORMAL_LEFT, true);
+ assertThat(LaneDirection.create(SHAPE_NORMAL_LEFT, true)).isEqualTo(laneDirection);
+ }
+
+ @Test
+ public void notEquals_differentShape() {
+ LaneDirection laneDirection = LaneDirection.create(SHAPE_NORMAL_LEFT, true);
+ assertThat(LaneDirection.create(LaneDirection.SHAPE_STRAIGHT, true))
+ .isNotEqualTo(laneDirection);
+ }
+
+ @Test
+ public void notEquals_differentHighlighted() {
+ LaneDirection laneDirection = LaneDirection.create(SHAPE_NORMAL_LEFT, true);
+ assertThat(LaneDirection.create(SHAPE_NORMAL_LEFT, false)).isNotEqualTo(laneDirection);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
new file mode 100644
index 0000000..8d4a1197
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_LEFT;
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_SHARP_LEFT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Lane}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LaneTest {
+
+ @Test
+ public void createInstance() {
+ LaneDirection laneDirection1 = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+ LaneDirection laneDirection2 = LaneDirection.create(SHAPE_NORMAL_LEFT, false);
+ Lane lane = Lane.builder().addDirection(laneDirection1).addDirection(
+ laneDirection2).build();
+
+ assertThat(lane.getDirections()).hasSize(2);
+ assertThat(laneDirection1).isEqualTo(lane.getDirections().get(0));
+ assertThat(laneDirection2).isEqualTo(lane.getDirections().get(1));
+ }
+
+ @Test
+ public void clearDirections() {
+ LaneDirection laneDirection1 = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+ LaneDirection laneDirection2 = LaneDirection.create(SHAPE_NORMAL_LEFT, false);
+ Lane lane =
+ Lane.builder()
+ .addDirection(laneDirection1)
+ .addDirection(laneDirection2)
+ .clearDirections()
+ .build();
+
+ assertThat(lane.getDirections()).hasSize(0);
+ }
+
+ @Test
+ public void equals() {
+ LaneDirection laneDirection = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+ Lane lane = Lane.builder().addDirection(laneDirection).build();
+
+ assertThat(Lane.builder().addDirection(laneDirection).build()).isEqualTo(lane);
+ }
+
+ @Test
+ public void notEquals_differentDirections() {
+ LaneDirection laneDirection = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+ Lane lane = Lane.builder().addDirection(laneDirection).build();
+
+ assertThat(Lane.builder().addDirection(laneDirection).addDirection(laneDirection).build())
+ .isNotEqualTo(lane);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
new file mode 100644
index 0000000..390b2ee
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Maneuver}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ManeuverTest {
+
+ @Test
+ public void createInstance_non_roundabout() {
+ int type = TYPE_STRAIGHT;
+
+ Maneuver maneuver = Maneuver.builder(type).setIcon(CarIcon.APP_ICON).build();
+ assertThat(type).isEqualTo(maneuver.getType());
+ assertThat(CarIcon.APP_ICON).isEqualTo(maneuver.getIcon());
+ }
+
+ @Test
+ public void createInstance_non_roundabout_invalid_type() {
+ int typeHigh = 1000;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(typeHigh).setIcon(CarIcon.APP_ICON).build());
+
+ int typeLow = -1;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(typeLow).setIcon(CarIcon.APP_ICON).build());
+ }
+
+ @Test
+ public void createInstance_non_roundabout_roundabout_type() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW).setIcon(
+ CarIcon.APP_ICON).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW).setIcon(
+ CarIcon.APP_ICON).build());
+ assertThrows(IllegalArgumentException.class,
+ () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void createInstance_roundabout_only_exit_number() {
+ int type = TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+ int roundaboutExitNumber = 2;
+
+ Maneuver maneuver =
+ Maneuver.builder(type)
+ .setRoundaboutExitNumber(roundaboutExitNumber)
+ .setIcon(CarIcon.APP_ICON)
+ .build();
+ assertThat(type).isEqualTo(maneuver.getType());
+ assertThat(roundaboutExitNumber).isEqualTo(maneuver.getRoundaboutExitNumber());
+ assertThat(CarIcon.APP_ICON).isEqualTo(maneuver.getIcon());
+ }
+
+ @Test
+ public void createInstance_roundabout_invalid_type() {
+ int typeHigh = 1000;
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Maneuver.builder(typeHigh)
+ .setRoundaboutExitNumber(1)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+
+ int typeLow = -1;
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Maneuver.builder(typeLow).setRoundaboutExitNumber(1).setIcon(
+ CarIcon.APP_ICON).build());
+ }
+
+ @Test
+ public void createInstance_roundabout_non_roundabout_type() {
+ int type = TYPE_STRAIGHT;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(type).setRoundaboutExitNumber(1).setIcon(
+ CarIcon.APP_ICON).build());
+ }
+
+ @Test
+ public void createInstance_roundabout_invalid_exit() {
+ int type = TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+ int roundaboutExitNumber = 0;
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Maneuver.builder(type)
+ .setRoundaboutExitNumber(roundaboutExitNumber)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void createInstance_roundabout_with_angle() {
+ int type = TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE;
+ int roundaboutExitNumber = 3;
+ int roundaboutExitAngle = 270;
+
+ Maneuver maneuver =
+ Maneuver.builder(type)
+ .setRoundaboutExitNumber(roundaboutExitNumber)
+ .setRoundaboutExitAngle(roundaboutExitAngle)
+ .setIcon(CarIcon.APP_ICON)
+ .build();
+ assertThat(type).isEqualTo(maneuver.getType());
+ assertThat(roundaboutExitNumber).isEqualTo(maneuver.getRoundaboutExitNumber());
+ assertThat(roundaboutExitAngle).isEqualTo(maneuver.getRoundaboutExitAngle());
+ assertThat(CarIcon.APP_ICON).isEqualTo(maneuver.getIcon());
+ }
+
+ @Test
+ public void createInstance_roundabout_with_angle_invalid_type() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Maneuver.builder(TYPE_STRAIGHT)
+ .setRoundaboutExitNumber(1)
+ .setRoundaboutExitAngle(1)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Maneuver.builder(TYPE_ROUNDABOUT_ENTER)
+ .setRoundaboutExitNumber(1)
+ .setRoundaboutExitAngle(1)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW)
+ .setRoundaboutExitNumber(1)
+ .setRoundaboutExitAngle(1)
+ .setIcon(CarIcon.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void createInstance_invalid_carIcon() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Maneuver.builder(TYPE_STRAIGHT).setIcon(carIcon).build());
+ }
+
+ @Test
+ public void equals() {
+ Maneuver maneuver = Maneuver.builder(TYPE_STRAIGHT).setIcon(CarIcon.APP_ICON).build();
+
+ assertThat(Maneuver.builder(TYPE_STRAIGHT).setIcon(CarIcon.APP_ICON).build())
+ .isEqualTo(maneuver);
+ }
+
+ @Test
+ public void notEquals_differentType() {
+ Maneuver maneuver = Maneuver.builder(TYPE_DESTINATION_LEFT).setIcon(
+ CarIcon.APP_ICON).build();
+
+ assertThat(Maneuver.builder(TYPE_STRAIGHT).setIcon(CarIcon.APP_ICON).build())
+ .isNotEqualTo(maneuver);
+ }
+
+ @Test
+ public void notEquals_differentImage() {
+ Maneuver maneuver = Maneuver.builder(TYPE_DESTINATION_LEFT).setIcon(
+ CarIcon.APP_ICON).build();
+
+ assertThat(Maneuver.builder(TYPE_DESTINATION_LEFT).setIcon(CarIcon.ALERT).build())
+ .isNotEqualTo(maneuver);
+ }
+
+ @Test
+ public void notEquals_differentRoundaboutExit() {
+ Maneuver maneuver =
+ Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW)
+ .setRoundaboutExitNumber(1)
+ .setIcon(CarIcon.APP_ICON)
+ .build();
+
+ assertThat(
+ Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW)
+ .setRoundaboutExitNumber(2)
+ .setIcon(CarIcon.APP_ICON)
+ .build())
+ .isNotEqualTo(maneuver);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
new file mode 100644
index 0000000..1be5851
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MessageInfoTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MessageInfoTest {
+
+ @Test
+ public void invalidCarIcon_throws() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> MessageInfo.builder("Message").setImage(carIcon));
+ }
+
+ /** Tests basic construction of a template with a minimal data. */
+ @Test
+ public void createMinimalInstance() {
+ MessageInfo messageInfo = MessageInfo.builder("Message").build();
+ assertThat(messageInfo.getTitle().getText()).isEqualTo("Message");
+ assertThat(messageInfo.getText()).isNull();
+ assertThat(messageInfo.getImage()).isNull();
+ }
+
+ /** Tests construction of a template with all data. */
+ @Test
+ public void createFullInstance() {
+ MessageInfo messageInfo =
+ MessageInfo.builder("Message").setImage(CarIcon.APP_ICON).setText(
+ "Secondary").build();
+ assertThat(messageInfo.getTitle().getText()).isEqualTo("Message");
+ assertThat(messageInfo.getText().getText()).isEqualTo("Secondary");
+ assertThat(messageInfo.getImage()).isEqualTo(CarIcon.APP_ICON);
+ }
+
+ @Test
+ public void no_message_throws() {
+ assertThrows(NullPointerException.class, () -> MessageInfo.builder(null));
+ }
+
+ @Test
+ public void equals() {
+ final String title = "Primary";
+ final String text = "Secondary";
+
+ MessageInfo messageInfo =
+ MessageInfo.builder(title).setText(text).setImage(CarIcon.APP_ICON).build();
+
+ assertThat(messageInfo)
+ .isEqualTo(MessageInfo.builder(title).setText(text).setImage(
+ CarIcon.APP_ICON).build());
+ }
+
+ @Test
+ public void notEquals() {
+ final String title = "Primary";
+ final String text = "Secondary";
+
+ MessageInfo messageInfo =
+ MessageInfo.builder(title).setText(text).setImage(CarIcon.APP_ICON).build();
+
+ assertThat(messageInfo)
+ .isNotEqualTo(
+ MessageInfo.builder("Not Primary").setText(text).setImage(
+ CarIcon.APP_ICON).build());
+
+ assertThat(messageInfo)
+ .isNotEqualTo(
+ MessageInfo.builder(title).setText("Not Secondary").setImage(
+ CarIcon.APP_ICON).build());
+
+ assertThat(messageInfo)
+ .isNotEqualTo(MessageInfo.builder(title).setText(text).setImage(
+ CarIcon.ERROR).build());
+ }
+
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
new file mode 100644
index 0000000..2528dfb
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
@@ -0,0 +1,449 @@
+/*
+ * 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.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.utils.Logger;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+
+/** Tests for {@link NavigationTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavigationTemplateTest {
+ private final ActionStrip mActionStrip =
+ ActionStrip.builder().addAction(TestUtils.createAction("test", null)).build();
+ private final Maneuver mManeuver =
+ Maneuver.builder(Maneuver.TYPE_FERRY_BOAT).setIcon(CarIcon.APP_ICON).build();
+ private final Step mCurrentStep =
+ Step.builder("Go Straight").setManeuver(mManeuver).setRoad("405").build();
+ private final Distance mCurrentDistance =
+ Distance.create(/* displayDistance= */ 100, Distance.UNIT_METERS);
+
+ @Test
+ public void noActionStrip_throws() {
+ assertThrows(IllegalStateException.class, () -> NavigationTemplate.builder().build());
+ }
+
+ /** Tests basic construction of a template with a minimal data. */
+ @Test
+ public void createMinimalInstance() {
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setNavigationInfo(
+ RoutingInfo.builder().setCurrentStep(mCurrentStep,
+ mCurrentDistance).build())
+ .setActionStrip(mActionStrip)
+ .build();
+ RoutingInfo routingInfo = (RoutingInfo) template.getNavigationInfo();
+ assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+ assertThat(routingInfo.getNextStep()).isNull();
+ assertThat(template.getBackgroundColor()).isNull();
+ assertThat(template.getDestinationTravelEstimate()).isNull();
+ assertThat(template.getActionStrip()).isEqualTo(mActionStrip);
+ }
+
+ /** Tests construction of a template with all data. */
+ @Test
+ public void createFullInstance() {
+ Maneuver nextManeuver =
+ Maneuver.builder(Maneuver.TYPE_U_TURN_LEFT).setIcon(CarIcon.APP_ICON).build();
+ Step nextStep = Step.builder("Turn Around").setManeuver(nextManeuver).setRoad(
+ "520").build();
+
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+ Duration.ofHours(1),
+ ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]"));
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(mCurrentStep, mCurrentDistance)
+ .setNextStep(nextStep)
+ .build())
+ .setBackgroundColor(CarColor.BLUE)
+ .setDestinationTravelEstimate(travelEstimate)
+ .setActionStrip(mActionStrip)
+ .build();
+ RoutingInfo routingInfo = (RoutingInfo) template.getNavigationInfo();
+ assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+ assertThat(routingInfo.getCurrentDistance()).isEqualTo(mCurrentDistance);
+ assertThat(routingInfo.getNextStep()).isEqualTo(nextStep);
+ assertThat(template.getBackgroundColor()).isEqualTo(CarColor.BLUE);
+ assertThat(template.getDestinationTravelEstimate()).isEqualTo(travelEstimate);
+ assertThat(template.getActionStrip()).isEqualTo(mActionStrip);
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+ Duration.ofHours(1),
+ ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]"));
+
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(
+ Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ NavigationTemplate reroutingTemplate =
+ NavigationTemplate.builder()
+ .setNavigationInfo(RoutingInfo.builder().setIsLoading(true).build())
+ .setActionStrip(mActionStrip)
+ .build();
+
+ NavigationTemplate navigatingTemplate =
+ NavigationTemplate.builder()
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build())
+ .setActionStrip(mActionStrip)
+ .setDestinationTravelEstimate(travelEstimate)
+ .setBackgroundColor(CarColor.BLUE)
+ .build();
+
+ NavigationTemplate arrivedTemplate =
+ NavigationTemplate.builder()
+ .setNavigationInfo(MessageInfo.builder("Arrived!").setText(
+ "name\naddress").build())
+ .setActionStrip(mActionStrip)
+ .build();
+
+ assertThat(navigatingTemplate.isRefresh(reroutingTemplate, logger)).isTrue();
+ assertThat(arrivedTemplate.isRefresh(navigatingTemplate, logger)).isTrue();
+ assertThat(reroutingTemplate.isRefresh(arrivedTemplate, logger)).isTrue();
+ }
+
+ @Test
+ public void equals() {
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+ Duration.ofHours(1),
+ ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]"));
+
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(
+ Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setDestinationTravelEstimate(travelEstimate)
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build())
+ .setBackgroundColor(CarColor.BLUE)
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setDestinationTravelEstimate(travelEstimate)
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build())
+ .setBackgroundColor(CarColor.BLUE)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ NavigationTemplate template = NavigationTemplate.builder().setActionStrip(
+ mActionStrip).build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ NavigationTemplate.builder()
+ .setActionStrip(
+ ActionStrip.builder().addAction(
+ TestUtils.createAction("title2", null)).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTravelEstimate() {
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+ Duration.ofHours(1),
+ ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]"));
+
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setDestinationTravelEstimate(travelEstimate)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setDestinationTravelEstimate(
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 21000,
+ Distance.UNIT_METERS),
+ Duration.ofHours(1),
+ ZonedDateTime.parse(
+ "2020-05-14T19:57:00-07:00[US/Pacific]")))
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentCurrentStep() {
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(
+ Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(
+ RoutingInfo.builder().setCurrentStep(currentStep,
+ currentDistance).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(RoutingInfo.builder()
+ .setCurrentStep(Step.builder("do a back flip")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT,
+ false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build(),
+ currentDistance)
+ .build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentCurrentDistance() {
+ Step currentStep = Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(
+ RoutingInfo.builder().setCurrentStep(currentStep,
+ currentDistance).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(
+ currentStep,
+ Distance.create(/* displayDistance= */ 200,
+ Distance.UNIT_METERS))
+ .build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentJunctionImage() {
+ Step currentStep = Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ERROR)
+ .setNextStep(currentStep)
+ .build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentNextStep() {
+ Step currentStep = Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setNextStep(currentStep)
+ .build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setNavigationInfo(RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setNextStep(Step.builder("Do a backflip")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT,
+ false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build())
+ .build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentBackgroundColors() {
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setBackgroundColor(CarColor.BLUE)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setBackgroundColor(CarColor.GREEN)
+ .build());
+ }
+
+ @Test
+ public void checkPermissions_hasPermissions() {
+ //TODO(rampara): Investigate failure to create ShadowPackageManager
+// NavigationTemplate template =
+// NavigationTemplate.builder()
+// .setActionStrip(mActionStrip)
+// .setBackgroundColor(CarColor.BLUE)
+// .build();
+//
+// Context context = ApplicationProvider.getApplicationContext();
+// PackageManager packageManager = context.getPackageManager();
+// PackageInfo pi = new PackageInfo();
+// pi.packageName = context.getPackageName();
+// pi.versionCode = 1;
+// pi.requestedPermissions = new String[]{CarAppPermission.NAVIGATION_TEMPLATES};
+// shadowOf(packageManager).installPackage(pi);
+//
+// // Expect that it does not throw
+// template.checkPermissions(context);
+ }
+
+ @Test
+ public void checkPermissions_doesNotHavePermissions() {
+ NavigationTemplate template =
+ NavigationTemplate.builder()
+ .setActionStrip(mActionStrip)
+ .setBackgroundColor(CarColor.BLUE)
+ .build();
+
+ assertThrows(
+ SecurityException.class,
+ () -> template.checkPermissions(ApplicationProvider.getApplicationContext()));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
new file mode 100644
index 0000000..18a4946
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
@@ -0,0 +1,524 @@
+/*
+ * 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.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.LatLng;
+import androidx.car.app.model.Metadata;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceListNavigationTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceListNavigationTemplateTest {
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final DistanceSpan mDistanceSpan =
+ DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+
+ @Test
+ public void createInstance_emptyList_notLoading_Throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PlaceListNavigationTemplate.builder().setTitle("Title").build());
+
+ // Positive case
+ PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_isLoading_hasList_Throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setIsLoading(true)
+ .setItemList(ItemList.builder().build())
+ .build());
+ }
+
+ @Test
+ public void addList_selectable_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, true,
+ mDistanceSpan))
+ .build());
+
+ // Positive cases.
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .build();
+ }
+
+ @Test
+ public void addList_moreThanMaxTexts_throws() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row rowExceedsMaxTexts =
+ Row.builder().setTitle(title).addText("text1").addText("text2").addText(
+ "text3").build();
+ Row rowMeetingMaxTexts =
+ Row.builder().setTitle(title).addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowExceedsMaxTexts).build())
+ .build());
+
+ // Positive cases.
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+ .build();
+ }
+
+ @Test
+ public void addList_hasToggle_throws() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row rowWithToggle =
+ Row.builder().setTitle(title).setToggle(Toggle.builder(isChecked -> {
+ }).build()).build();
+ Row rowMeetingRestrictions =
+ Row.builder().setTitle(title).addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowWithToggle).build())
+ .build());
+
+ // Positive cases.
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowMeetingRestrictions).build())
+ .build();
+ }
+
+ @Test
+ public void createInstance_noHeaderTitleOrAction_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ PlaceListNavigationTemplate.builder().setItemList(
+ ItemList.builder().build()).build());
+
+ // Positive cases.
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().build())
+ .build();
+ PlaceListNavigationTemplate.builder()
+ .setHeaderAction(Action.BACK)
+ .setItemList(ItemList.builder().build())
+ .build();
+ }
+
+ @Test
+ public void createEmpty() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().build())
+ .build();
+ assertThat(template.getItemList().getItems()).isEmpty();
+ assertThat(template.getTitle().getText()).isEqualTo("Title");
+ assertThat(template.getActionStrip()).isNull();
+ }
+
+ @Test
+ public void createInstance() {
+ String title = "title";
+ ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+ ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setItemList(itemList)
+ .setTitle(title)
+ .setActionStrip(actionStrip)
+ .build();
+ assertThat(template.getItemList()).isEqualTo(itemList);
+ assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+ assertThat(template.getTitle().getText()).isEqualTo(title);
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void createInstance_setHeaderAction() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setItemList(ItemList.builder().build())
+ .setHeaderAction(Action.BACK)
+ .build();
+
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ }
+
+ @Test
+ public void createInstance_notAllRowHaveDistances() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row rowWithDistance = Row.builder().setTitle(title).build();
+ Row rowWithoutDistance = Row.builder().setTitle("Google Kir").build();
+ Row browsableRowWithoutDistance =
+ Row.builder()
+ .setTitle("Google Kir")
+ .setBrowsable(true)
+ .setOnClickListener(() -> {
+ })
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder().addItem(rowWithDistance).addItem(
+ rowWithoutDistance).build())
+ .build());
+
+ // Positive cases
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(rowWithDistance).build())
+ .build();
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(rowWithDistance)
+ .addItem(browsableRowWithoutDistance)
+ .build())
+ .build();
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(browsableRowWithoutDistance).build())
+ .build();
+ }
+
+ @Test
+ public void createInstance_rowHasBothMarkerAndImages() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row row =
+ Row.builder()
+ .setTitle("Google Kir")
+ .setOnClickListener(() -> {
+ })
+ .setImage(CarIcon.ALERT)
+ .setMetadata(
+ Metadata.ofPlace(
+ Place.builder(LatLng.create(10.f, 10.f))
+ .setMarker(PlaceMarker.getDefault())
+ .build()))
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(row).build()));
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ Row.Builder row =
+ Row.builder().setTitle(title).setBrowsable(true).setOnClickListener(() -> {
+ });
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(row.build()).build())
+ .build();
+
+ assertThat(template.isRefresh(template, logger)).isTrue();
+
+ // Going from loading state to new content is allowed.
+ assertThat(
+ template.isRefresh(
+ PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(
+ true).build(),
+ logger))
+ .isTrue();
+
+ // Other allowed mutable states.
+ SpannableString stringWithSpan = new SpannableString("Title");
+ stringWithSpan.setSpan(mDistanceSpan, 1, /* end= */ 2, /* flags= */ 0);
+ assertThat(template.isRefresh(PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder()
+ .addItem(row.setOnClickListener(() -> {
+ })
+ .setBrowsable(false)
+ .setTitle(stringWithSpan)
+ .setImage(CarIcon.of(
+ IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .setMetadata(Metadata.ofPlace(
+ Place.builder(
+ LatLng.create(
+ 1,
+ 1)).build()))
+ .build())
+ .build())
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build(),
+ logger))
+ .isTrue();
+
+ // Title updates are disallowed.
+ assertThat(
+ template.isRefresh(
+ PlaceListNavigationTemplate.builder()
+ .setItemList(ItemList.builder().addItem(row.build()).build())
+ .setTitle("Title2")
+ .build(),
+ logger))
+ .isFalse();
+
+ // Text updates are disallowed.
+ SpannableString title2 = new SpannableString("Title2");
+ title2.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+ assertThat(
+ template.isRefresh(
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(
+ row.setTitle(title2).build()).build())
+ .build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(row.setTitle(title).addText(
+ "Text").build())
+ .build())
+ .build(),
+ logger))
+ .isFalse();
+
+ // Additional rows are disallowed.
+ assertThat(
+ template.isRefresh(
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder().addItem(row.build()).addItem(
+ row.build()).build())
+ .build(),
+ logger))
+ .isFalse();
+
+ // Going from content to loading state is disallowed.
+ assertThat(
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setIsLoading(true)
+ .build()
+ .isRefresh(template, logger))
+ .isFalse();
+ }
+
+ @Test
+ public void equals() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .setTitle("title")
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ PlaceListNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setHeaderAction(Action.BACK)
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.BACK).build())
+ .setTitle("title")
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentItemList() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(5, false,
+ mDistanceSpan))
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setHeaderAction(Action.BACK)
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setHeaderAction(Action.APP_ICON)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setTitle("title")
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ PlaceListNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+ mDistanceSpan))
+ .setTitle("other")
+ .build());
+ }
+
+ @Test
+ public void checkPermissions_hasPermissions() {
+ //TODO(rampara): Investigate failure to create ShadowPackageManager
+// PlaceListNavigationTemplate template =
+// PlaceListNavigationTemplate.builder()
+// .setItemList(
+// TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+// .setTitle("title")
+// .build();
+//
+// PackageManager packageManager = mContext.getPackageManager();
+// PackageInfo pi = new PackageInfo();
+// pi.packageName = mContext.getPackageName();
+// pi.versionCode = 1;
+// pi.requestedPermissions = new String[]{CarAppPermission.NAVIGATION_TEMPLATES};
+// shadowOf(packageManager).installPackage(pi);
+//
+// // Expect that it does not throw
+// template.checkPermissions(mContext);
+ }
+
+ @Test
+ public void checkPermissions_doesNotHavePermissions() {
+ PlaceListNavigationTemplate template =
+ PlaceListNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+ .setTitle("title")
+ .build();
+
+ assertThrows(SecurityException.class, () -> template.checkPermissions(mContext));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
new file mode 100644
index 0000000..ac7252b
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
@@ -0,0 +1,615 @@
+/*
+ * 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.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Row;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RoutePreviewNavigationTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RoutePreviewNavigationTemplateTest {
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private static final DistanceSpan DISTANCE =
+ DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+
+ @Test
+ public void createInstance_emptyList_notLoading_Throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> RoutePreviewNavigationTemplate.builder().setTitle("Title").build());
+
+ // Positive case
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_isLoading_hasList_Throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setIsLoading(true)
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .build());
+ }
+
+ @Test
+ public void addList_notSelectable_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(3, false,
+ DISTANCE)));
+
+ // Positive case.
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(3, true, DISTANCE));
+ }
+
+ @Test
+ public void addList_moreThanMaxTexts_throws() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(DISTANCE, 0, 1, 0);
+ Row rowExceedsMaxTexts =
+ Row.builder().setTitle(title).addText("text1").addText("text2").addText(
+ "text3").build();
+ Row rowMeetingMaxTexts =
+ Row.builder().setTitle(title).addText("text1").addText("text2").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(rowExceedsMaxTexts)
+ .setSelectable(selectedIndex -> {
+ })
+ .build()));
+
+ // Positive case.
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(rowMeetingMaxTexts)
+ .setSelectable(selectedIndex -> {
+ })
+ .build());
+ }
+
+ @Test
+ public void noHeaderTitleOrAction_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> RoutePreviewNavigationTemplate.builder().setIsLoading(true).build());
+
+ // Positive cases.
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ RoutePreviewNavigationTemplate.builder()
+ .setHeaderAction(Action.BACK)
+ .setIsLoading(true)
+ .build();
+ }
+
+ @Test
+ public void createInstance() {
+ ItemList itemList = TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE);
+ String title = "title";
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(itemList)
+ .setTitle(title)
+ .setNavigateAction(
+ Action.builder().setTitle("Navigate").setOnClickListener(() -> {
+ }).build())
+ .build();
+ assertThat(template.getItemList()).isEqualTo(itemList);
+ assertThat(template.getTitle().getText()).isEqualTo(title);
+ }
+
+ @Test
+ public void createInstance_setHeaderAction_invalidActionThrows() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("Navigate").setOnClickListener(
+ () -> {
+ }).build())
+ .setHeaderAction(
+ Action.builder().setTitle("Action").setOnClickListener(
+ () -> {
+ }).build()));
+ }
+
+ @Test
+ public void createInstance_setHeaderAction() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("Navigate").setOnClickListener(() -> {
+ }).build())
+ .setHeaderAction(Action.BACK)
+ .build();
+
+ assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+ }
+
+ @Test
+ public void setOnNavigateAction() throws RemoteException {
+ // TODO(rampara): Confirm that UiThreadTest annotation is required for test.
+// OnClickListener mockListener = mock(OnClickListener.class);
+// RoutePreviewNavigationTemplate template =
+// RoutePreviewNavigationTemplate.builder()
+// .setTitle("Title")
+// .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+// .setNavigateAction(
+// Action.builder().setTitle("Navigate").setOnClickListener(
+// mockListener).build())
+// .build();
+//
+// template.getNavigateAction()
+// .getOnClickListener()
+// .getListener()
+// .onClick(mock(IOnDoneCallback.class));
+// verify(mockListener).onClick();
+ }
+
+ @Test
+ public void createInstance_emptyNavigateAction_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .build());
+
+ // Positive case
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_emptyListeners_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, false,
+ DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("Navigate").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+
+ // Positive case
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ }
+
+ @Test
+ public void createInstance_navigateActionNoTitle_throws() {
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(Action.builder().setIcon(carIcon).setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+
+ // Positive case
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(Action.builder()
+ .setIcon(carIcon)
+ .setTitle("Navigate")
+ .setOnClickListener(() -> {
+ })
+ .build())
+ .build();
+ }
+
+ @Test
+ public void createInstance_notAllRowsHaveTime() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(DISTANCE, 0, 1, 0);
+ Row rowWithTime = Row.builder().setTitle(title).build();
+ Row rowWithoutTime = Row.builder().setTitle("Google Bve").build();
+ Action navigateAction = Action.builder()
+ .setIcon(CarIcon.of(IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .setTitle("Navigate")
+ .setOnClickListener(() -> {
+ })
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder()
+ .addItem(rowWithTime)
+ .addItem(rowWithoutTime)
+ .setSelectable(index -> {
+ })
+ .build())
+ .setNavigateAction(navigateAction)
+ .build());
+
+ // Positive case
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().setSelectable(index -> {
+ }).addItem(rowWithTime).build())
+ .setNavigateAction(navigateAction)
+ .build();
+ }
+
+ @Test
+ public void validate_isRefresh() {
+ Logger logger = message -> {
+ };
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(DISTANCE, 0, 1, 0);
+ Row.Builder row = Row.builder().setTitle(title);
+ Action navigateAction = Action.builder().setTitle("Navigate").setOnClickListener(() -> {
+ }).build();
+ RoutePreviewNavigationTemplate template = RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder().addItem(row.build()).setSelectable(
+ index -> {
+ }).build())
+ .setNavigateAction(navigateAction)
+ .build();
+
+ assertThat(template.isRefresh(template, logger)).isTrue();
+
+ // Going from loading state to new content is allowed.
+ assertThat(template.isRefresh(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setIsLoading(true)
+ .build(),
+ logger))
+ .isTrue();
+
+ // Other allowed mutable states.
+ SpannableString stringWithSpan = new SpannableString("Title");
+ stringWithSpan.setSpan(DISTANCE, 1, /* end= */ 2, /* flags= */ 0);
+ assertThat(template.isRefresh(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(ItemList.builder()
+ .addItem(row.setImage(
+ CarIcon.of(IconCompat.createWithResource(
+ ApplicationProvider.getApplicationContext(),
+ R.drawable.ic_test_1)))
+ .setTitle(stringWithSpan)
+ .build())
+ .setSelectable(index -> {
+ })
+ .build())
+ .setHeaderAction(Action.BACK)
+ .setNavigateAction(Action.builder().setTitle(
+ "Navigate2").setOnClickListener(() -> {
+ }
+ ).build())
+ .setActionStrip(ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .build(),
+ logger))
+ .isTrue();
+
+ // Title updates are disallowed.
+ assertThat(template.isRefresh(
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(ItemList.builder().addItem(row.build()).setSelectable(
+ index -> {
+ }).build())
+ .setTitle("Title2")
+ .setNavigateAction(navigateAction)
+ .build(),
+ logger))
+ .isFalse();
+
+ // Text updates are disallowed.
+ SpannableString title2 = new SpannableString("Title2");
+ title2.setSpan(DISTANCE, 0, 1, 0);
+ assertThat(
+ template.isRefresh(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(row.setTitle(title2).build())
+ .setSelectable(index -> {
+ })
+ .build())
+ .setNavigateAction(navigateAction)
+ .build(),
+ logger))
+ .isFalse();
+ assertThat(
+ template.isRefresh(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(row.addText("Text").build())
+ .setSelectable(index -> {
+ })
+ .build())
+ .setNavigateAction(navigateAction)
+ .build(),
+ logger))
+ .isFalse();
+
+ // Additional rows are disallowed.
+ assertThat(
+ template.isRefresh(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ ItemList.builder()
+ .addItem(row.build())
+ .addItem(row.build())
+ .setSelectable(index -> {
+ })
+ .build())
+ .setNavigateAction(navigateAction)
+ .build(),
+ logger))
+ .isFalse();
+
+ // Going from content to loading state is disallowed.
+ assertThat(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setIsLoading(true)
+ .build()
+ .isRefresh(template, logger))
+ .isFalse();
+ }
+
+ @Test
+ public void equals() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .setTitle("title")
+ .setHeaderAction(Action.BACK)
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ assertThat(template)
+ .isEqualTo(
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.BACK).build())
+ .setTitle("title")
+ .setHeaderAction(Action.BACK)
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentItemList() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(1, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentHeaderAction() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setHeaderAction(Action.BACK)
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setHeaderAction(Action.APP_ICON)
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentActionStrip() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setActionStrip(
+ ActionStrip.builder().addAction(Action.APP_ICON).build())
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentTitle() {
+ SpannableString title = new SpannableString("Title");
+ title.setSpan(DISTANCE, 0, 1, 0);
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setTitle(title)
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ SpannableString title2 = new SpannableString("Title2");
+ title2.setSpan(DISTANCE, 0, 1, 0);
+ assertThat(template)
+ .isNotEqualTo(
+ RoutePreviewNavigationTemplate.builder()
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setTitle(title2)
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(
+ () -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentNavigateAction() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ assertThat(template)
+ .isNotEqualTo(
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(
+ TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("stop").setOnClickListener(() -> {
+ }).build())
+ .build());
+ }
+
+ @Test
+ public void checkPermissions_hasPermissions() {
+ //TODO(rampara): Investigate failure to create ShadowPackageManager
+// RoutePreviewNavigationTemplate template =
+// RoutePreviewNavigationTemplate.builder()
+// .setTitle("Title")
+// .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+// .setNavigateAction(
+// Action.builder().setTitle("drive").setOnClickListener(() -> {
+// }).build())
+// .build();
+//
+// PackageManager packageManager = mContext.getPackageManager();
+// PackageInfo pi = new PackageInfo();
+// pi.packageName = mContext.getPackageName();
+// pi.versionCode = 1;
+// pi.requestedPermissions = new String[]{CarAppPermission.NAVIGATION_TEMPLATES};
+// shadowOf(packageManager).installPackage(pi);
+//
+// // Expect that it does not throw
+// template.checkPermissions(mContext);
+ }
+
+ @Test
+ public void checkPermissions_doesNotHavePermissions() {
+ RoutePreviewNavigationTemplate template =
+ RoutePreviewNavigationTemplate.builder()
+ .setTitle("Title")
+ .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+ .setNavigateAction(
+ Action.builder().setTitle("drive").setOnClickListener(() -> {
+ }).build())
+ .build();
+
+ assertThrows(SecurityException.class, () -> template.checkPermissions(mContext));
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
new file mode 100644
index 0000000..58ea716
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RoutingInfoTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RoutingInfoTest {
+
+ private final Maneuver mManeuver =
+ Maneuver.builder(Maneuver.TYPE_FERRY_BOAT).setIcon(CarIcon.APP_ICON).build();
+ private final Step mCurrentStep =
+ Step.builder("Go Straight").setManeuver(mManeuver).setRoad("405").build();
+ private final Distance mCurrentDistance =
+ Distance.create(/* displayDistance= */ 100, Distance.UNIT_METERS);
+
+ @Test
+ public void noCurrentStep_throws() {
+ assertThrows(IllegalStateException.class, () -> RoutingInfo.builder().build());
+ }
+
+ @Test
+ public void isLoading_throws_when_not_empty() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> RoutingInfo.builder()
+ .setIsLoading(true)
+ .setCurrentStep(mCurrentStep, mCurrentDistance)
+ .build());
+ }
+
+ @Test
+ public void invalidCarIcon_throws() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.appendPath("foo/bar");
+ Uri iconUri = builder.build();
+ CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> RoutingInfo.builder().setJunctionImage(carIcon));
+ }
+
+ /** Tests basic construction of a template with a minimal data. */
+ @Test
+ public void createMinimalInstance() {
+ RoutingInfo routingInfo =
+ RoutingInfo.builder().setCurrentStep(mCurrentStep, mCurrentDistance).build();
+ assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+ assertThat(routingInfo.getNextStep()).isNull();
+ }
+
+ /** Tests construction of a template with all data. */
+ @Test
+ public void createFullInstance() {
+ Maneuver nextManeuver =
+ Maneuver.builder(Maneuver.TYPE_U_TURN_LEFT).setIcon(CarIcon.APP_ICON).build();
+ Step nextStep = Step.builder("Turn Around").setManeuver(nextManeuver).setRoad(
+ "520").build();
+
+ RoutingInfo routingInfo = RoutingInfo.builder()
+ .setCurrentStep(mCurrentStep, mCurrentDistance)
+ .setNextStep(nextStep)
+ .build();
+ assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+ assertThat(routingInfo.getCurrentDistance()).isEqualTo(mCurrentDistance);
+ assertThat(routingInfo.getNextStep()).isEqualTo(nextStep);
+ }
+
+ @Test
+ public void laneInfo_set_no_lanesImage_throws() {
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+ assertThrows(
+ IllegalStateException.class,
+ () -> RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build());
+ }
+
+ @Test
+ public void laneInfo_set_with_lanesImage_doesnt_throws() {
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+ RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build();
+ }
+
+ @Test
+ public void equals() {
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ RoutingInfo routingInfo =
+ RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build();
+
+ assertThat(routingInfo)
+ .isEqualTo(RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentCurrentStep() {
+ Step currentStep =
+ Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ RoutingInfo routingInfo =
+ RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build();
+
+ assertThat(routingInfo)
+ .isNotEqualTo(RoutingInfo.builder()
+ .setCurrentStep(Step.builder("do a back flip")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT,
+ false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build(),
+ currentDistance)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentCurrentDistance() {
+ Step currentStep = Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.APP_ICON)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ RoutingInfo routingInfo =
+ RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build();
+
+ assertThat(routingInfo)
+ .isNotEqualTo(RoutingInfo.builder()
+ .setCurrentStep(currentStep,
+ Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS))
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentJunctionImage() {
+ Step currentStep = Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ RoutingInfo routingInfo = RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ALERT)
+ .setNextStep(currentStep)
+ .build();
+
+ assertThat(routingInfo)
+ .isNotEqualTo(RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setJunctionImage(CarIcon.ERROR)
+ .setNextStep(currentStep)
+ .build());
+ }
+
+ @Test
+ public void notEquals_differentNextStep() {
+ Step currentStep = Step.builder("Hop on a ferry")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build();
+ Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+
+ RoutingInfo routingInfo = RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setNextStep(currentStep)
+ .build();
+
+ assertThat(routingInfo)
+ .isNotEqualTo(RoutingInfo.builder()
+ .setCurrentStep(currentStep, currentDistance)
+ .setNextStep(Step.builder("Do a backflip")
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(
+ LaneDirection.SHAPE_NORMAL_LEFT, false))
+ .build())
+ .setLanesImage(CarIcon.ALERT)
+ .build())
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
new file mode 100644
index 0000000..bc8a79d
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_SHARP_LEFT;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Step}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StepTest {
+ @Test
+ public void createInstance() {
+ Lane lane = Lane.builder().addDirection(
+ LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+ Maneuver maneuver =
+ Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+ .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+ .setIcon(CarIcon.APP_ICON)
+ .build();
+ String cue = "Left at State street.";
+ String road = "State St.";
+ Step step =
+ Step.builder(cue)
+ .addLane(lane)
+ .setLanesImage(CarIcon.APP_ICON)
+ .setManeuver(maneuver)
+ .setRoad(road)
+ .build();
+
+ assertThat(step.getLanes()).hasSize(1);
+ assertThat(lane).isEqualTo(step.getLanes().get(0));
+ assertThat(CarIcon.APP_ICON).isEqualTo(step.getLanesImage());
+ assertThat(maneuver).isEqualTo(step.getManeuver());
+ assertThat(cue).isEqualTo(step.getCue().getText());
+ assertThat(road).isEqualTo(step.getRoad().getText());
+ }
+
+ @Test
+ public void clearLanes() {
+ Lane lane1 = Lane.builder().addDirection(
+ LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+ Lane lane2 = Lane.builder()
+ .addDirection(LaneDirection.create(LaneDirection.SHAPE_SHARP_RIGHT, true))
+ .build();
+ String cue = "Left at State street.";
+ Step step = Step.builder(cue).addLane(lane1).addLane(lane2).clearLanes().build();
+
+ assertThat(step.getLanes()).hasSize(0);
+ }
+
+ @Test
+ public void createInstance_lanesImage_no_lanes_throws() {
+ String cue = "Left at State street.";
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> Step.builder(cue).setLanesImage(CarIcon.APP_ICON).build());
+ }
+
+ @Test
+ public void equals() {
+ Lane lane = Lane.builder().addDirection(
+ LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+ Maneuver maneuver = Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+ .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+ .setIcon(CarIcon.APP_ICON)
+ .build();
+ String cue = "Left at State street.";
+ String road = "State St.";
+ Step step = Step.builder(cue)
+ .addLane(lane)
+ .setLanesImage(CarIcon.APP_ICON)
+ .setManeuver(maneuver)
+ .setRoad(road)
+ .build();
+
+ assertThat(Step.builder(cue)
+ .addLane(lane)
+ .setLanesImage(CarIcon.APP_ICON)
+ .setManeuver(maneuver)
+ .setRoad(road)
+ .build())
+ .isEqualTo(step);
+ }
+
+ @Test
+ public void notEquals_differentCue() {
+ String cue = "Left at State street.";
+ Step step = Step.builder(cue).build();
+
+ assertThat(Step.builder("foo").build()).isNotEqualTo(step);
+ }
+
+ @Test
+ public void notEquals_differentLane() {
+ Lane lane = Lane.builder().addDirection(
+ LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+ String cue = "Left at State street.";
+
+ Step step = Step.builder(cue).addLane(lane).build();
+
+ assertThat(Step.builder(cue)
+ .addLane(Lane.builder()
+ .addDirection(LaneDirection.create(SHAPE_SHARP_LEFT, false))
+ .build())
+ .build())
+ .isNotEqualTo(step);
+ }
+
+ @Test
+ public void notEquals_differentLanesImage() {
+ String cue = "Left at State street.";
+ Lane lane = Lane.builder().addDirection(
+ LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+
+ Step step = Step.builder(cue).addLane(lane).setLanesImage(CarIcon.APP_ICON).build();
+
+ assertThat(Step.builder(cue).addLane(lane).setLanesImage(CarIcon.ALERT).build())
+ .isNotEqualTo(step);
+ }
+
+ @Test
+ public void notEquals_differentManeuver() {
+ Maneuver maneuver =
+ Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+ .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+ .setIcon(CarIcon.APP_ICON)
+ .build();
+ String cue = "Left at State street.";
+
+ Step step = Step.builder(cue).setManeuver(maneuver).build();
+
+ assertThat(Step.builder(cue)
+ .setManeuver(Maneuver.builder(Maneuver.TYPE_DESTINATION).setIcon(
+ CarIcon.APP_ICON).build())
+ .build())
+ .isNotEqualTo(step);
+ }
+
+ @Test
+ public void notEquals_differentRoad() {
+ String cue = "Left at State street.";
+
+ Step step = Step.builder(cue).setRoad("road").build();
+
+ assertThat(Step.builder(cue).setRoad("foo").build()).isNotEqualTo(step);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
new file mode 100644
index 0000000..2e95d5e
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.TestUtils.assertDateTimeWithZoneEquals;
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Build.VERSION_CODES;
+
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.Distance;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Tests for {@link TravelEstimate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TravelEstimateTest {
+ private final DateTimeWithZone mArrivalTime =
+ createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+ private final Distance mRemainingDistance =
+ Distance.create(/* displayDistance= */ 100, Distance.UNIT_METERS);
+ private final Duration mRemainingTime = Duration.ofHours(10);
+
+ @Test
+ @Config(minSdk = VERSION_CODES.O)
+ public void create_duration() {
+ ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(mRemainingDistance, mRemainingTime, arrivalTime);
+
+ assertThat(travelEstimate.getRemainingDistance()).isEqualTo(mRemainingDistance);
+ assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(mRemainingTime.getSeconds());
+ assertDateTimeWithZoneEquals(arrivalTime, travelEstimate.getArrivalTimeAtDestination());
+ }
+
+ @Test
+ public void create() {
+ DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+ Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+ Duration remainingTime = Duration.ofHours(10);
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(remainingDistance, remainingTime.getSeconds(), arrivalTime);
+
+ assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+ assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(remainingTime.getSeconds());
+ assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+ assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(CarColor.DEFAULT);
+ }
+
+ @Test
+ public void create_custom_remainingTimeColor() {
+ DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+ Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+ Duration remainingTime = Duration.ofHours(10);
+
+ List<CarColor> allowedColors = new ArrayList<>();
+ allowedColors.add(CarColor.DEFAULT);
+ allowedColors.add(CarColor.PRIMARY);
+ allowedColors.add(CarColor.SECONDARY);
+ allowedColors.add(CarColor.RED);
+ allowedColors.add(CarColor.GREEN);
+ allowedColors.add(CarColor.BLUE);
+ allowedColors.add(CarColor.YELLOW);
+
+ for (CarColor carColor : allowedColors) {
+ TravelEstimate travelEstimate =
+ TravelEstimate.builder(remainingDistance, remainingTime.getSeconds(),
+ arrivalTime)
+ .setRemainingTimeColor(carColor)
+ .build();
+
+ assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+ assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+ remainingTime.getSeconds());
+ assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+ assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(carColor);
+ }
+ }
+
+ @Test
+ public void create_custom_remainingDistanceColor() {
+ DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+ Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+ Duration remainingTime = Duration.ofHours(10);
+
+ List<CarColor> allowedColors = new ArrayList<>();
+ allowedColors.add(CarColor.DEFAULT);
+ allowedColors.add(CarColor.PRIMARY);
+ allowedColors.add(CarColor.SECONDARY);
+ allowedColors.add(CarColor.RED);
+ allowedColors.add(CarColor.GREEN);
+ allowedColors.add(CarColor.BLUE);
+ allowedColors.add(CarColor.YELLOW);
+
+ for (CarColor carColor : allowedColors) {
+ TravelEstimate travelEstimate =
+ TravelEstimate.builder(remainingDistance, remainingTime.getSeconds(),
+ arrivalTime)
+ .setRemainingDistanceColor(carColor)
+ .build();
+
+ assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+ assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+ remainingTime.getSeconds());
+ assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+ assertThat(travelEstimate.getRemainingDistanceColor()).isEqualTo(carColor);
+ }
+ }
+
+ @Test
+ public void create_custom_remainingTimeColor_invalid_throws() {
+ DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+ Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+ Distance.UNIT_METERS);
+ Duration remainingTime = Duration.ofHours(10);
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ TravelEstimate.builder(remainingDistance, remainingTime.getSeconds(),
+ arrivalTime)
+ .setRemainingTimeColor(CarColor.createCustom(1, 2)));
+ }
+
+ @Test
+ public void equals() {
+ TravelEstimate travelEstimate = TravelEstimate.create(mRemainingDistance,
+ mRemainingTime.getSeconds(), mArrivalTime);
+
+ assertThat(travelEstimate)
+ .isEqualTo(
+ TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+ mArrivalTime));
+ }
+
+ @Test
+ public void notEquals_differentRemainingDistance() {
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+ mArrivalTime);
+
+ assertThat(travelEstimate)
+ .isNotEqualTo(
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS),
+ mRemainingTime.getSeconds(),
+ mArrivalTime));
+ }
+
+ @Test
+ public void notEquals_differentRemainingTime() {
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+ mArrivalTime);
+
+ assertThat(travelEstimate)
+ .isNotEqualTo(
+ TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds() + 1,
+ mArrivalTime));
+ }
+
+ @Test
+ public void notEquals_differentArrivalTime() {
+ TravelEstimate travelEstimate =
+ TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+ mArrivalTime);
+
+ assertThat(travelEstimate)
+ .isNotEqualTo(
+ TravelEstimate.create(
+ mRemainingDistance,
+ mRemainingTime.getSeconds(),
+ createDateTimeWithZone("2020-04-14T15:57:01", "US/Pacific")));
+ }
+
+ @Test
+ public void notEquals_differentRemainingTimeColor() {
+ TravelEstimate travelEstimate =
+ TravelEstimate.builder(mRemainingDistance, mRemainingTime.getSeconds(),
+ mArrivalTime)
+ .setRemainingTimeColor(CarColor.YELLOW)
+ .build();
+
+ assertThat(travelEstimate)
+ .isNotEqualTo(
+ TravelEstimate.builder(mRemainingDistance, mRemainingTime.getSeconds(),
+ mArrivalTime)
+ .setRemainingTimeColor(CarColor.GREEN)
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
new file mode 100644
index 0000000..761fa5b
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_SHARP_LEFT;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+
+/** Tests for {@link Trip}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TripTest {
+
+ private final Step mStep =
+ Step.builder("Take the second exit of the roundabout.")
+ .addLane(Lane.builder().addDirection(
+ LaneDirection.create(SHAPE_SHARP_LEFT, true)).build())
+ .setManeuver(Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+ .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+ .setIcon(CarIcon.APP_ICON)
+ .build())
+ .build();
+ private final Destination mDestination =
+ Destination.builder().setName("Google BVE").setAddress("1120 112th Ave NE").build();
+ private final TravelEstimate mStepTravelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS),
+ Duration.ofHours(1).getSeconds(),
+ createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"));
+ private final TravelEstimate mDestinationTravelEstimate =
+ TravelEstimate.create(
+ Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
+ Duration.ofHours(1).getSeconds(),
+ createDateTimeWithZone("2020-04-14T16:57:00", "US/Pacific"));
+ private static final String ROAD = "State St.";
+
+ @Test
+ public void createInstance() {
+ Trip trip =
+ Trip.builder()
+ .addDestination(mDestination)
+ .addStep(mStep)
+ .addDestinationTravelEstimate(mDestinationTravelEstimate)
+ .addStepTravelEstimate(mStepTravelEstimate)
+ .setCurrentRoad(ROAD)
+ .setIsLoading(false)
+ .build();
+
+ assertThat(trip.getDestinations()).hasSize(1);
+ assertThat(mDestination).isEqualTo(trip.getDestinations().get(0));
+ assertThat(trip.getSteps()).hasSize(1);
+ assertThat(mStep).isEqualTo(trip.getSteps().get(0));
+ assertThat(trip.getDestinationTravelEstimates()).hasSize(1);
+ assertThat(mDestinationTravelEstimate).isEqualTo(
+ trip.getDestinationTravelEstimates().get(0));
+ assertThat(trip.getStepTravelEstimates()).hasSize(1);
+ assertThat(mStepTravelEstimate).isEqualTo(trip.getStepTravelEstimates().get(0));
+ assertThat(trip.isLoading()).isFalse();
+ }
+
+ @Test
+ public void getDestinationWithEstimates_mismatch_count() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Trip.builder().addDestination(mDestination).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Trip.builder().addDestinationTravelEstimate(
+ mDestinationTravelEstimate).build());
+ }
+
+ @Test
+ public void getStepWithEstimates_mismatch_count() {
+ assertThrows(IllegalArgumentException.class, () -> Trip.builder().addStep(mStep).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).build());
+ }
+
+ @Test
+ public void createInstance_loading_no_steps() {
+ Trip trip =
+ Trip.builder()
+ .addDestination(mDestination)
+ .addDestinationTravelEstimate(mDestinationTravelEstimate)
+ .setCurrentRoad(ROAD)
+ .setIsLoading(true)
+ .build();
+
+ assertThat(trip.getDestinations()).hasSize(1);
+ assertThat(mDestination).isEqualTo(trip.getDestinations().get(0));
+ assertThat(trip.getSteps()).hasSize(0);
+ assertThat(trip.getDestinationTravelEstimates()).hasSize(1);
+ assertThat(mDestinationTravelEstimate).isEqualTo(
+ trip.getDestinationTravelEstimates().get(0));
+ assertThat(trip.getStepTravelEstimates()).hasSize(0);
+ assertThat(trip.isLoading()).isTrue();
+ }
+
+ @Test
+ public void createInstance_loading_with_steps() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Trip.builder().addStep(mStep).setIsLoading(true).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).setIsLoading(
+ true).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Trip.builder()
+ .addStep(mStep)
+ .addStepTravelEstimate(mStepTravelEstimate)
+ .setIsLoading(true)
+ .build());
+ }
+}
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_1.xml b/car/app/app/src/androidTest/res/drawable/ic_test_1.xml
new file mode 100644
index 0000000..88398fc
--- /dev/null
+++ b/car/app/app/src/androidTest/res/drawable/ic_test_1.xml
@@ -0,0 +1,24 @@
+<!--
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"/>
+</vector>
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_2.xml b/car/app/app/src/androidTest/res/drawable/ic_test_2.xml
new file mode 100644
index 0000000..cdcdf20c
--- /dev/null
+++ b/car/app/app/src/androidTest/res/drawable/ic_test_2.xml
@@ -0,0 +1,24 @@
+<!--
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="64dp"
+ android:height="64dp"
+ android:viewportWidth="64.0"
+ android:viewportHeight="64.0">
+ <path
+ android:fillColor="#FF000000"/>
+</vector>
diff --git a/car/app/app/src/main/AndroidManifest.xml b/car/app/app/src/main/AndroidManifest.xml
index a9ecfe8..188169a 100644
--- a/car/app/app/src/main/AndroidManifest.xml
+++ b/car/app/app/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
+
<!--
Copyright 2020 The Android Open Source Project
@@ -14,7 +15,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.car.app">
+<manifest package="androidx.car.app">
</manifest>
\ No newline at end of file
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
index 3639a6f..8383e12 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
@@ -18,9 +18,7 @@
import androidx.car.app.IOnDoneCallback;
-/**
- * @hide
- */
+/** @hide */
oneway interface IOnCheckedChangeListener {
void onCheckedChange(boolean isChecked, IOnDoneCallback callback) = 1;
}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
index 8ca1db6..ff43f3f 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
@@ -18,9 +18,7 @@
import androidx.car.app.IOnDoneCallback;
-/**
- * @hide
- */
+/** @hide */
oneway interface IOnItemVisibilityChangedListener {
/**
* A callback for when the range of items that are visible in the UI changes.
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
index a483c01..2896a83 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
@@ -18,9 +18,7 @@
import androidx.car.app.IOnDoneCallback;
-/**
- * @hide
- */
+/** @hide */
oneway interface IOnSelectedListener {
void onSelected(int index, IOnDoneCallback callback) = 1;
}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
index 7bf092b..9af81e0 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
@@ -18,9 +18,7 @@
import androidx.car.app.IOnDoneCallback;
-/**
- * @hide
- */
+/** @hide */
oneway interface ISearchListener {
void onSearchTextChanged(String text, IOnDoneCallback callback) = 1;
void onSearchSubmitted(String text, IOnDoneCallback callback) = 2;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl b/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl
index c686a13..14ef334 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl
@@ -18,9 +18,7 @@
import androidx.car.app.IOnDoneCallback;
-/**
- * @hide
- */
+/** @hide */
interface IStartCarApp {
/**
* Starts the car app on the car screen.
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl
index 87482ed..7d9192d 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl
@@ -22,9 +22,7 @@
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.IOnDoneCallback;
-/**
- * @hide
- */
+/** @hide */
oneway interface ISurfaceListener {
/**
* Notifies the app that the surface has changed.
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
new file mode 100644
index 0000000..7308d03
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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 androidx.car.app.IOnDoneCallback;
+
+/** @hide */
+oneway interface IOnClickListener {
+ void onClick(IOnDoneCallback callback) = 1;
+}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationHost.aidl b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationHost.aidl
new file mode 100644
index 0000000..f86e45d
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationHost.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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.navigation;
+
+import androidx.car.app.serialization.Bundleable;
+
+/** @hide */
+interface INavigationHost {
+ /**
+ * Update the host when active navigation in the app has started.
+ */
+ void navigationStarted() = 1;
+
+ /**
+ * Update the host when active navigation in the app has ended.
+ */
+ void navigationEnded() = 2;
+
+ /**
+ * Sends the navigation state to the host which can be rendered at different
+ * places in the car such as the navigation templates, cluster screens, etc.
+ */
+ void updateTrip(in Bundleable trip) = 3;
+}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
new file mode 100644
index 0000000..4e16e7e
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.navigation;
+
+import androidx.car.app.IOnDoneCallback;
+
+/**
+ * @hide
+ */
+oneway interface INavigationManager {
+ /**
+ * Notifies the app that it should stop the active navigation right away.
+ *
+ * <p>The app should stop any audio guidance, routing notifications tagged for
+ * the car, and metadata state updates.
+ */
+ void stopNavigation(IOnDoneCallback callback) = 1;
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/AppManager.java b/car/app/app/src/main/java/androidx/car/app/AppManager.java
index fca68a0..131ab08 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppManager.java
@@ -24,19 +24,15 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.car.app.model.TemplateWrapper;
import androidx.car.app.utils.RemoteUtils;
import androidx.car.app.utils.ThreadUtils;
import java.util.Objects;
-// TODO(rampara): Uncomment on commit of model modules.
-//import androidx.car.app.model.TemplateWrapper;
-
/** Manages the communication between the app and the host. */
public class AppManager {
@NonNull
- @SuppressWarnings("UnusedVariable")
- // TODO(rampara): Remove suppress annotation on commit of model modules.
private final CarContext mCarContext;
@NonNull
private final IAppManager.Stub mAppManager;
@@ -68,9 +64,8 @@
"setSurfaceListener");
}
- // TODO(rampara): Change code tags to link after commit of model module.
/**
- * Requests the current template to be invalidated, which eventually triggers a call to {@code
+ * Requests the current template to be invalidated, which eventually triggers a call to {@link
* Screen#getTemplate} to get the new template to display.
*
* @throws HostException if the remote call fails.
@@ -127,22 +122,21 @@
public void getTemplate(IOnDoneCallback callback) {
ThreadUtils.runOnMain(
() -> {
- // TODO(rampara): Uncomment on commit of model modules.
-// TemplateWrapper templateWrapper;
-// try {
-// templateWrapper =
-// AppManager.this
-// .mCarContext
-// .getCarService(ScreenManager.class)
-// .getTopTemplate();
-// } catch (RuntimeException e) {
-// RemoteUtils.sendFailureResponse(callback,
-// "getTemplate", e);
-// throw new WrappedRuntimeException(e);
-// }
-//
-// RemoteUtils.sendSuccessResponse(callback, "getTemplate",
-// templateWrapper);
+ TemplateWrapper templateWrapper;
+ try {
+ templateWrapper =
+ AppManager.this
+ .mCarContext
+ .getCarService(ScreenManager.class)
+ .getTopTemplate();
+ } catch (RuntimeException e) {
+ RemoteUtils.sendFailureResponse(callback,
+ "getTemplate", e);
+ throw new WrappedRuntimeException(e);
+ }
+
+ RemoteUtils.sendSuccessResponse(callback, "getTemplate",
+ templateWrapper);
});
}
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index a322d00..58a108f 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -30,6 +30,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext.CarServiceType;
+import androidx.car.app.navigation.NavigationManager;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.RemoteUtils;
@@ -44,9 +45,6 @@
import java.io.PrintWriter;
import java.security.InvalidParameterException;
-// TODO(rampara): Uncomment on addition of navigation module
-//import androidx.car.app.navigation.NavigationManager;
-
/**
* The base class for implementing a car app that runs in the car.
*
@@ -107,8 +105,7 @@
mRegistry.handleLifecycleEvent(Event.ON_STOP);
// Stop any active navigation
- // TODO(rampara): Uncomment on addition of navigation module
-// carContext.getCarService(NavigationManager.class).stopNavigation();
+ mCarContext.getCarService(NavigationManager.class).stopNavigation();
// Destroy all screens in the stack
mCarContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
@@ -192,13 +189,12 @@
return mCarContext;
}
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
* Requests the first {@link Screen} for the application.
*
* <p>This method is invoked when this car app is first opened by the user.
*
- * <p>Once the method returns, {@code Screen#getTemplate} will be called on the {@link Screen}
+ * <p>Once the method returns, {@link Screen#getTemplate} will be called on the {@link Screen}
* returned, and the app will be displayed on the car screen.
*
* <p>To pre-seed a back stack, you can push {@link Screen}s onto the stack, via {@link
@@ -218,11 +214,10 @@
@NonNull
public abstract Screen onCreateScreen(@NonNull Intent intent);
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
* Notifies that the car app has received a new {@link Intent}.
*
- * <p>Once the method returns, {@code Screen#getTemplate} will be called on the {@link Screen}
+ * <p>Once the method returns, {@link Screen#getTemplate} will be called on the {@link Screen}
* that is on top of the {@link Screen} stack managed by the {@link ScreenManager}, and the app
* will be displayed on the car screen.
*
@@ -255,7 +250,6 @@
public void onCarConfigurationChanged(@NonNull Configuration newConfiguration) {
}
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
* Returns the {@link CarAppService}'s {@link Lifecycle}.
*
@@ -264,8 +258,7 @@
* <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
- * {@link
- * Screen} receives different {@link Event}s.
+ * {@link Screen} receives different {@link Event}s.
* <li>Use this {@link CarAppService} to observe {@link androidx.lifecycle.LiveData}s that
* may drive the backing data for your application.
* </ul>
@@ -293,7 +286,7 @@
* app is no longer displaying in the car, the host may finish this car app.
* <dt>{@link Event#ON_DESTROY}
* <dd>The OS has now destroyed this {@link CarAppService} instance, and it is no longer
- * valid.
+ * valid.
* </dl>
*
* <p>Listeners that are added in {@link Event#ON_START}, should be removed in {@link
@@ -318,8 +311,7 @@
for (String arg : args) {
if (AUTO_DRIVE.equals(arg)) {
- // TODO(rampara): Uncomment on addition of navigation module
-// runOnMain(carContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
+ runOnMain(mCarContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
}
}
}
@@ -435,11 +427,11 @@
AppManager.class).getIInterface());
return;
case CarContext.NAVIGATION_SERVICE:
- // TODO(rampara): Uncomment on addition of navigation module
-// RemoteUtils.sendSuccessResponse(
-// callback,
-// "getManager",
-// carContext.getCarService(NavigationManager.class).getIInterface());
+ RemoteUtils.sendSuccessResponse(
+ callback,
+ "getManager",
+ mCarContext.getCarService(
+ NavigationManager.class).getIInterface());
return;
default:
Log.e(TAG, type + "%s is not a valid manager");
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index 826614d..8c44fe5 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -40,6 +40,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
+import androidx.car.app.navigation.NavigationManager;
import androidx.car.app.utils.RemoteUtils;
import androidx.car.app.utils.ThreadUtils;
import androidx.lifecycle.Lifecycle;
@@ -49,16 +50,12 @@
import java.lang.annotation.RetentionPolicy;
import java.security.InvalidParameterException;
-// TODO(rampara): Uncomment on addition of navigation module
-//import androidx.car.app.navigation.NavigationManager;
-
-// TODO(rampara): Replace code tags with links on addition of model module.
/**
* The CarContext class is a {@link ContextWrapper} subclass accessible to your {@link
* CarAppService} and {@link Screen} instances, which provides access to car services such as the
* {@link ScreenManager} for managing the screen stack, the {@link AppManager} for general
* app-related functionality such as accessing a surface for drawing your navigation app’s map, and
- * the {@code NavigationManager} used by turn-by-turn navigation apps to communicate navigation
+ * the {@link NavigationManager} used by turn-by-turn navigation apps to communicate navigation
* metadata and other navigation-related events with the host. See Access the navigation templates
* for a comprehensive list of library functionality available to navigation apps.
*
@@ -121,8 +118,7 @@
"androidx.car.app.action.NAVIGATE";
private final AppManager mAppManager;
- // TODO(rampara): Uncomment on addition of navigation module
- // private final NavigationManager navigationManager;
+ private final NavigationManager mNavigationManager;
private final ScreenManager mScreenManager;
private final OnBackPressedDispatcher mOnBackPressedDispatcher;
@@ -135,7 +131,6 @@
return new CarContext(lifecycle, new HostDispatcher());
}
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
* Provides a car service by name.
*
@@ -147,7 +142,7 @@
* <dt>{@link #APP_SERVICE}
* <dd>An {@link AppManager} for communication between the app and the host.
* <dt>{@link #NAVIGATION_SERVICE}
- * <dd>A {@code NavigationManager} for management of navigation updates.
+ * <dd>A {@link NavigationManager} for management of navigation updates.
* <dt>{@link #SCREEN_MANAGER_SERVICE}
* <dd>A {@link ScreenManager} for management of {@link Screen}s.
* </dl>
@@ -165,9 +160,8 @@
switch (requireNonNull(name)) {
case APP_SERVICE:
return mAppManager;
- // TODO(rampara): Uncomment on addition of navigation module
-// case NAVIGATION_SERVICE:
-// return navigationManager;
+ case NAVIGATION_SERVICE:
+ return mNavigationManager;
case SCREEN_MANAGER_SERVICE:
return mScreenManager;
default: // fall out
@@ -177,11 +171,10 @@
"The name '" + name + "' does not correspond to a car service.");
}
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
* Returns the a car service, by class.
*
- * <p>Currently supported classes are: {@link AppManager}, {@code NavigationManager}, {@link
+ * <p>Currently supported classes are: {@link AppManager}, {@link NavigationManager}, {@link
* ScreenManager}.
*
* @param serviceClass the class of the requested service.
@@ -210,9 +203,8 @@
public String getCarServiceName(@NonNull Class<?> serviceClass) {
if (requireNonNull(serviceClass).isInstance(mAppManager)) {
return APP_SERVICE;
- // TODO(rampara): Uncomment on addition of navigation module
-// } else if (serviceClass.isInstance(navigationManager)) {
-// return NAVIGATION_SERVICE;
+ } else if (serviceClass.isInstance(mNavigationManager)) {
+ return NAVIGATION_SERVICE;
} else if (serviceClass.isInstance(mScreenManager)) {
return SCREEN_MANAGER_SERVICE;
}
@@ -450,8 +442,7 @@
this.mHostDispatcher = hostDispatcher;
mAppManager = AppManager.create(this, hostDispatcher);
- // TODO(rampara): Uncomment on addition of navigation module
- //navigationManager = NavigationManager.create(hostDispatcher);
+ mNavigationManager = NavigationManager.create(hostDispatcher);
mScreenManager = ScreenManager.create(this, lifecycle);
mOnBackPressedDispatcher =
new OnBackPressedDispatcher(() -> getCarService(ScreenManager.class).pop());
diff --git a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
index dc17bc4..3453d3b 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
@@ -29,8 +29,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.car.app.CarContext.CarServiceType;
-// TODO(rampara): Uncomment on addition of navigation module
-//import androidx.car.app.navigation.INavigationHost;
+import androidx.car.app.navigation.INavigationHost;
import androidx.car.app.utils.RemoteUtils;
import androidx.car.app.utils.ThreadUtils;
@@ -47,8 +46,8 @@
private ICarHost mCarHost;
@Nullable
private IAppHost mAppHost;
- // TODO(rampara): Uncomment on addition of navigation module
-// @Nullable private INavigationHost navigationHost;
+ @Nullable
+ private INavigationHost mNavigationHost;
/**
* Dispatches the {@code call} to the host for the given {@code hostType}.
@@ -85,8 +84,7 @@
mCarHost = null;
mAppHost = null;
- // TODO(rampara): Uncomment on addition of navigation module
-// navigationHost = null;
+ mNavigationHost = null;
}
/**
@@ -114,17 +112,17 @@
host = mAppHost;
break;
case CarContext.NAVIGATION_SERVICE:
- // TODO(rampara): Uncomment on addition of navigation module
-// if (navigationHost == null) {
-// navigationHost =
-// RemoteUtils.call(
-// () ->
-// INavigationHost.Stub.asInterface(
-// requireNonNull(carHost).getHost(CarContext.NAVIGATION_SERVICE)),
-// "getHost(Navigation)");
-// }
-// host = navigationHost;
-// break;
+ if (mNavigationHost == null) {
+ mNavigationHost =
+ RemoteUtils.call(
+ () ->
+ INavigationHost.Stub.asInterface(
+ requireNonNull(mCarHost).getHost(
+ CarContext.NAVIGATION_SERVICE)),
+ "getHost(Navigation)");
+ }
+ host = mNavigationHost;
+ break;
case CarContext.CAR_SERVICE:
host = mCarHost;
break;
diff --git a/car/app/app/src/main/java/androidx/car/app/Screen.java b/car/app/app/src/main/java/androidx/car/app/Screen.java
index b7be413..d78d413 100644
--- a/car/app/app/src/main/java/androidx/car/app/Screen.java
+++ b/car/app/app/src/main/java/androidx/car/app/Screen.java
@@ -17,12 +17,18 @@
package androidx.car.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.car.app.utils.CommonUtils.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;
@@ -30,16 +36,10 @@
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
-// TODO(rampara): Uncomment on addition of model module
-//import androidx.car.app.model.Template;
-//import androidx.car.app.model.TemplateInfo;
-//import androidx.car.app.model.TemplateWrapper;
-
-// TODO(rampara): Replace code tags with links on addition of model module.
/**
- * A Screen has a {@link Lifecycle} and provides the mechanism for the app to send {@code Template}s
+ * 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 {@code
+ * a Screen stack, which ensures they adhere to the template flow restrictions (see {@link
* #getTemplate} 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
@@ -77,39 +77,32 @@
* A reference to the last template returned by this screen, or {@code null} if one has not been
* returned yet.
*/
- // TODO(rampara): Uncomment on addition of model module
-// @Nullable
-// private TemplateWrapper mTemplateWrapper;
+ @Nullable
+ private TemplateWrapper mTemplateWrapper;
- // TODO(rampara): Uncomment on addition of model module
/**
* Whether to set the ID of the last template in the next template to be returned.
*
-// * @see #getTemplate
+ * @see #getTemplate
*/
- @SuppressWarnings("UnusedVariable")
- // TODO(rampara): Remove suppress annotation on commit of model modules.
private boolean mUseLastTemplateId;
protected Screen(@NonNull CarContext carContext) {
this.mCarContext = requireNonNull(carContext);
}
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
- * Requests the current template to be invalidated, which eventually triggers a call to {@code
+ * Requests the current template to be invalidated, which eventually triggers a call to {@link
* #getTemplate} 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 {@code #getTemplate}.
+ * template is returned by {@link #getTemplate}.
*
- * <p>To avoid race conditions with calls to {@code #getTemplate} you should call this method
- * with
- * the main thread.
+ * <p>To avoid race conditions with calls to {@link #getTemplate} you should call this method
+ * with the main thread.
*
* @throws HostException if the remote call fails.
*/
@@ -170,7 +163,6 @@
return mMarker;
}
- // TODO(rampara): Replace code tags with links on addition of model module.
/**
* Returns this screen's lifecycle.
*
@@ -191,7 +183,7 @@
* <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
- * {@code #getTemplate} at a point after this call.
+ * {@link #getTemplate} at a point after this call.
* <dt>{@link Event#ON_START}
* <dd>The template returned from this screen is visible in the car screen.
* <dt>{@link Event#ON_RESUME}
@@ -230,6 +222,7 @@
return mCarContext.getCarService(ScreenManager.class);
}
+ // TODO(rampara): Replace code tags with link on submission of notification module
/**
* Returns the {@link Template} to present in the car screen.
*
@@ -307,16 +300,15 @@
* 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.
+ * <p>See {@code androidx.car.app.notification.CarAppExtender} for details on notifications.
*/
- // TODO(rampara): Uncomment on addition of model module
-// @NonNull
-// public abstract Template getTemplate();
-//
-// /** Sets a {@link OnScreenResultCallback} for this {@link Screen}. */
-// void setOnResultCallback(OnScreenResultCallback onScreenResultCallback) {
-// this.mOnScreenResultCallback = onScreenResultCallback;
-// }
+ @NonNull
+ public abstract Template getTemplate();
+
+ /** Sets a {@link OnScreenResultCallback} for this {@link Screen}. */
+ void setOnResultCallback(OnScreenResultCallback onScreenResultCallback) {
+ this.mOnScreenResultCallback = onScreenResultCallback;
+ }
/**
* Dispatches lifecycle event for {@code event} on the main thread.
@@ -350,27 +342,26 @@
* that is stamped with the same ID as the last template returned by this screen. This is
* used to identify back (stack pop) operations.
*/
- // TODO(rampara): Uncomment on addition of model module
-// @NonNull
-// TemplateWrapper getTemplateWrapper() {
-// Template template = getTemplate();
-//
-// TemplateWrapper wrapper;
-// if (mUseLastTemplateId) {
-// wrapper =
-// TemplateWrapper.wrap(
-// template, getLastTemplateInfo(
-// requireNonNull(mTemplateWrapper)).getTemplateId());
-// } else {
-// wrapper = TemplateWrapper.wrap(template);
-// }
-// mUseLastTemplateId = false;
-//
-// mTemplateWrapper = wrapper;
-//
-// Log.d(TAG, "Returning " + template + " from screen " + this);
-// return wrapper;
-// }
+ @NonNull
+ TemplateWrapper getTemplateWrapper() {
+ Template template = getTemplate();
+
+ TemplateWrapper wrapper;
+ if (mUseLastTemplateId) {
+ wrapper =
+ TemplateWrapper.wrap(
+ template, getLastTemplateInfo(
+ requireNonNull(mTemplateWrapper)).getTemplateId());
+ } else {
+ wrapper = TemplateWrapper.wrap(template);
+ }
+ mUseLastTemplateId = false;
+
+ mTemplateWrapper = wrapper;
+
+ Log.d(TAG, "Returning " + template + " from screen " + this);
+ return wrapper;
+ }
/**
* Returns the information for the template that was last returned by this screen.
@@ -381,24 +372,22 @@
* dispatched to the top screen, allowing to notify the host of the current stack of template
* ids known to the client.
*/
- // TODO(rampara): Uncomment on addition of model module
-// @NonNull
-// TemplateInfo getLastTemplateInfo() {
-// if (mTemplateWrapper == null) {
-// mTemplateWrapper = TemplateWrapper.wrap(getTemplate());
-// }
-// return new TemplateInfo(mTemplateWrapper.getTemplate(), mTemplateWrapper.getId());
-// }
-//
-// @NonNull
-// private static TemplateInfo getLastTemplateInfo(TemplateWrapper lastTemplateWrapper) {
-// return new TemplateInfo(lastTemplateWrapper.getTemplate(), lastTemplateWrapper.getId());
-// }
+ @NonNull
+ TemplateInfo getLastTemplateInfo() {
+ if (mTemplateWrapper == null) {
+ mTemplateWrapper = TemplateWrapper.wrap(getTemplate());
+ }
+ return new TemplateInfo(mTemplateWrapper.getTemplate(), mTemplateWrapper.getId());
+ }
- // TODO(rampara): Replace code tags with links on addition of model module.
+ @NonNull
+ private static TemplateInfo getLastTemplateInfo(TemplateWrapper lastTemplateWrapper) {
+ return new TemplateInfo(lastTemplateWrapper.getTemplate(), lastTemplateWrapper.getId());
+ }
+
/**
- * Denotes whether the next {@code Template} retrieved via {@code #getTemplate} should reuse the
- * ID of the last {@code Template}.
+ * Denotes whether the next {@link Template} retrieved via {@link #getTemplate} 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
diff --git a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index 3cd433f..56b3365 100644
--- a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
@@ -27,9 +27,8 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-// TODO(rampara): Uncomment on commit of model modules.
-//import androidx.car.app.model.TemplateInfo;
-//import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.TemplateInfo;
+import androidx.car.app.model.TemplateWrapper;
import androidx.car.app.utils.ThreadUtils;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
@@ -92,8 +91,7 @@
@SuppressLint("ExecutorRegistration")
public void pushForResult(
@NonNull Screen screen, @NonNull OnScreenResultCallback onScreenResultCallback) {
- // TODO(rampara): Uncomment on addition of model module
-// requireNonNull(screen).setOnResultCallback(requireNonNull(onScreenResultCallback));
+ requireNonNull(screen).setOnResultCallback(requireNonNull(onScreenResultCallback));
pushInternal(screen);
}
@@ -170,26 +168,24 @@
}
/** Returns the {@link TemplateWrapper} for the {@link Screen} that is on top of the stack. */
-// @NonNull
-// @MainThread
- // TODO(rampara): Uncomment on commit of model modules.
-// TemplateWrapper getTopTemplate() {
-// ThreadUtils.checkMainThread();
-//
-// Screen screen = getTop();
-// Log.d(TAG, "Requesting template from Screen " + screen);
-//
-// TemplateWrapper templateWrapper = screen.getTemplateWrapper();
-//
-// List<TemplateInfo> templateInfoList = new ArrayList<>();
-// for (Screen s : mScreenStack) {
-// templateInfoList.add(s.getLastTemplateInfo());
-// }
-//
-// templateWrapper.setTemplateInfosForScreenStack(templateInfoList);
-// return templateWrapper;
-// return templateWrapper;
-// }
+ @NonNull
+ @MainThread
+ TemplateWrapper getTopTemplate() {
+ ThreadUtils.checkMainThread();
+
+ Screen screen = getTop();
+ Log.d(TAG, "Requesting template from Screen " + screen);
+
+ TemplateWrapper templateWrapper = screen.getTemplateWrapper();
+
+ List<TemplateInfo> templateInfoList = new ArrayList<>();
+ for (Screen s : mScreenStack) {
+ templateInfoList.add(s.getLastTemplateInfo());
+ }
+
+ templateWrapper.setTemplateInfosForScreenStack(templateInfoList);
+ return templateWrapper;
+ }
void destroyAndClearScreenStack() {
for (Screen screen : mScreenStack) {
diff --git a/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java b/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java
index cf38d0b..0a7307a 100644
--- a/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java
+++ b/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java
@@ -22,7 +22,6 @@
* A wrapper to allow rethrowing any {@link RuntimeException} that the car app threw, after
* notifying the host of them.
*/
-// Developers can catch this exception so keeping it for stack traces.
public class WrappedRuntimeException extends RuntimeException {
public WrappedRuntimeException(@Nullable Throwable cause) {
super(cause);
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
new file mode 100644
index 0000000..0d21904
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -0,0 +1,417 @@
+/*
+ * 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 androidx.car.app.model.CarColor.DEFAULT;
+import static androidx.car.app.model.constraints.CarColorConstraints.STANDARD_ONLY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.text.TextUtils;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.activity.OnBackPressedDispatcher;
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarContext;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents an action with an optional icon and text.
+ *
+ * <p>Actions may be displayed differently depending on the template or model they are added to. For
+ * example, the host may decide to display an action as a floating action button (FAB) when
+ * displayed over a map, as a button when displayed in a {@link Pane}, or as a simple icon with no
+ * title when displayed within a {@link Row}.
+ *
+ * <h4>Standard actions</h4>
+ *
+ * A set of standard, built-in {@link Action} instances is available with a few of the common basic
+ * actions car apps may need (for example a {@link #BACK} action).
+ *
+ * <p>With the exception of {@link #APP_ICON} and {@link #BACK}, an app can provide a custom title
+ * and icon for the action. However, depending on the template the action belongs to, the title or
+ * icon may be disallowed. If such restrictions apply, the documentation of the APIs that consume
+ * the action will note them accordingly.
+ */
+public final class Action {
+ /**
+ * The type of action represented by the {@link Action } instance.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @RestrictTo(LIBRARY)
+ @IntDef(
+ value = {
+ TYPE_UNKNOWN,
+ TYPE_CUSTOM,
+ TYPE_APP_ICON,
+ TYPE_BACK,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActionType {
+ }
+
+ static final int FLAG_STANDARD = 1 << 16;
+
+ /**
+ * An unknown action type.
+ */
+ public static final int TYPE_UNKNOWN = 0;
+
+ /**
+ * An app-defined custom action type.
+ */
+ public static final int TYPE_CUSTOM = 1;
+
+ /**
+ * An action representing an app's icon.
+ *
+ * @see #APP_ICON
+ */
+ public static final int TYPE_APP_ICON = 2 | FLAG_STANDARD;
+
+ /**
+ * An action to navigate back in the user interface.
+ *
+ * @see #BACK
+ */
+ public static final int TYPE_BACK = 3 | FLAG_STANDARD;
+
+ /**
+ * A standard action to show the app's icon.
+ *
+ * <p>This action is non-interactive.
+ */
+ @NonNull
+ public static final Action APP_ICON = new Action(TYPE_APP_ICON);
+
+ /**
+ * A standard action to navigate back in the user interface.
+ *
+ * <p>The default behavior for a back press will call
+ * {@link androidx.car.app.ScreenManager#pop}.
+ *
+ * <p>To override the default behavior, register a {@link OnBackPressedCallback} via
+ * {@link OnBackPressedDispatcher#addCallback(LifecycleOwner, OnBackPressedCallback)}, which
+ * you can retrieve from {@link CarContext#getOnBackPressedDispatcher()}.
+ */
+ @NonNull
+ public static final Action BACK = new Action(TYPE_BACK);
+
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final CarIcon mIcon;
+ @Keep
+ private final CarColor mBackgroundColor;
+ @Keep
+ @Nullable
+ private final OnClickListenerWrapper mListener;
+ @Keep
+ @ActionType
+ private final int mType;
+
+ /** Constructs a new builder of {@link Action}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns a {@link Builder} instance configured with the same data as this {@link Action}
+ * instance.
+ */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Returns the title displayed in the action, or {@code null} if the action does not have a
+ * title.
+ */
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the {@link CarIcon} to displayed in the action, or {@code null} if the action does
+ * not
+ * have an icon.
+ */
+ @Nullable
+ public CarIcon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Returns the {@link CarColor} used for the background color of the action.
+ */
+ @NonNull
+ public CarColor getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+
+ @ActionType
+ public int getType() {
+ return mType;
+ }
+
+
+ public boolean isStandard() {
+ return isStandardActionType(mType);
+ }
+
+
+ @Nullable
+ public OnClickListenerWrapper getOnClickListener() {
+ return mListener;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[type: " + typeToString(mType) + ", icon: " + mIcon + ", bkg: " + mBackgroundColor
+ + "]";
+ }
+
+ /**
+ * Converts the given {@code type} into a string representation.
+ */
+ @NonNull
+ public static String typeToString(@ActionType int type) {
+ switch (type) {
+ case TYPE_CUSTOM:
+ return "CUSTOM";
+ case TYPE_APP_ICON:
+ return "APP_ICON";
+ case TYPE_BACK:
+ return "BACK";
+ default:
+ return "<unknown>";
+ }
+ }
+
+ /** Convenience constructor for standard action singletons. */
+ private Action(@ActionType int type) {
+ if (!isStandardActionType(type)) {
+ throw new IllegalArgumentException(
+ "Standard action constructor used with non standard type");
+ }
+
+ mTitle = null;
+ mIcon = null;
+ mBackgroundColor = DEFAULT;
+
+ // The listeners can be set, for actions that support it, by copying the standard action
+ // instance with the newBuilder and setting it.
+ mListener = null;
+ this.mType = type;
+ }
+
+ private Action(
+ @Nullable CarText title,
+ @Nullable CarIcon icon,
+ CarColor backgroundColor,
+ @Nullable OnClickListenerWrapper listener,
+ @ActionType int type) {
+ this.mTitle = title;
+ this.mIcon = icon;
+ this.mBackgroundColor = backgroundColor;
+ this.mListener = listener;
+ this.mType = type;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Action() {
+ mTitle = null;
+ mIcon = null;
+ mBackgroundColor = DEFAULT;
+ mListener = null;
+ mType = TYPE_UNKNOWN;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mType, mListener == null, mIcon == null);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Action)) {
+ return false;
+ }
+ Action otherAction = (Action) other;
+
+ // Don't compare callback, only ensure if it is present in one, it is also present in
+ // the other.
+ return Objects.equals(mTitle, otherAction.mTitle)
+ && mType == otherAction.mType
+ && Objects.equals(mIcon, otherAction.mIcon)
+ && Objects.equals(mListener == null, otherAction.mListener == null);
+ }
+
+ private static boolean isStandardActionType(@ActionType int type) {
+ return 0 != (type & FLAG_STANDARD);
+ }
+
+ /** A builder of {@link Action}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ @Nullable
+ private CarIcon mIcon;
+ @Nullable
+ private OnClickListenerWrapper mListener;
+ private CarColor mBackgroundColor = DEFAULT;
+ @ActionType
+ private int mType = TYPE_CUSTOM;
+
+ /**
+ * Sets the title to display in the action, or {@code null} to not display a title.
+ *
+ * <p>The title of a standard action can be set with this method. Actions, including
+ * standard
+ * actions, don't have a title by default.
+ */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets the icon to display in the action, or {@code null} to not display an icon.
+ *
+ * <p>Icons can't be set in standard actions.
+ *
+ * <h4>Icon Sizing Guidance</h4>
+ *
+ * The provided icon should have a maximum size of 36 x 36 dp. If the icon exceeds this
+ * maximum
+ * size in either one of the dimensions, it will be scaled down to be centered inside the
+ * bounding box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that
+ * work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setIcon(@Nullable CarIcon icon) {
+ CarIconConstraints.DEFAULT.validateOrThrow(icon);
+ this.mIcon = icon;
+ return this;
+ }
+
+ /** Sets the {@link OnClickListener} to call when the action is clicked. */
+ @NonNull
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public Builder setOnClickListener(@Nullable OnClickListener listener) {
+ this.mListener = listener == null ? null : OnClickListenerWrapper.create(listener);
+ return this;
+ }
+
+ /**
+ * Sets the background color to be used for the action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * <p>The host may ignore this color and use the default instead if the color does not
+ * pass the
+ * contrast requirements.
+ *
+ * @param backgroundColor the {@link CarColor} to set as background. Use {@link
+ * CarColor#DEFAULT} to let the host pick a default.
+ * @throws IllegalArgumentException if {@code backgroundColor} is not a standard color.
+ * @throws NullPointerException if {@code backgroundColor} is {@code null}.
+ */
+ @NonNull
+ public Builder setBackgroundColor(@NonNull CarColor backgroundColor) {
+ STANDARD_ONLY.validateOrThrow(requireNonNull(backgroundColor));
+ this.mBackgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link Action} defined by this builder.
+ *
+ * @throws IllegalStateException if the action is not a standard action and does not have an
+ * icon or a title.
+ * @throws IllegalStateException if a listener is set on either {@link #APP_ICON} or {@link
+ * #BACK}.
+ * @throws IllegalStateException if an icon or title is set on either {@link #APP_ICON} or
+ * {@link #BACK}.
+ */
+ @NonNull
+ public Action build() {
+ if (mType == TYPE_UNKNOWN) {
+ throw new IllegalStateException("Missing action type");
+ }
+ boolean isStandard = isStandardActionType(mType);
+ if (!isStandard && mIcon == null && (mTitle == null || TextUtils.isEmpty(
+ mTitle.getText()))) {
+ throw new IllegalStateException("An action must have either an icon or a title");
+ }
+
+ if ((mType == TYPE_APP_ICON || mType == TYPE_BACK)) {
+ if (mListener != null) {
+ throw new IllegalStateException(
+ "An on-click listener can't be set on the standard back or app-icon "
+ + "action");
+ }
+
+ if (mIcon != null || (mTitle != null && !TextUtils.isEmpty(mTitle.getText()))) {
+ throw new IllegalStateException(
+ "An icon or title can't be set on the standard back or app-icon "
+ + "action");
+ }
+ }
+
+ return new Action(mTitle, mIcon, mBackgroundColor, mListener, mType);
+ }
+
+ private Builder() {
+ }
+
+ private Builder(Action action) {
+ mTitle = action.mTitle;
+ mIcon = action.mIcon;
+ mBackgroundColor = action.mBackgroundColor;
+ mListener = action.mListener;
+ mType = action.mType;
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionList.java b/car/app/app/src/main/java/androidx/car/app/model/ActionList.java
new file mode 100644
index 0000000..99f5bdf
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionList.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a simple list of {@link Action} models.
+ *
+ * <p>This model is intended for internal and host use only, as a transport artifact for
+ * homogeneous lists of {@link Action} items.
+ */
+public class ActionList {
+ private final List<Action> mList;
+
+ /**
+ * Returns the list of {@link Action}'s.
+ */
+ @NonNull
+ public List<Action> getList() {
+ return mList;
+ }
+
+ /**
+ * Creates an {@link ActionList} instance based on the list of {@link Action}'s.
+ */
+ @NonNull
+ public static ActionList create(@NonNull List<Action> list) {
+ requireNonNull(list);
+ for (Action action : list) {
+ if (action == null) {
+ throw new IllegalArgumentException("Disallowed null action found in action list");
+ }
+ }
+ return new ActionList(list);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return mList.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mList);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ActionList)) {
+ return false;
+ }
+ ActionList otherActionList = (ActionList) other;
+
+ return Objects.equals(mList, otherActionList.mList);
+ }
+
+ private ActionList(List<Action> list) {
+ this.mList = new ArrayList<>(list);
+ }
+
+ /** For serialization. */
+ private ActionList() {
+ mList = Collections.emptyList();
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
new file mode 100644
index 0000000..19ee2d5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
@@ -0,0 +1,160 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action.ActionType;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a list of {@link Action}s that are used for a template.
+ *
+ * <p>The {@link Action}s in the {@link ActionStrip} may be displayed differently depending on the
+ * template they are used with. For example, a map template may display them as a group of floating
+ * action buttons (FABs) over the map background.
+ *
+ * <p>See the documentation of individual {@link Template}s on restrictions around what actions are
+ * supported.
+ */
+public class ActionStrip {
+ @Keep
+ private final List<Object> mActions;
+
+ /** Constructs a new builder of {@link ActionStrip}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the list of {@link Action}'s.
+ */
+ @NonNull
+ public List<Object> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Returns the {@link Action} associated with the input {@code actionType}, or {@code null} if
+ * no matching {@link Action} is found.
+ */
+ @Nullable
+ public Action getActionOfType(@ActionType int actionType) {
+ for (Object object : mActions) {
+ if (object instanceof Action) {
+ Action action = (Action) object;
+ if (action.getType() == actionType) {
+ return action;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[action count: " + mActions.size() + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mActions);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ActionStrip)) {
+ return false;
+ }
+ ActionStrip otherActionStrip = (ActionStrip) other;
+
+ return Objects.equals(mActions, otherActionStrip.mActions);
+ }
+
+ private ActionStrip(Builder builder) {
+ mActions = builder.mActions;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private ActionStrip() {
+ mActions = Collections.emptyList();
+ }
+
+ /** A builder of {@link ActionStrip}. */
+ public static final class Builder {
+ private final List<Object> mActions = new ArrayList<>();
+ private final Set<Integer> mAddedActionTypes = new HashSet<>();
+
+ /**
+ * Adds an {@link Action} to the list.
+ *
+ * @throws IllegalArgumentException if {@code action} is a standard action and an action of
+ * the same type has already been added.
+ * @throws NullPointerException if {@code action} is {@code null}.
+ */
+ @NonNull
+ public Builder addAction(@NonNull Action action) {
+ int actionType = requireNonNull(action).getType();
+ if (actionType != Action.TYPE_CUSTOM && mAddedActionTypes.contains(actionType)) {
+ throw new IllegalArgumentException(
+ "Duplicated action types are disallowed: " + action);
+ }
+ mAddedActionTypes.add(actionType);
+ mActions.add(action);
+ return this;
+ }
+
+ /**
+ * Clears any actions that may have been added with {@link #addAction(Action)} up to this
+ * point.
+ */
+ @NonNull
+ public Builder clearActions() {
+ mActions.clear();
+ mAddedActionTypes.clear();
+ return this;
+ }
+
+ /**
+ * Constructs the {@link ActionStrip} defined by this builder.
+ *
+ * @throws IllegalStateException if the action strip is empty.
+ */
+ @NonNull
+ public ActionStrip build() {
+ if (mActions.isEmpty()) {
+ throw new IllegalStateException("Action strip must contain at least one action");
+ }
+ return new ActionStrip(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarColor.java b/car/app/app/src/main/java/androidx/car/app/model/CarColor.java
new file mode 100644
index 0000000..24020e2
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarColor.java
@@ -0,0 +1,323 @@
+/*
+ * 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 androidx.annotation.ColorInt;
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+// TODO(shiufai): Add link to color guidelines.
+
+/**
+ * Represents a color to be used in a car app.
+ *
+ * <p>The host chooses the dark or light variant of the color when displaying the user interface,
+ * depending where the color is used, to ensure the proper contrast ratio is maintained. For
+ * example, the dark variant when may be used as the background of a view with brighter text on it,
+ * and the light variant for text on a dark background.
+ *
+ * <p>Colors provided by the app should meet the contrast requirements defined by the host, and
+ * documented by the app quality guidelines.
+ *
+ * <h4>Standard colors</h4>
+ *
+ * A set of standard {@link CarColor} instances (for example, {@link #BLUE}) is available in this
+ * class. It is recommended to use these standard colors whenever possible as they are guaranteed to
+ * adhere to the contrast requirements.
+ *
+ * <h4>Primary and secondary colors</h4>
+ *
+ * The app can define two additional {@link CarColor}s in its manifest metadata, through the <code>
+ * carColorPrimary</code>, <code>carColorPrimaryDark</code>, <code>
+ * carColorSecondary</code>, and <code>carColorSecondaryDark</code> theme attributes, by declaring
+ * them in a theme and referencing the theme from the <code>
+ * androidx.car.app.theme</code> metadata. Both the light and dark variants must
+ * be declared for the primary and secondary colors, otherwise default variants will be used.
+ * Wherever primary and secondary colors are used by the app, the host may use a default color
+ * instead if the colors do not pass the contrast requirements.
+ *
+ * <p>In <code>AndroidManifest.xml</code>, under the <code>application</code> element corresponding
+ * to the car app:
+ *
+ * <pre>{@code
+ * <meta-data
+ * android:name="androidx.car.app.theme"
+ * android:resource="@style/CarAppTheme"/>
+ * }</pre>
+ *
+ * The <code>CarAppTheme</code> style is defined as any other themes in a resource file:
+ *
+ * <pre>{@code
+ * <resources>
+ * <style name="CarAppTheme">
+ * <item name="carColorPrimary">@color/my_primary_car_color</item>
+ * <item name="carColorPrimaryDark">@color/my_primary_dark_car_color</item>
+ * <item name="carColorSecondary">@color/my_secondary_car_color</item>
+ * <item name="carColorSecondaryDark">@color/my_secondary_cark_car_color</item>
+ * </style>
+ * </resources>
+ * }</pre>
+ *
+ * <h4>Custom Colors</h4>
+ *
+ * Besides the primary and secondary colors, custom colors can be created at runtime with {@link
+ * #createCustom}. Wherever custom colors are used by the app, the host may use a default color
+ * instead if the custom color does not pass the contrast requirements.
+ */
+public class CarColor {
+ /**
+ * The type of color represented by the {@link CarColor} instance.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @IntDef(
+ value = {
+ TYPE_CUSTOM,
+ TYPE_DEFAULT,
+ TYPE_PRIMARY,
+ TYPE_SECONDARY,
+ TYPE_RED,
+ TYPE_GREEN,
+ TYPE_BLUE,
+ TYPE_YELLOW
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY)
+ public @interface CarColorType {
+ }
+
+ /**
+ * A custom, non-standard, app-defined color.
+ */
+ @CarColorType
+ public static final int TYPE_CUSTOM = 0;
+
+ /**
+ * A default color, chosen by the host.
+ *
+ * @see #DEFAULT
+ */
+ @CarColorType
+ public static final int TYPE_DEFAULT = 1;
+
+ /**
+ * The primary app color.
+ *
+ * @see #PRIMARY
+ */
+ @CarColorType
+ public static final int TYPE_PRIMARY = 2;
+
+ /**
+ * The secondary app color.
+ *
+ * @see #SECONDARY
+ */
+ @CarColorType
+ public static final int TYPE_SECONDARY = 3;
+
+ /**
+ * The standard red color.
+ *
+ * @see #RED
+ */
+ @CarColorType
+ public static final int TYPE_RED = 4;
+
+ /**
+ * The standard green color.
+ *
+ * @see #GREEN
+ */
+ @CarColorType
+ public static final int TYPE_GREEN = 5;
+
+ /**
+ * The standard blue color.
+ *
+ * @see #BLUE
+ */
+ @CarColorType
+ public static final int TYPE_BLUE = 6;
+
+ /**
+ * The standard yellow color.
+ *
+ * @see #YELLOW
+ */
+ @CarColorType
+ public static final int TYPE_YELLOW = 7;
+
+ /**
+ * Indicates that a default color should be used.
+ *
+ * <p>This can be used for example to tell the host that the app has no preference for the
+ * tint of
+ * an icon, and it should use whatever default it finds appropriate.
+ */
+ @NonNull
+ public static final CarColor DEFAULT = create(TYPE_DEFAULT);
+
+ /**
+ * Indicates that the app primary color and its dark version should be used, as declared in the
+ * app manifest through the <code>carColorPrimary</code> and <code>carColorPrimaryDark</code>
+ * theme attributes.
+ */
+ @NonNull
+ public static final CarColor PRIMARY = create(TYPE_PRIMARY);
+
+ /**
+ * Indicates that the app secondary color and its dark version should be used, as declared in
+ * the
+ * app manifest through the <code>carColorSecondary</code> and
+ * <code>carColorSecondaryDark</code>
+ * theme attributes.
+ */
+ @NonNull
+ public static final CarColor SECONDARY = create(TYPE_SECONDARY);
+
+ /** A standard red color. */
+ @NonNull
+ public static final CarColor RED = create(TYPE_RED);
+
+ /** A standard green color. */
+ @NonNull
+ public static final CarColor GREEN = create(TYPE_GREEN);
+
+ /** A standard blue color. */
+ @NonNull
+ public static final CarColor BLUE = create(TYPE_BLUE);
+
+ /** A standard yellow color. */
+ @NonNull
+ public static final CarColor YELLOW = create(TYPE_YELLOW);
+
+ @Keep
+ @CarColorType
+ private final int mType;
+
+ /** A light-variant custom color-int, used when the type is {@link #TYPE_CUSTOM}. */
+ @Keep
+ @ColorInt
+ private final int mColor;
+
+ /** A dark-variant custom color-int, used when the type is {@link #TYPE_CUSTOM}. */
+ @Keep
+ @ColorInt
+ private final int mColorDark;
+
+ /**
+ * Returns an instance of {@link CarColor} containing a non-standard color.
+ *
+ * <p>See the top-level documentation of {@link CarColor} for details about how the host
+ * determines which variant is used.
+ */
+ @NonNull
+ public static CarColor createCustom(@ColorInt int color, @ColorInt int colorDark) {
+ return new CarColor(TYPE_CUSTOM, color, colorDark);
+ }
+
+ @CarColorType
+ public int getType() {
+ return mType;
+ }
+
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ @ColorInt
+ public int getColorDark() {
+ return mColorDark;
+ }
+
+ @Override
+ public String toString() {
+ return "[type: " + typeToString(mType) + ", color: " + mColor + ", dark: " + mColorDark
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mColor, mColorDark);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CarColor)) {
+ return false;
+ }
+ CarColor otherColor = (CarColor) other;
+
+ return mColor == otherColor.mColor
+ && mColorDark == otherColor.mColorDark
+ && mType == otherColor.mType;
+ }
+
+ private static CarColor create(@CarColorType int type) {
+ return new CarColor(type, 0, 0);
+ }
+
+ private static String typeToString(@CarColorType int type) {
+ switch (type) {
+ case TYPE_BLUE:
+ return "BLUE";
+ case TYPE_DEFAULT:
+ return "DEFAULT";
+ case TYPE_PRIMARY:
+ return "PRIMARY";
+ case TYPE_SECONDARY:
+ return "SECONDARY";
+ case TYPE_CUSTOM:
+ return "CUSTOM";
+ case TYPE_GREEN:
+ return "GREEN";
+ case TYPE_RED:
+ return "RED";
+ case TYPE_YELLOW:
+ return "YELLOW";
+ default:
+ return "<unknown>";
+ }
+ }
+
+ private CarColor() {
+ mType = TYPE_DEFAULT;
+ mColor = 0;
+ mColorDark = 0;
+ }
+
+ private CarColor(@CarColorType int type, @ColorInt int color, @ColorInt int colorDark) {
+ this.mType = type;
+ this.mColor = color;
+ this.mColorDark = colorDark;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
new file mode 100644
index 0000000..ae04556
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
@@ -0,0 +1,441 @@
+/*
+ * 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 androidx.car.app.model.CarColor.DEFAULT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.ContentResolver;
+import android.graphics.PorterDuff.Mode;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarColorConstraints;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.core.graphics.drawable.IconCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents an icon to be used in a car app.
+ *
+ * <p>Car icons wrap a backing {@link IconCompat}, and add additional attributes optimized for the
+ * car such as a {@link CarColor} tint.
+ *
+ * <h4>Car Screen Pixel Densities</h4>
+ *
+ * <p>Similar to Android devices, car screens cover a wide range of pixel densities. To ensure that
+ * icons and images render well across all car screens, use vector assets whenever possible to avoid
+ * scaling issues.
+ *
+ * <p>In order to support all car screen sizes and pixel density, you can use configuration
+ * qualifiers in your resource files (e.g. "mdpi", "hdpi", etc). See
+ * {@link androidx.car.app.CarContext} for more details.
+ *
+ * <h4>Themed Drawables</h4>
+ *
+ * Vector drawables can contain references to attributes declared in a theme. For example:
+ *
+ * <pre>{@code
+ * <vector ...
+ * <path
+ * android:pathData="..."
+ * android:fillColor="?myIconColor"/>
+ * </vector>
+ * }</pre>
+ *
+ * The theme must be defined in the app's manifest metadata, by declaring them in a theme and
+ * referencing it from the <code>androidx.car.app.theme</code> metadata.
+ *
+ * <p>In <code>AndroidManifest.xml</code>, under the <code>application</code> element corresponding
+ * to the car app:
+ *
+ * <pre>{@code
+ * <meta-data
+ * android:name="androidx.car.app.theme"
+ * android:resource="@style/CarAppTheme"/>
+ * }</pre>
+ *
+ * The <code>CarAppTheme</code> style is defined as any other themes in a resource file:
+ *
+ * <pre>{@code
+ * <resources>
+ * <style name="CarAppTheme">
+ * <item name="myIconColor">@color/my_icon_color</item>
+ * ...
+ * </style>
+ * </resources>
+ * }</pre>
+ */
+public class CarIcon {
+ /** Matches with {@link android.graphics.drawable.Icon#TYPE_RESOURCE} */
+ private static final int TYPE_RESOURCE = 2;
+
+ /** Matches with {@link android.graphics.drawable.Icon#TYPE_URI} */
+ private static final int TYPE_URI = 4;
+
+ /**
+ * The type of car icon represented by the {@link CarIcon} instance.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @RestrictTo(LIBRARY)
+ @IntDef(
+ value = {
+ TYPE_CUSTOM,
+ TYPE_BACK,
+ TYPE_ALERT,
+ TYPE_APP,
+ TYPE_ERROR,
+ TYPE_WILLIAM_ALERT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CarIconType {
+ }
+
+ /**
+ * An unknown icon type.
+ */
+ public static final int TYPE_UNKNOWN = 0;
+
+ /**
+ * A custom, non-standard, app-defined icon.
+ */
+ public static final int TYPE_CUSTOM = 1;
+
+ /**
+ * An icon representing a "back" action.
+ *
+ * @see #BACK
+ */
+ public static final int TYPE_BACK = 3;
+
+ /**
+ * An alert icon.
+ *
+ * @see #ALERT
+ */
+ public static final int TYPE_ALERT = 4;
+
+ /**
+ * The app's icon.
+ *
+ * @see #APP_ICON
+ */
+ public static final int TYPE_APP = 5;
+
+ /**
+ * An error icon.
+ *
+ * @see #ERROR
+ */
+ public static final int TYPE_ERROR = 6;
+
+ /**
+ * An alerting William.
+ *
+ * @see #WILLIAM_ALERT
+ */
+ public static final int TYPE_WILLIAM_ALERT = 7;
+
+ /**
+ * Represents the app's icon, as defined in the app's manifest by the {@code android:icon}
+ * attribute of the {@code application} element.
+ */
+ @NonNull
+ public static final CarIcon APP_ICON = CarIcon.forStandardType(TYPE_APP);
+
+ @NonNull
+ public static final CarIcon BACK = CarIcon.forStandardType(TYPE_BACK);
+
+ @NonNull
+ public static final CarIcon ALERT = CarIcon.forStandardType(TYPE_ALERT);
+
+ @NonNull
+ public static final CarIcon ERROR = CarIcon.forStandardType(TYPE_ERROR);
+
+ @NonNull
+ public static final CarIcon WILLIAM_ALERT =
+ CarIcon.forStandardType(TYPE_WILLIAM_ALERT, /* tint= */ null);
+
+ @Keep
+ @CarIconType
+ private final int mType;
+ @Keep
+ @Nullable
+ private final IconCompat mIcon;
+ @Keep
+ @Nullable
+ private final CarColor mTint;
+
+ @Nullable
+ public IconCompat getIcon() {
+ return mIcon;
+ }
+
+ @Nullable
+ public CarColor getTint() {
+ return mTint;
+ }
+
+ @CarIconType
+ public int getType() {
+ return mType;
+ }
+
+ @Override
+ public String toString() {
+ return "[type: " + typeToString(mType) + ", tint: " + mTint + "]";
+ }
+
+ /**
+ * Returns a {@link Builder} instance configured with the same data as this {@link CarIcon}
+ * instance.
+ */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Returns a {@link Builder} instance using the given {@link IconCompat}.
+ *
+ * <p>The following types are supported:
+ *
+ * <ul>
+ * <li>{@link IconCompat#TYPE_BITMAP}
+ * <li>{@link IconCompat#TYPE_RESOURCE}
+ * <li>{@link IconCompat#TYPE_URI}
+ * </ul>
+ *
+ * <p>{@link IconCompat#TYPE_URI} is only supported in templates that explicitly allow it. In
+ * those cases, the appropriate APIs will be documented to indicate this.
+ *
+ * <p>For {@link IconCompat#TYPE_URI}, the URI's scheme must be {@link
+ * ContentResolver#SCHEME_CONTENT}.
+ *
+ * <p>If the icon image is loaded from URI, it may be cached on the host side. Changing the
+ * contents of the URI will result in the host showing a stale image.
+ *
+ * @throws IllegalArgumentException if {@code icon}'s URI scheme is not supported.
+ * @throws NullPointerException if {@code icon} is {@code null}.
+ */
+ @NonNull
+ public static Builder builder(@NonNull IconCompat icon) {
+ return new Builder(
+ CarIconConstraints.UNCONSTRAINED.checkSupportedIcon(requireNonNull(icon)));
+ }
+
+ /**
+ * Returns a {@link CarIcon} instance wrapping the given {@link IconCompat}.
+ *
+ * @throws IllegalArgumentException if {@code icon}'s type is not supported.
+ * @throws NullPointerException if {@code icon} is {@code null}.
+ * @see #builder(IconCompat)
+ */
+ @NonNull
+ public static CarIcon of(@NonNull IconCompat icon) {
+ return builder(requireNonNull(icon)).setTint(null).build();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mTint, iconCompatHash());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CarIcon)) {
+ return false;
+ }
+ CarIcon otherIcon = (CarIcon) other;
+
+ return mType == otherIcon.mType
+ && Objects.equals(mTint, otherIcon.mTint)
+ && iconCompatEquals(otherIcon.mIcon);
+ }
+
+ @Nullable
+ private Object iconCompatHash() {
+ // Use the same things being compared in iconCompatEquals for hashing.
+ if (mIcon == null) {
+ return null;
+ }
+
+ int type = mIcon.getType();
+ if (type == TYPE_RESOURCE) {
+ return mIcon.getResPackage() + mIcon.getResId();
+ } else if (type == TYPE_URI) {
+ return mIcon.getUri();
+ }
+
+ return VERSION.SDK_INT >= VERSION_CODES.M;
+ }
+
+ private boolean iconCompatEquals(@Nullable IconCompat other) {
+ if (mIcon == null) {
+ return other == null;
+ } else if (other == null) {
+ return false;
+ }
+
+ int type = mIcon.getType();
+ int otherType = other.getType();
+
+ if (type != otherType) {
+ return false;
+ }
+
+ // TODO(shiufai): Decide how/if we will diff bitmap type IconCompat
+ if (type == TYPE_RESOURCE) {
+ return Objects.equals(mIcon.getResPackage(), other.getResPackage())
+ && mIcon.getResId() == other.getResId();
+ } else if (type == TYPE_URI) {
+ return Objects.equals(mIcon.getUri(), other.getUri());
+ }
+
+ // Before Android version M, we support a subset of image types (resource or uri), so we
+ // compare the instances' resource info or uri to check for equality. For M or above,
+ // since we support any icon types, we only check for type equality if the type is
+ // neither a resource or uri.
+ return VERSION.SDK_INT >= VERSION_CODES.M;
+ }
+
+ private static CarIcon forStandardType(@CarIconType int type) {
+ return forStandardType(type, DEFAULT);
+ }
+
+ private static CarIcon forStandardType(@CarIconType int type, @Nullable CarColor tint) {
+ return new CarIcon(null, tint, type);
+ }
+
+ private static String typeToString(@CarIconType int type) {
+ switch (type) {
+ case TYPE_ALERT:
+ return "ALERT";
+ case TYPE_APP:
+ return "APP";
+ case TYPE_ERROR:
+ return "ERROR";
+ case TYPE_WILLIAM_ALERT:
+ return "WILLIAM_ALERT";
+ case TYPE_BACK:
+ return "BACK";
+ case TYPE_CUSTOM:
+ return "CUSTOM";
+ default:
+ return "<unknown>";
+ }
+ }
+
+ private CarIcon(@Nullable IconCompat icon, @Nullable CarColor tint, @CarIconType int type) {
+ this.mType = type;
+ this.mIcon = icon;
+ this.mTint = tint;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private CarIcon() {
+ this.mType = TYPE_UNKNOWN;
+ this.mIcon = null;
+ this.mTint = null;
+ }
+
+ /** A builder of {@link CarIcon}. */
+ public static final class Builder {
+ @Nullable
+ private IconCompat mIcon;
+ @Nullable
+ private CarColor mTint;
+ @CarIconType
+ private int mType;
+
+ /**
+ * Configures the builder with the same icon and tint as the given {@link CarIcon}.
+ *
+ * @throws NullPointerException if {@code carIcon} is {@code null}.
+ */
+ @NonNull
+ public Builder setIcon(@NonNull CarIcon carIcon) {
+ requireNonNull(carIcon);
+ mIcon = carIcon.getIcon();
+ mTint = carIcon.getTint();
+ mType = carIcon.getType();
+ return this;
+ }
+
+ /**
+ * Sets the tint of the icon to the given {@link CarColor}.
+ *
+ *
+ * <p>This tint overrides the tint set through {@link IconCompat#setTint(int)} in the
+ * backing
+ * {@link IconCompat} with a {@link CarColor} tint.The tint set through {@link
+ * IconCompat#setTint(int)} is not guaranteed to be applied if the {@link CarIcon} tint
+ * is not
+ * set.
+ *
+ * <p>The tint mode used to blend this color is {@link Mode#SRC_IN}.
+ *
+ * <p>If set to {@code null}, then no tint will be applied to the icon.
+ *
+ * <p>By default, no tint is set unless one is specified with this method.
+ *
+ * @see CarColor
+ * @see android.graphics.drawable.Drawable#setTintMode(Mode)
+ */
+ @NonNull
+ public Builder setTint(@Nullable CarColor tint) {
+ if (tint != null) {
+ CarColorConstraints.UNCONSTRAINED.validateOrThrow(tint);
+ }
+ this.mTint = tint;
+ return this;
+ }
+
+ /** Constructs the {@link CarIcon} defined by this builder. */
+ @NonNull
+ public CarIcon build() {
+ return new CarIcon(mIcon, mTint, mType);
+ }
+
+ private Builder(@NonNull IconCompat icon) {
+ mType = TYPE_CUSTOM;
+ this.mIcon = icon;
+ mTint = null;
+ }
+
+ private Builder(@NonNull CarIcon carIcon) {
+ mType = carIcon.getType();
+ mIcon = carIcon.getIcon();
+ mTint = carIcon.getTint();
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java b/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
new file mode 100644
index 0000000..55c9163
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
@@ -0,0 +1,214 @@
+/*
+ * 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 android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+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(CarIcon.of(
+ * 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
+ */
+public class CarIconSpan extends CharacterStyle {
+ /**
+ * Indicates how to align a car icon span with its surrounding text.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ ALIGN_CENTER,
+ ALIGN_BOTTOM,
+ ALIGN_BASELINE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @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);
+ return new CarIconSpan(requireNonNull(icon), validateAlignment(alignment));
+ }
+
+ /**
+ * Ensures that the {@code alignment} is of one of the supported types.
+ */
+ public static int validateAlignment(int alignment) {
+ if (alignment != ALIGN_BASELINE && alignment != ALIGN_BOTTOM && alignment != ALIGN_CENTER) {
+ throw new IllegalStateException("Invalid alignment value: " + alignment);
+ }
+ return alignment;
+ }
+
+ private CarIconSpan(@Nullable CarIcon icon, @Alignment int alignment) {
+ this.mIcon = icon;
+ this.mAlignment = alignment;
+ }
+
+ private CarIconSpan() {
+ mIcon = null;
+ mAlignment = ALIGN_BASELINE;
+ }
+
+ @Nullable
+ public CarIcon getIcon() {
+ return mIcon;
+ }
+
+ @Alignment
+ public int getAlignment() {
+ return mAlignment;
+ }
+
+ @Override
+ public void updateDrawState(@Nullable TextPaint paint) {
+ // Not relevant.
+ }
+
+ @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";
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarText.java b/car/app/app/src/main/java/androidx/car/app/model/CarText.java
new file mode 100644
index 0000000..a1b846a
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarText.java
@@ -0,0 +1,196 @@
+/*
+ * 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 android.text.Spanned;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.utils.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A model used to send text with attached spans to the host.
+ */
+public class CarText {
+ /** An empty CarText for convenience. */
+ @NonNull
+ public static final CarText EMPTY = CarText.create("");
+
+ @Keep
+ @Nullable
+ private String mText;
+ @Keep
+ private final List<SpanWrapper> mSpans;
+
+ /**
+ * Returns {@code true} if the {@code carText} is {@code null} or an empty string, {@code
+ * false} otherwise.
+ */
+ public static boolean isNullOrEmpty(@Nullable CarText carText) {
+ if (carText == null) {
+ return true;
+ }
+
+ String text = carText.mText;
+ return text == null || text.isEmpty();
+ }
+
+ /**
+ * Returns a {@link CarText} instance for the given {@link CharSequence}, by sanitizing the car
+ * sequence (dropping unsupported {@link Spanned} objects, and wrapping the remaining supported
+ * {@link Spanned} objects into data that can be sent across to the host in a bundle.
+ */
+ @NonNull
+ public static CarText create(@NonNull CharSequence text) {
+ return new CarText(text);
+ }
+
+ @Nullable
+ public String getText() {
+ return mText;
+ }
+
+ public boolean isEmpty() {
+ return mText == null || mText.isEmpty();
+ }
+
+ /** Returns the optional list of spans attached to the text. */
+ @NonNull
+ public List<SpanWrapper> getSpans() {
+ return mSpans;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ String text = getText();
+ return text == null ? "" : text;
+ }
+
+ /**
+ * Returns a shortened string from the input {@code text}.
+ */
+ @Nullable
+ public static String toShortString(@Nullable CarText text) {
+ return text == null ? null : StringUtils.shortenString(text.toString());
+ }
+
+ public CarText() {
+ mText = null;
+ mSpans = Collections.emptyList();
+ }
+
+ private CarText(CharSequence text) {
+ this.mText = text.toString();
+
+ mSpans = new ArrayList<>();
+
+ if (text instanceof Spanned) {
+ Spanned spanned = (Spanned) text;
+
+ for (Object span : spanned.getSpans(0, text.length(), Object.class)) {
+ if (span instanceof ForegroundCarColorSpan
+ || span instanceof CarIconSpan
+ || span instanceof DurationSpan
+ || span instanceof DistanceSpan) {
+ mSpans.add(SpanWrapper.wrap(spanned, span));
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CarText)) {
+ return false;
+ }
+ CarText otherText = (CarText) other;
+ return Objects.equals(mText, otherText.mText) && Objects.equals(mSpans, otherText.mSpans);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mText, mSpans);
+ }
+
+ /**
+ * Wraps a span to send it to the host.
+ */
+ public static class SpanWrapper {
+ @Keep
+ public final int start;
+ @Keep
+ public final int end;
+ @Keep
+ public final int flags;
+ @Keep
+ @Nullable
+ public final Object span;
+
+ static SpanWrapper wrap(Spanned spanned, Object span) {
+ return new SpanWrapper(spanned, span);
+ }
+
+ SpanWrapper(Spanned spanned, Object span) {
+ this.start = spanned.getSpanStart(span);
+ this.end = spanned.getSpanEnd(span);
+ this.flags = spanned.getSpanFlags(span);
+ this.span = span;
+ }
+
+ SpanWrapper() {
+ start = 0;
+ end = 0;
+ flags = 0;
+ span = null;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SpanWrapper)) {
+ return false;
+ }
+ SpanWrapper wrapper = (SpanWrapper) other;
+ return start == wrapper.start
+ && end == wrapper.end
+ && flags == wrapper.flags
+ && Objects.equals(span, wrapper.span);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(start, end, flags, span);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + span + ": " + start + ", " + end + ", flags: " + flags + "]";
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DateTimeWithZone.java b/car/app/app/src/main/java/androidx/car/app/model/DateTimeWithZone.java
new file mode 100644
index 0000000..4b1416f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/DateTimeWithZone.java
@@ -0,0 +1,218 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.TextStyle;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * A time with an associated time zone information.
+ *
+ * <p>In order to avoid time zone databases being out of sync between the app and the host, this
+ * model avoids using <a href="https://www.iana.org/time-zones">IANA database</a> time zone IDs and
+ * instead relies on the app passing the time zone offset and its abbreviated name. Apps can use
+ * their time library of choice to retrieve the time zone information.
+ *
+ * <p>{@link #create(long, TimeZone)} and {@link #create(ZonedDateTime)} are provided for
+ * convenience if using {@code java.util} and {@code java.time} respectively. If using another
+ * library such as Joda time, {@link #create(long, int, String)} can be used.
+ */
+@SuppressWarnings("MissingSummary")
+public class DateTimeWithZone {
+ /** The maximum allowed offset for a time zone, in seconds. */
+ private static final long MAX_ZONE_OFFSET_SECONDS = 18 * HOURS.toSeconds(1);
+
+ @Keep
+ private final long mTimeSinceEpochMillis;
+ @Keep
+ private final int mZoneOffsetSeconds;
+ @Nullable
+ @Keep
+ private final String mZoneShortName;
+
+ /** Returns the number of milliseconds from the epoch of 1970-01-01T00:00:00Z. */
+ public long getTimeSinceEpochMillis() {
+ return mTimeSinceEpochMillis;
+ }
+
+ /** Returns the offset of the time zone from UTC. */
+ @SuppressLint("MethodNameUnits")
+ public int getZoneOffsetSeconds() {
+ return mZoneOffsetSeconds;
+ }
+
+ /**
+ * Returns the abbreviated name of the time zone, for example "PST" for Pacific Standard
+ * Time.
+ */
+ @Nullable
+ public String getZoneShortName() {
+ return mZoneShortName;
+ }
+
+ @Override
+ @NonNull
+ @RequiresApi(26)
+ // TODO(shiufai): consider removing the @RequiresApi annotation for a toString method.
+ @SuppressLint("UnsafeNewApiCall")
+ public String toString() {
+ return "[local: "
+ + LocalDateTime.ofEpochSecond(
+ mTimeSinceEpochMillis / 1000,
+ /* nanoOfSecond= */ 0,
+ ZoneOffset.ofTotalSeconds(mZoneOffsetSeconds))
+ + ", zone: "
+ + mZoneShortName
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTimeSinceEpochMillis, mZoneOffsetSeconds, mZoneShortName);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DateTimeWithZone)) {
+ return false;
+ }
+ DateTimeWithZone otherDateTime = (DateTimeWithZone) other;
+
+ return mTimeSinceEpochMillis == otherDateTime.mTimeSinceEpochMillis
+ && mZoneOffsetSeconds == otherDateTime.mZoneOffsetSeconds
+ && Objects.equals(mZoneShortName, otherDateTime.mZoneShortName);
+ }
+
+ /**
+ * Returns an instance of a {@link DateTimeWithZone}.
+ *
+ * @param timeSinceEpochMillis The number of milliseconds from the epoch of
+ * 1970-01-01T00:00:00Z.
+ * @param zoneOffsetSeconds The offset of the time zone from UTC at the date specified by
+ * {@code timeInUtcMillis}. This offset must be in the range
+ * {@code -18:00} to {@code +18:00}, which corresponds to -64800
+ * to +64800.
+ * @param zoneShortName The abbreviated name of the time zone, for example, "PST" for
+ * Pacific Standard Time. This string may be used to display to
+ * the user along with the date when needed, for example, if this
+ * time zone is different than the current system time zone.
+ * @throws IllegalArgumentException if {@code timeSinceEpochMillis} is a negative value.
+ * @throws IllegalArgumentException if {@code zoneOffsetSeconds} is no within the required
+ * range.
+ * @throws NullPointerException if {@code zoneShortName} is {@code null}.
+ * @throws IllegalArgumentException if {@code zoneShortName} is empty.
+ */
+ @NonNull
+ public static DateTimeWithZone create(
+ long timeSinceEpochMillis, int zoneOffsetSeconds, @NonNull String zoneShortName) {
+ if (timeSinceEpochMillis < 0) {
+ throw new IllegalArgumentException(
+ "Time since epoch must be greater than or equal to zero");
+ }
+ if (Math.abs(zoneOffsetSeconds) > MAX_ZONE_OFFSET_SECONDS) {
+ throw new IllegalArgumentException("Zone offset not in valid range: -18:00 to +18:00");
+ }
+ if (requireNonNull(zoneShortName).isEmpty()) {
+ throw new IllegalArgumentException("The time zone short name can not be null or empty");
+ }
+ return new DateTimeWithZone(timeSinceEpochMillis, zoneOffsetSeconds, zoneShortName);
+ }
+
+ /**
+ * Returns an instance of a {@link DateTimeWithZone}.
+ *
+ * @param timeSinceEpochMillis The number of milliseconds from the epoch of
+ * 1970-01-01T00:00:00Z.
+ * @param timeZone The time zone at the date specified by {@code timeInUtcMillis}.
+ * The abbreviated
+ * name of this time zone, formatted using the default locale, may
+ * be displayed to the user
+ * when needed, for example, if this time zone is different than
+ * the current system time zone.
+ * @throws IllegalArgumentException if {@code timeSinceEpochMillis} is a negative value.
+ * @throws NullPointerException if {@code timeZone} is {@code null}.
+ */
+ @NonNull
+ public static DateTimeWithZone create(long timeSinceEpochMillis, @NonNull TimeZone timeZone) {
+ if (timeSinceEpochMillis < 0) {
+ throw new IllegalArgumentException(
+ "timeSinceEpochMillis must be greater than or equal to zero");
+ }
+ return create(
+ timeSinceEpochMillis,
+ (int) MILLISECONDS.toSeconds(
+ requireNonNull(timeZone).getOffset(timeSinceEpochMillis)),
+ timeZone.getDisplayName(false, TimeZone.SHORT));
+ }
+
+ /**
+ * Returns an instance of a {@link DateTimeWithZone}.
+ *
+ * @param zonedDateTime The time with a time zone. The abbreviated name of this time zone,
+ * formatted using the default locale, may be displayed to the user when
+ * needed, for example,
+ * if this time zone is different than the current system time zone.
+ * @throws NullPointerException if {@code zonedDateTime} is {@code null}.
+ */
+ // TODO(shiufai): revisit wrapping this method in a container class (e.g. Api26Impl).
+ @SuppressLint("UnsafeNewApiCall")
+ @RequiresApi(26)
+ @NonNull
+ public static DateTimeWithZone create(@NonNull ZonedDateTime zonedDateTime) {
+ LocalDateTime localDateTime = requireNonNull(zonedDateTime).toLocalDateTime();
+ ZoneId zoneId = zonedDateTime.getZone();
+ ZoneOffset zoneOffset = zoneId.getRules().getOffset(localDateTime);
+ return create(
+ SECONDS.toMillis(localDateTime.toEpochSecond(zoneOffset)),
+ zoneOffset.getTotalSeconds(),
+ zoneId.getDisplayName(TextStyle.SHORT, Locale.getDefault()));
+ }
+
+ private DateTimeWithZone() {
+ mTimeSinceEpochMillis = 0;
+ mZoneOffsetSeconds = 0;
+ mZoneShortName = null;
+ }
+
+ private DateTimeWithZone(
+ long timeSinceEpochMillis, int zoneOffsetSeconds, @Nullable String timeZoneShortName) {
+ this.mTimeSinceEpochMillis = timeSinceEpochMillis;
+ this.mZoneOffsetSeconds = zoneOffsetSeconds;
+ this.mZoneShortName = timeZoneShortName;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Distance.java b/car/app/app/src/main/java/androidx/car/app/model/Distance.java
new file mode 100644
index 0000000..2f1ea2d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Distance.java
@@ -0,0 +1,215 @@
+/*
+ * 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 androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+import java.util.Objects;
+
+/** Represents a distance value and how it should be displayed in the UI. */
+public final class Distance {
+ /**
+ * Possible units used to display {@link Distance}
+ *
+ * @hide
+ */
+ @IntDef({
+ UNIT_METERS,
+ UNIT_KILOMETERS,
+ UNIT_MILES,
+ UNIT_FEET,
+ UNIT_YARDS,
+ UNIT_KILOMETERS_P1,
+ UNIT_MILES_P1
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @RestrictTo(LIBRARY)
+ public @interface Unit {
+ }
+
+ /** Meter unit. */
+ @Unit
+ public static final int UNIT_METERS = 1;
+
+ /** Kilometer unit. */
+ @Unit
+ public static final int UNIT_KILOMETERS = 2;
+
+ /**
+ * Kilometer unit with the additional requirement that distances of this type be displayed
+ * with at least 1 digit of precision after the decimal point (for example, 2.0).
+ */
+ @Unit
+ public static final int UNIT_KILOMETERS_P1 = 3;
+
+ /** Miles unit. */
+ @Unit
+ public static final int UNIT_MILES = 4;
+
+ /**
+ * Mile unit with the additional requirement that distances of this type be displayed with at
+ * least 1 digit of precision after the decimal point (for example, 2.0).
+ */
+ @Unit
+ public static final int UNIT_MILES_P1 = 5;
+
+ /** Feet unit. */
+ @Unit
+ public static final int UNIT_FEET = 6;
+
+ /** Yards unit. */
+ @Unit
+ public static final int UNIT_YARDS = 7;
+
+ @Keep
+ private final double mDisplayDistance;
+ @Keep
+ @Unit
+ private final int mDisplayUnit;
+
+ /**
+ * Constructs a new instance of a {@link Distance}.
+ *
+ * <p>Units with precision requirements, {@link #UNIT_KILOMETERS_P1} and {@link #UNIT_MILES_P1},
+ * will always show one decimal digit. All other units will show a decimal digit if needed but
+ * will not if the distance is a whole number.
+ *
+ * <h4>Examples</h4>
+ *
+ * A display distance of 1.0 with a display unit of {@link #UNIT_KILOMETERS} will display "1
+ * km", whereas if the display unit is {@link #UNIT_KILOMETERS_P1} it will display "1.0 km".
+ * Note the "km" part of the string in this example depends on the locale the host is
+ * configured with.
+ *
+ * <p>A display distance of 1.46 however will display "1.4 km" for both {@link #UNIT_KILOMETERS}
+ * and {@link #UNIT_KILOMETERS} display units.
+ *
+ * <p>{@link #UNIT_KILOMETERS_P1} and {@link #UNIT_MILES_P1} can be used to provide consistent
+ * digit placement for a sequence of distances. For example, as the user is driving and the next
+ * turn distance changes, using {@link #UNIT_KILOMETERS_P1} will produce: "2.5 km", "2.0 km",
+ * "1.5 km", "1.0 km", and so on.
+ *
+ * @param displayDistance the distance to display, in the units specified in {@code
+ * displayUnit}. See {@link #getDisplayDistance()}.
+ * @param displayUnit the unit of distance to use when displaying the value in {@code
+ * displayUnit}. This should be one of the {@code UNIT_*} static
+ * constants defined in this class. See {@link #getDisplayUnit()}.
+ * @throws IllegalArgumentException if {@code displayDistance} is negative.
+ */
+ @NonNull
+ public static Distance create(double displayDistance, @Unit int displayUnit) {
+ if (displayDistance < 0) {
+ throw new IllegalArgumentException("displayDistance must be a positive value");
+ }
+ return new Distance(displayDistance, displayUnit);
+ }
+
+ /**
+ * Returns the distance measured in the unit indicated at {@link #getDisplayUnit()}.
+ *
+ * <p>This distance is for display purposes only and it might be a rounded representation of the
+ * actual distance. For example, a distance of 1000 meters could be shown in the following ways:
+ *
+ * <ul>
+ * <li>Display unit of {@link #UNIT_METERS} and distance of 1000, resulting in a display of
+ * "1000 m".
+ * <li>Display unit of {@link #UNIT_KILOMETERS} and distance of 1, resulting in a
+ * display of "1 km".
+ * <li>Display unit of {@link #UNIT_KILOMETERS_P1} and distance of 1, resulting in a
+ * display of "1.0 km".
+ * </ul>
+ */
+ public double getDisplayDistance() {
+ return mDisplayDistance;
+ }
+
+ /**
+ * Returns the unit that should be used to display the distance value, adjusted to the current
+ * user's locale and location. This should match the unit used in {@link #getDisplayDistance()}.
+ */
+ @Unit
+ public int getDisplayUnit() {
+ return mDisplayUnit;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "%.04f%s", mDisplayDistance, unitToString(mDisplayUnit));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDisplayDistance, mDisplayUnit);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Distance)) {
+ return false;
+ }
+ Distance otherDistance = (Distance) other;
+
+ return mDisplayUnit == otherDistance.mDisplayUnit
+ && mDisplayDistance == otherDistance.mDisplayDistance;
+ }
+
+ private Distance(double displayDistance, @Unit int displayUnit) {
+ this.mDisplayDistance = displayDistance;
+ this.mDisplayUnit = displayUnit;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Distance() {
+ mDisplayDistance = 0.0d;
+ mDisplayUnit = UNIT_METERS;
+ }
+
+ private static String unitToString(@Unit int displayUnit) {
+ switch (displayUnit) {
+ case UNIT_FEET:
+ return "ft";
+ case UNIT_KILOMETERS:
+ return "km";
+ case UNIT_KILOMETERS_P1:
+ return "km_p1";
+ case UNIT_METERS:
+ return "m";
+ case UNIT_MILES:
+ return "mi";
+ case UNIT_MILES_P1:
+ return "mi_p1";
+ case UNIT_YARDS:
+ return "yd";
+ default:
+ return "?";
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java b/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
new file mode 100644
index 0000000..9fe03a8
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
@@ -0,0 +1,114 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * A span that replaces the text it is attached to with the string representation of a {@link
+ * Distance} instance.
+ *
+ * <p>The {@link Distance} instance will be displayed by the host in a localized format, so that it
+ * will be consistent with the rest of the user interface where distance information are displayed.
+ *
+ * <p>For example, the following code creates a string that shows the distance as the first text in
+ * the string before the interpunct:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString(" " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(
+ * DistanceSpan.create(
+ * Distance.create(1000, "1.0", UNIT_KILOMETERS)), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * }</pre>
+ *
+ * <p>The span flags (e.g. SPAN_EXCLUSIVE_EXCLUSIVE) will be ignored.
+ *
+ * <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}. However, it is possible to
+ * apply styling to the text, such as changing colors:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString(" " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(
+ * DistanceSpan.create(
+ * Distance.create(1000, "1.0", UNIT_KILOMETERS)), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * string.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
+ * }</pre>
+ */
+public class DistanceSpan extends CharacterStyle {
+ @Nullable
+ @Keep
+ private final Distance mDistance;
+
+ /** Creates a {@link DistanceSpan} from a {@link CarIcon}. */
+ @NonNull
+ public static DistanceSpan create(@NonNull Distance distance) {
+ return new DistanceSpan(requireNonNull(distance));
+ }
+
+ private DistanceSpan(Distance distance) {
+ this.mDistance = distance;
+ }
+
+ private DistanceSpan() {
+ mDistance = null;
+ }
+
+ @NonNull
+ public Distance getDistance() {
+ return requireNonNull(mDistance);
+ }
+
+ @Override
+ public void updateDrawState(@Nullable TextPaint paint) {
+ // Not relevant.
+ }
+
+ @Override
+ public String toString() {
+ return "[distance: " + mDistance + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mDistance);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DistanceSpan)) {
+ return false;
+ }
+ DistanceSpan otherSpan = (DistanceSpan) other;
+
+ return Objects.equals(mDistance, otherSpan.mDistance);
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java b/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
new file mode 100644
index 0000000..05e58a9
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
@@ -0,0 +1,118 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.time.Duration;
+
+/**
+ * A span that replaces the text it is attached to with a localized duration string.
+ *
+ * <p>For example, the following code creates a string that shows the duration as the first text in
+ * the string before the interpunct:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString(" " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(DurationSpan.create(300), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * }</pre>
+ *
+ * <p>The span flags (e.g. SPAN_EXCLUSIVE_EXCLUSIVE) will be ignored.
+ *
+ * <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}. However, it is possible to *
+ * apply styling to the text, such as changing colors:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString(" " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(DurationSpan.create(300), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * string.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
+ * }</pre>
+ */
+public class DurationSpan extends CharacterStyle {
+ @Keep
+ private final long mDurationSeconds;
+
+ /** Creates a {@link DurationSpan} with the given duration. */
+ @NonNull
+ public static DurationSpan create(long durationSeconds) {
+ return new DurationSpan(durationSeconds);
+ }
+
+ /** Creates a {@link DurationSpan} with the given duration. */
+ @NonNull
+ @RequiresApi(26)
+ // TODO(shiufai): revisit wrapping this method in a container class (e.g. Api26Impl).
+ @SuppressLint("UnsafeNewApiCall")
+ public static DurationSpan create(@NonNull Duration duration) {
+ return new DurationSpan(requireNonNull(duration).getSeconds());
+ }
+
+ private DurationSpan(long durationSeconds) {
+ this.mDurationSeconds = durationSeconds;
+ }
+
+ private DurationSpan() {
+ mDurationSeconds = 0;
+ }
+
+ @SuppressLint("MethodNameUnits")
+ public long getDurationSeconds() {
+ return mDurationSeconds;
+ }
+
+ @Override
+ public void updateDrawState(@Nullable TextPaint paint) {
+ // Not relevant.
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[seconds: " + mDurationSeconds + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ // Equivalent implementation as Long.hashcode() but avoids the boxing.
+ return (int) (mDurationSeconds ^ (mDurationSeconds >>> 32));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DurationSpan)) {
+ return false;
+ }
+ DurationSpan otherSpan = (DurationSpan) other;
+
+ return mDurationSeconds == otherSpan.mDurationSeconds;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ForegroundCarColorSpan.java b/car/app/app/src/main/java/androidx/car/app/model/ForegroundCarColorSpan.java
new file mode 100644
index 0000000..b9fdfb3
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ForegroundCarColorSpan.java
@@ -0,0 +1,122 @@
+/*
+ * 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 android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.constraints.CarColorConstraints;
+
+import java.util.Objects;
+
+/**
+ * A span that changes the color of the text to which the span is attached.
+ *
+ * <p>For example, to set a green text color to a span of a string:
+ *
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with a foreground color span");
+ * string.setSpan(ForegroundCarColorSpan.create(CarColor.GREEN),
+ * 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE));
+ * }</pre>
+ *
+ * <p>The host may ignore the color specified in the {@link ForegroundCarColorSpan} and instead use
+ * a default color unless support for {@link ForegroundCarColorSpan} is explicitly documented in the
+ * API that takes the string. The host may use a default color if the color in the span does not
+ * pass the contrast requirements.
+ *
+ * @see CarColor
+ * @see ForegroundColorSpan
+ */
+public class ForegroundCarColorSpan extends CharacterStyle {
+ @Keep
+ private final CarColor mCarColor;
+
+ @NonNull
+ public CarColor getColor() {
+ return mCarColor;
+ }
+
+ /**
+ * Creates a {@link ForegroundColorSpan} from a {@link CarColor}.
+ *
+ * <p>Custom colors created with {@link CarColor#createCustom} are not supported in text spans.
+ *
+ * @throws IllegalArgumentException if {@code carColor} contains a custom color.
+ * @throws NullPointerException if {@code carColor} is {@code null}.
+ */
+ @NonNull
+ public static ForegroundCarColorSpan create(@NonNull CarColor carColor) {
+ CarColorConstraints.STANDARD_ONLY.validateOrThrow(carColor);
+ return new ForegroundCarColorSpan(requireNonNull(carColor));
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY)
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @NonNull
+ public static ForegroundCarColorSpan createForTesting(@NonNull CarColor carColor) {
+ return new ForegroundCarColorSpan(carColor);
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint paint) {
+ // Not relevant.
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[color: " + mCarColor + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mCarColor);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ForegroundCarColorSpan)) {
+ return false;
+ }
+ ForegroundCarColorSpan otherSpan = (ForegroundCarColorSpan) other;
+
+ return Objects.equals(mCarColor, otherSpan.mCarColor);
+ }
+
+ private ForegroundCarColorSpan(CarColor carColor) {
+ this.mCarColor = carColor;
+ }
+
+ private ForegroundCarColorSpan() {
+ mCarColor = CarColor.DEFAULT;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
new file mode 100644
index 0000000..535d90c
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -0,0 +1,330 @@
+/*
+ * 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 android.annotation.SuppressLint;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents a grid item with an image and an optional title.
+ */
+// TODO(shiufai): Support toggle state in a grid item.
+// TODO(shiufai): Make grid item browsable.
+public class GridItem implements Item {
+ /**
+ * The type of images supported within grid items.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @RestrictTo(LIBRARY)
+ @IntDef(value = {IMAGE_TYPE_ICON, IMAGE_TYPE_LARGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface GridItemImageType {
+ }
+
+ /**
+ * Represents an icon to be displayed in the grid item.
+ *
+ * <p>If necessary, icons will be scaled down to fit within a 44 x 44 dp bounding box,
+ * preserving
+ * their aspect ratios.
+ *
+ * <p>A tint color is expected to be provided via {@link CarIcon.Builder#setTint}. Otherwise, a
+ * default tint color as determined by the host will be applied.
+ */
+ public static final int IMAGE_TYPE_ICON = (1 << 0);
+
+ /**
+ * Represents a large image to be displayed in the grid item.
+ *
+ * <p>If necessary, these images will be scaled down to fit within a 80 x 80 dp bounding box,
+ * preserving their aspect ratio.
+ */
+ public static final int IMAGE_TYPE_LARGE = (1 << 1);
+
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final CarText mText;
+ @Keep
+ @Nullable
+ private final CarIcon mImage;
+ @Keep
+ @Nullable
+ private final Toggle mToggle;
+ @Keep
+ @GridItemImageType
+ private final int mImageType;
+ @Keep
+ @Nullable
+ private final OnClickListenerWrapper mOnClickListener;
+
+ /** Constructs a new builder of {@link GridItem}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Returns the title of the grid item. */
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ /** Returns the list of text below the title. */
+ @Nullable
+ public CarText getText() {
+ return mText;
+ }
+
+ /** Returns the image of the grid item. */
+ @NonNull
+ public CarIcon getImage() {
+ return requireNonNull(mImage);
+ }
+
+ /** Returns the image type of the grid item. */
+ @GridItemImageType
+ public int getImageType() {
+ return mImageType;
+ }
+
+ /**
+ * Returns the {@link Toggle} in the grid item or {@code null} if the grid item does not
+ * contain a toggle.
+ */
+ @Nullable
+ public Toggle getToggle() {
+ return mToggle;
+ }
+
+ /**
+ * Returns the {@link OnClickListener} to be called back when the grid item is clicked, or
+ * {@code null} if the grid item is non-clickable.
+ */
+ @Nullable
+ public OnClickListenerWrapper getOnClickListener() {
+ return mOnClickListener;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[title: "
+ + CarText.toShortString(mTitle)
+ + ", text: "
+ + CarText.toShortString(mText)
+ + ", image: "
+ + mImage
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mImage, mImageType, mToggle, mOnClickListener == null);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof GridItem)) {
+ return false;
+ }
+ GridItem otherGridItem = (GridItem) other;
+
+ return Objects.equals(mTitle, otherGridItem.mTitle)
+ && Objects.equals(mText, otherGridItem.mText)
+ && Objects.equals(mImage, otherGridItem.mImage)
+ && Objects.equals(mToggle, otherGridItem.mToggle)
+ && Objects.equals(mOnClickListener == null, otherGridItem.mOnClickListener == null)
+ && mImageType == otherGridItem.mImageType;
+ }
+
+ private GridItem(Builder builder) {
+ mTitle = builder.mTitle;
+ mText = builder.mText;
+ mImage = builder.mImage;
+ mImageType = builder.mImageType;
+ mToggle = builder.mToggle;
+ mOnClickListener = builder.mOnClickListener;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private GridItem() {
+ mTitle = null;
+ mText = null;
+ mImage = null;
+ mImageType = IMAGE_TYPE_LARGE;
+ mToggle = null;
+ mOnClickListener = null;
+ }
+
+ /** A builder of {@link GridItem}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ @Nullable
+ private CarText mText;
+ @Nullable
+ private CarIcon mImage;
+ @GridItemImageType
+ private int mImageType = IMAGE_TYPE_LARGE;
+ @Nullable
+ private Toggle mToggle;
+ @Nullable
+ private OnClickListenerWrapper mOnClickListener;
+
+ /** Sets the title of the grid item, or {@code null} to not show the title. */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets the text string to the grid item that is displayed below the title, or {@code
+ * null} to not show any text below the title.
+ *
+ * <h2>Text Wrapping</h2>
+ *
+ * The string added with {@link #setText} is truncated at the end to fit in a single line
+ * below
+ * the title.
+ */
+ @NonNull
+ public Builder setText(@Nullable CharSequence text) {
+ this.mText = text == null ? null : CarText.create(text);
+ return this;
+ }
+
+ /**
+ * Sets an image to show in the grid item with the default size {@link #IMAGE_TYPE_LARGE}.
+ *
+ * @see #setImage(CarIcon, int)
+ */
+ @NonNull
+ public Builder setImage(@NonNull CarIcon image) {
+ return setImage(image, IMAGE_TYPE_LARGE);
+ }
+
+ /**
+ * Sets an image to show in the grid item with the given {@code imageType}.
+ *
+ * <p>For a custom {@link CarIcon}, its {@link androidx.core.graphics.drawable.IconCompat}
+ * instance can be of {@link androidx.core.graphics.drawable.IconCompat#TYPE_BITMAP},
+ * {@link androidx.core.graphics.drawable.IconCompat#TYPE_RESOURCE}, or
+ * {@link androidx.core.graphics.drawable.IconCompat#TYPE_URI}.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * <p>If the input image's size exceeds the sizing requirements for the given image type in
+ * either one of the dimensions, it will be scaled down to be centered inside the
+ * bounding box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ *
+ * @param image the {@link CarIcon} to display.
+ * @param imageType one of {@link #IMAGE_TYPE_ICON} or {@link #IMAGE_TYPE_LARGE}.
+ */
+ @NonNull
+ public Builder setImage(@NonNull CarIcon image, @GridItemImageType int imageType) {
+ CarIconConstraints.UNCONSTRAINED.validateOrThrow(image);
+ this.mImage = image;
+ this.mImageType = imageType;
+ return this;
+ }
+
+ /**
+ * Sets a {@link Toggle} for the grid item, or {@code null} to not have any toggle states
+ * in the grid item. If set, this grid item acts as a toggle.
+ *
+ * <p>If the grid item has a {@link Toggle}, then no {@link OnClickListener} can be added
+ * to it.
+ */
+ @NonNull
+ public Builder setToggle(@Nullable Toggle toggle) {
+ this.mToggle = toggle;
+ return this;
+ }
+
+ /**
+ * Sets the {@link OnClickListener} to be called back when the grid item is clicked, or
+ * {@code null} to make the grid item non-clickable.
+ */
+ @NonNull
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+ if ( null) {
+ this.mOnClickListener = null;
+ } else {
+ this.mOnClickListener = OnClickListenerWrapper.create(onClickListener);
+ }
+ return this;
+ }
+
+ /**
+ * Constructs the {@link GridItem} defined by this builder.
+ *
+ * @throws IllegalStateException if the grid item's image is not set.
+ * @throws IllegalStateException if the grid item doesn't have a title but the text is set.
+ * @throws IllegalStateException if the grid item has both a {@link OnClickListener} and a
+ * {@link Toggle}.
+ */
+ @NonNull
+ public GridItem build() {
+ if (mImage == null) {
+ throw new IllegalStateException("An image must be set on the grid item");
+ }
+
+ if (mTitle == null && mText != null) {
+ throw new IllegalStateException(
+ "If a grid item doesn't have a title, it must not have a text set");
+ }
+
+ if (mToggle != null && mOnClickListener != null) {
+ throw new IllegalStateException(
+ "If a grid item contains a toggle, it must not have a onClickListener set"
+ + " and vice versa");
+ }
+
+ return new GridItem(this);
+ }
+
+ private Builder() {
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
new file mode 100644
index 0000000..a3a4ddf
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
@@ -0,0 +1,341 @@
+/*
+ * 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.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A template representing a grid of items.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * previous one if:
+ *
+ * <ul>
+ * <li>The template title has not changed, and
+ * <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
+ * number of grid items and the string contents (title, texts) of each grid item have not
+ * changed.
+ * <li>For grid items that contain a {@link Toggle}, updates to the title, text and image are also
+ * allowed if the toggle state has changed between the previous and new templates.
+ * </ul>
+ */
+public final class GridTemplate implements Template {
+ @Keep
+ private final boolean mIsLoading;
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ItemList mSingleList;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+ @Keep
+ @Nullable
+ private final CarIcon mBackgroundImage;
+
+ /** Constructs a new builder of {@link GridTemplate}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @Nullable
+ public ItemList getSingleList() {
+ return mSingleList;
+ }
+
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ @Nullable
+ public CarIcon getBackgroundImage() {
+ return mBackgroundImage;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ requireNonNull(oldTemplate);
+
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ GridTemplate old = (GridTemplate) oldTemplate;
+
+ if (!Objects.equals(old.getTitle(), getTitle())) {
+ return false;
+ }
+
+ if (old.mIsLoading) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (mIsLoading) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ if (mSingleList != null && old.mSingleList != null) {
+ return mSingleList.isRefresh(old.mSingleList, logger);
+ }
+
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "GridTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsLoading, mTitle, mHeaderAction, mSingleList, mActionStrip,
+ mBackgroundImage);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof GridTemplate)) {
+ return false;
+ }
+ GridTemplate otherTemplate = (GridTemplate) other;
+
+ return mIsLoading == otherTemplate.mIsLoading
+ && Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mSingleList, otherTemplate.mSingleList)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+ && Objects.equals(mBackgroundImage, otherTemplate.mBackgroundImage);
+ }
+
+ private GridTemplate(Builder builder) {
+ mIsLoading = builder.mIsLoading;
+ mTitle = builder.mTitle;
+ mHeaderAction = builder.mHeaderAction;
+ mSingleList = builder.mSingleList;
+ mActionStrip = builder.mActionStrip;
+ mBackgroundImage = builder.mBackgroundImage;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private GridTemplate() {
+ mIsLoading = false;
+ mTitle = null;
+ mHeaderAction = null;
+ mSingleList = null;
+ mActionStrip = null;
+ mBackgroundImage = null;
+ }
+
+ /** A builder of {@link GridTemplate}. */
+ public static final class Builder {
+ private boolean mIsLoading;
+ @Nullable
+ private ItemList mSingleList;
+ @Nullable
+ private CarText mTitle;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+
+ /** For internal, host-side use only. */
+ @Nullable
+ private CarIcon mBackgroundImage;
+
+ /**
+ * Sets whether the template is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI shows a loading indicator where the grid content
+ * would be
+ * otherwise. The caller is expected to call {@link androidx.car.app.Screen#invalidate()}
+ * and send the new template content to the host once the data is ready. If set to {@code
+ * false}, the UI shows the {@link ItemList} contents added via {@link #setSingleList}.
+ */
+ @NonNull
+ public Builder setLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null}
+ * to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /** Sets the {@link CharSequence} to show as title, or {@code null} to not show a title. */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets a single {@link ItemList} to show in the template.
+ *
+ * @throws NullPointerException if {@code list} is null.
+ */
+ @NonNull
+ public Builder setSingleList(@NonNull ItemList list) {
+ mSingleList = requireNonNull(list);
+ return this;
+ }
+
+ /** Resets the list that was added via {@link #setSingleList}. */
+ @NonNull
+ public Builder clearAllLists() {
+ mSingleList = null;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+ * ActionStrip}.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+ * {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}.
+ * Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Sets a {@link CarIcon} to be shown as background of the template.
+ *
+ * <p>For internal, host-side use only.
+ */
+ @NonNull
+ public Builder setBackgroundImage(@Nullable CarIcon backgroundImage) {
+ CarIconConstraints.UNCONSTRAINED.validateOrThrow(backgroundImage);
+ this.mBackgroundImage = backgroundImage;
+ return this;
+ }
+
+ /**
+ * Constructs the template defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 6 {@link GridItem}s total in the {@link ItemList}(s). The host
+ * will ignore any items over that limit.
+ *
+ * <p>Either a header {@link Action} or title must be set on the template.
+ *
+ * @throws IllegalStateException if the template is in a loading state but there are
+ * lists
+ * added, or vice versa.
+ * @throws IllegalArgumentException if the added {@link ItemList} does not meet the
+ * template's
+ * requirements.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link
+ * Action} set.
+ */
+ @NonNull
+ public GridTemplate build() {
+ boolean hasList = mSingleList != null;
+ if (mIsLoading == hasList) {
+ throw new IllegalStateException(
+ "Template is in a loading state but lists are added, or vice versa");
+ }
+
+ if (mSingleList != null) {
+ for (Object gridItemObject : mSingleList.getItems()) {
+ if (!(gridItemObject instanceof GridItem)) {
+ throw new IllegalArgumentException(
+ "All the items in grid template's item list must be grid items");
+ }
+ }
+ }
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new GridTemplate(this);
+ }
+
+ private Builder() {
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Item.java b/car/app/app/src/main/java/androidx/car/app/model/Item.java
new file mode 100644
index 0000000..6e50daf
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Item.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+/** Interface implemented by models that can be added to an {@link ItemList}. */
+public interface Item {
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
new file mode 100644
index 0000000..92649b7
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
@@ -0,0 +1,436 @@
+/*
+ * 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 android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.IOnItemVisibilityChangedListener;
+import androidx.car.app.IOnSelectedListener;
+import androidx.car.app.utils.Logger;
+import androidx.car.app.utils.RemoteUtils;
+import androidx.car.app.utils.ValidationUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of {@link Item} instances. {@link ItemList} instances are used by templates
+ * that contain lists of models, such as for example, the list of {@link Row}s in a {@link
+ * ListTemplate}.
+ */
+public final class ItemList {
+ /**
+ * A listener for handling selection events for lists with selectable items.
+ *
+ * @see Builder#setSelectable(OnSelectedListener)
+ */
+ public interface OnSelectedListener {
+ /**
+ * Notifies that an item was selected.
+ *
+ * <p>This event is called even if the selection did not change, for example, if the user
+ * selected an already selected item.
+ *
+ * @param selectedIndex the index of the newly selected item.
+ */
+ void onSelected(int selectedIndex);
+ }
+
+ /** A listener for handling item visibility changes. */
+ public interface OnItemVisibilityChangedListener {
+ /**
+ * Notifies that the items in the list within the specified indices have become visible.
+ *
+ * <p>The start index is inclusive, and the end index is exclusive. For example, if only the
+ * first item in a list is visible, the start and end indices would be 0 and 1,
+ * respectively. If
+ * no items are visible, the indices will be set to -1.
+ *
+ * @param startIndex the index of the first item that is visible.
+ * @param endIndex the index of the first item that is not visible after the visible
+ * range.
+ */
+ void onItemVisibilityChanged(int startIndex, int endIndex);
+ }
+
+ @Keep
+ private final int mSelectedIndex;
+ @Keep
+ private final List<Object> mItems;
+ @Keep
+ @Nullable
+ private final IOnSelectedListener mOnSelectedListener;
+ @Keep
+ @Nullable
+ private final IOnItemVisibilityChangedListener mItemVisibilityChangedListener;
+ @Keep
+ @Nullable
+ private final CarText mNoItemsMessage;
+
+ /** Constructs a new builder of {@link ItemList}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Returns the index of the selected item of the list. */
+ public int getSelectedIndex() {
+ return mSelectedIndex;
+ }
+
+ /**
+ * Returns the {@link OnSelectedListener} to be called when when an item is selected by the
+ * user, or {@code null} is the list is non-selectable.
+ *
+ * @hide
+ */
+ // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+ @RestrictTo(LIBRARY)
+ @Nullable
+ public IOnSelectedListener getOnSelectedListener() {
+ return mOnSelectedListener;
+ }
+
+ /** Returns the text to be displayed if the list is empty. */
+ @Nullable
+ public CarText getNoItemsMessage() {
+ return mNoItemsMessage;
+ }
+
+ /**
+ * Returns the {@link OnItemVisibilityChangedListener} to be called when the visible items in
+ * the list changes.
+ *
+ * @hide
+ */
+ // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+ @RestrictTo(LIBRARY)
+ @Nullable
+ public IOnItemVisibilityChangedListener getOnItemsVisibilityChangeListener() {
+ return mItemVisibilityChangedListener;
+ }
+
+ /** Returns the list of items in this {@link ItemList}. */
+ @NonNull
+ public List<Object> getItems() {
+ return mItems;
+ }
+
+ /**
+ * Returns {@code true} if this {@link ItemList} instance is determined to be a refresh of the
+ * given list, or {@code false} otherwise.
+ *
+ * <p>A list is considered a refresh if:
+ *
+ * <ul>
+ * <li>The other list is in a loading state, or
+ * <li>The item size and string contents of the two lists are the same. For rows that
+ * contain a
+ * {@link Toggle}, the string contents can be updated if the toggle state has changed
+ * between the previous and new rows. For grid items that contain a {@link Toggle}, string
+ * contents and images can be updated if the toggle state has changed.
+ * </ul>
+ */
+ public boolean isRefresh(@Nullable ItemList other, @NonNull Logger logger) {
+ if (other == null) {
+ return false;
+ }
+
+ return ValidationUtils.itemsHaveSameContent(
+ other.getItems(), other.getSelectedIndex(), getItems(), getSelectedIndex(), logger);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[ items: "
+ + (mItems != null ? mItems.toString() : null)
+ + ", selected: "
+ + mSelectedIndex
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mSelectedIndex,
+ mItems,
+ mOnSelectedListener == null,
+ mItemVisibilityChangedListener == null,
+ mNoItemsMessage);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ItemList)) {
+ return false;
+ }
+ ItemList otherList = (ItemList) other;
+
+ // For listeners only check if they are either both null, or both set.
+ return mSelectedIndex == otherList.mSelectedIndex
+ && Objects.equals(mItems, otherList.mItems)
+ && Objects.equals(mOnSelectedListener == null,
+ otherList.mOnSelectedListener == null)
+ && Objects.equals(
+ mItemVisibilityChangedListener == null,
+ otherList.mItemVisibilityChangedListener == null)
+ && Objects.equals(mNoItemsMessage, otherList.mNoItemsMessage);
+ }
+
+ private ItemList(Builder builder) {
+ mSelectedIndex = builder.mSelectedIndex;
+ mItems = new ArrayList<>(builder.mItems);
+ mNoItemsMessage = builder.mNoItemsMessage;
+ mOnSelectedListener = builder.mOnSelectedListener;
+ mItemVisibilityChangedListener = builder.mOnItemVisibilityChangedListener;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private ItemList() {
+ mSelectedIndex = 0;
+ mItems = Collections.emptyList();
+ mNoItemsMessage = null;
+ mOnSelectedListener = null;
+ mItemVisibilityChangedListener = null;
+ }
+
+ /** A builder of {@link ItemList}. */
+ public static final class Builder {
+ private final List<Object> mItems = new ArrayList<>();
+ private int mSelectedIndex;
+ @Nullable
+ private IOnSelectedListener mOnSelectedListener;
+ @Nullable
+ private IOnItemVisibilityChangedListener mOnItemVisibilityChangedListener;
+ @Nullable
+ private CarText mNoItemsMessage;
+
+ /**
+ * Sets the {@link OnItemVisibilityChangedListener} to call when the visible items in the
+ * list changes.
+ */
+ @NonNull
+ // TODO(shiufai): remove MissingGetterMatchingBuilder once listener is properly exposed.
+ @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+ public Builder setOnItemsVisibilityChangeListener(
+ @Nullable OnItemVisibilityChangedListener itemVisibilityChangedListener) {
+ this.mOnItemVisibilityChangedListener =
+ itemVisibilityChangedListener == null
+ ? null
+ : new OnItemVisibilityChangedListenerStub(
+ itemVisibilityChangedListener);
+ return this;
+ }
+
+ /**
+ * Marks the list as selectable and sets the {@link OnSelectedListener} to call when an
+ * item is selected by the user. Set to {@code null} to mark the list as non-selectable.
+ *
+ * <p>Selectable lists, where allowed by the template they are added to, automatically
+ * display
+ * an item in a selected state when selected by the user.
+ *
+ * <p>The items in the list define a mutually exclusive selection scope: only a single
+ * item will
+ * be selected at any given time.
+ *
+ * <p>The specific way in which the selection will be visualized depends on the template
+ * and the
+ * host implementation. For example, some templates may display the list as a radio button
+ * group, while others may highlight the selected item's background.
+ *
+ * @see #setSelectedIndex(int)
+ */
+ @NonNull
+ // TODO(shiufai): remove MissingGetterMatchingBuilder once listener is properly exposed.
+ @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+ public Builder setSelectable(@Nullable OnSelectedListener onSelectedListener) {
+ this.mOnSelectedListener =
+ null ? null : new OnSelectedListenerStub(
+ onSelectedListener);
+ return this;
+ }
+
+ /**
+ * Sets the index of the item to show as selected.
+ *
+ * <p>By default and unless explicitly set with this method, the first item is selected.
+ *
+ * <p>If the list is not a selectable list set with {@link #setSelectable}, this value is
+ * ignored.
+ */
+ @NonNull
+ public Builder setSelectedIndex(int selectedIndex) {
+ if (selectedIndex < 0) {
+ throw new IllegalArgumentException(
+ "The item index must be larger than or equal to 0.");
+ }
+ this.mSelectedIndex = selectedIndex;
+ return this;
+ }
+
+ /**
+ * Sets the text to display if the list is empty.
+ *
+ * <p>If the list is empty and the app does not explicitly set the message with this
+ * method, the
+ * host will show a default message.
+ */
+ @NonNull
+ public Builder setNoItemsMessage(@Nullable CharSequence noItemsMessage) {
+ this.mNoItemsMessage = noItemsMessage == null ? null : CarText.create(noItemsMessage);
+ return this;
+ }
+
+ /**
+ * Adds an item to the list.
+ *
+ * @throws NullPointerException if {@code item} is {@code null}.
+ */
+ @NonNull
+ public Builder addItem(@NonNull Item item) {
+ mItems.add(requireNonNull(item));
+ return this;
+ }
+
+ /** Clears any items that may have been added up to this point. */
+ @NonNull
+ public Builder clearItems() {
+ mItems.clear();
+ return this;
+ }
+
+ /**
+ * Constructs the item list defined by this builder.
+ *
+ * @throws IllegalStateException if the list is selectable but does not have any items.
+ * @throws IllegalStateException if the selected index is greater or equal to the size of
+ * the
+ * list.
+ * @throws IllegalStateException if the list is selectable and any items have either one of
+ * their {@link OnClickListener} or {@link Toggle} set.
+ */
+ @NonNull
+ public ItemList build() {
+ if (mOnSelectedListener != null) {
+ int listSize = mItems.size();
+ if (listSize == 0) {
+ throw new IllegalStateException("A selectable list cannot be empty");
+ } else if (mSelectedIndex >= listSize) {
+ throw new IllegalStateException(
+ "The selected item index ("
+ + mSelectedIndex
+ + ") is larger than the size of the list ("
+ + listSize
+ + ")");
+ }
+
+ // Check that no items have disallowed elements if the list is selectable.
+ for (Object item : mItems) {
+ if (getOnClickListener(item) != null) {
+ throw new IllegalStateException(
+ "Items that belong to selectable lists can't have an "
+ + "onClickListener. Use the"
+ + " OnSelectedListener of the list instead");
+ }
+
+ if (getToggle(item) != null) {
+ throw new IllegalStateException(
+ "Items that belong to selectable lists can't have a toggle");
+ }
+ }
+ }
+
+ return new ItemList(this);
+ }
+ }
+
+ @Nullable
+ private static OnClickListenerWrapper getOnClickListener(Object item) {
+ if (item instanceof Row) {
+ return ((Row) item).getOnClickListener();
+ } else if (item instanceof GridItem) {
+ return ((GridItem) item).getOnClickListener();
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private static Toggle getToggle(Object item) {
+ if (item instanceof Row) {
+ return ((Row) item).getToggle();
+ } else if (item instanceof GridItem) {
+ return ((GridItem) item).getToggle();
+ }
+
+ return null;
+ }
+
+ @Keep // We need to keep these stub for Bundler serialization logic.
+ private static class OnSelectedListenerStub extends IOnSelectedListener.Stub {
+ private final OnSelectedListener mOnSelectedListener;
+
+ private OnSelectedListenerStub(OnSelectedListener onSelectedListener) {
+ this.mOnSelectedListener = onSelectedListener;
+ }
+
+ @Override
+ public void onSelected(int index, IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(
+ () -> mOnSelectedListener.onSelected(index), callback, "onSelectedListener");
+ }
+ }
+
+ /** Stub class for the {@link IOnItemVisibilityChangedListener} interface. */
+ @Keep // We need to keep these stub for Bundler serialization logic.
+ private static class OnItemVisibilityChangedListenerStub
+ extends IOnItemVisibilityChangedListener.Stub {
+ private final OnItemVisibilityChangedListener mOnItemVisibilityChangedListener;
+
+ private OnItemVisibilityChangedListenerStub(
+ OnItemVisibilityChangedListener onItemVisibilityChangedListener) {
+ this.mOnItemVisibilityChangedListener = onItemVisibilityChangedListener;
+ }
+
+ @Override
+ public void onItemVisibilityChanged(
+ int startIndexInclusive, int endIndexExclusive, IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(
+ () ->
+ mOnItemVisibilityChangedListener.onItemVisibilityChanged(
+ startIndexInclusive, endIndexExclusive),
+ callback,
+ "onItemVisibilityChanged");
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/LatLng.java b/car/app/app/src/main/java/androidx/car/app/model/LatLng.java
new file mode 100644
index 0000000..4164be5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/LatLng.java
@@ -0,0 +1,96 @@
+/*
+ * 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 java.util.Objects.hash;
+import static java.util.Objects.requireNonNull;
+
+import android.location.Location;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Represents a geographical location with a latitude and a longitude. */
+public final class LatLng {
+ @Keep
+ private final double mLat;
+ @Keep
+ private final double mLng;
+
+ /** Returns a new instance of a {@link LatLng}. */
+ @NonNull
+ public static LatLng create(double latitude, double longitude) {
+ return new LatLng(latitude, longitude);
+ }
+
+ /**
+ * Returns a new instance of a {@link LatLng} with the same latitude and longitude contained in
+ * the given {@link Location}.
+ *
+ * @throws NullPointerException if {@code location} is {@code null}.
+ */
+ @NonNull
+ public static LatLng create(@NonNull Location location) {
+ requireNonNull(location);
+ return create(location.getLatitude(), location.getLongitude());
+ }
+
+ /** Returns the latitude of the location, in degrees. */
+ public double getLatitude() {
+ return mLat;
+ }
+
+ /** Returns the longitude of the location, in degrees. */
+ public double getLongitude() {
+ return mLng;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + getLatitude() + ", " + getLongitude() + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return hash(mLat, mLng);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof LatLng)) {
+ return false;
+ }
+ LatLng otherLatLng = (LatLng) other;
+
+ return Double.doubleToLongBits(mLat) == Double.doubleToLongBits(otherLatLng.mLat)
+ && Double.doubleToLongBits(mLng) == Double.doubleToLongBits(otherLatLng.mLng);
+ }
+
+ private LatLng(double lat, double lng) {
+ this.mLat = lat;
+ this.mLng = lng;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private LatLng() {
+ this(0, 0);
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
new file mode 100644
index 0000000..d3da538
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
@@ -0,0 +1,428 @@
+/*
+ * 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 androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_FULL_LIST;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.Screen;
+import androidx.car.app.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template representing a list of items.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this
+ * template is considered a refresh of a previous one if:
+ *
+ * <ul>
+ * <li>The template title has not changed, and
+ * <li>The previous template is in a loading state (see {@link Builder#setLoading}}, or the
+ * {@link ItemList} structure between the templates have not changed. This means that if the
+ * previous template has multiple {@link ItemList} sections, the new template must have the
+ * same number of sections with the same headers. Further, the number of rows and the string
+ * contents (title, texts, not counting spans) of each row must not have changed.
+ * <li>For rows that contain a {@link Toggle}, updates to the title or texts are also allowed if
+ * the toggle state has changed between the previous and new templates.
+ * </ul>
+ */
+public final class ListTemplate implements Template {
+ @Keep
+ private final boolean mIsLoading;
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ItemList mSingleList;
+ @Keep
+ private final List<SectionedItemList> mSectionLists;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+
+ /** Constructs a new builder of {@link ListTemplate}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @Nullable
+ public ItemList getSingleList() {
+ return mSingleList;
+ }
+
+ @NonNull
+ public List<SectionedItemList> getSectionLists() {
+ return mSectionLists;
+ }
+
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ requireNonNull(oldTemplate);
+
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ ListTemplate old = (ListTemplate) oldTemplate;
+
+ if (!Objects.equals(old.getTitle(), getTitle())) {
+ return false;
+ }
+
+ if (old.mIsLoading) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (mIsLoading) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ if (mSingleList != null && old.mSingleList != null) {
+ return mSingleList.isRefresh(old.mSingleList, logger);
+ } else {
+ if (mSectionLists.size() != old.mSectionLists.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < mSectionLists.size(); i++) {
+ SectionedItemList section = mSectionLists.get(i);
+ SectionedItemList oldSection = old.mSectionLists.get(i);
+
+ if (!section.getHeader().equals(oldSection.getHeader())
+ || !section.getItemList().isRefresh(oldSection.getItemList(), logger)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ListTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsLoading, mTitle, mHeaderAction, mSingleList, mSectionLists,
+ mActionStrip);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ListTemplate)) {
+ return false;
+ }
+ ListTemplate otherTemplate = (ListTemplate) other;
+
+ return mIsLoading == otherTemplate.mIsLoading
+ && Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mSingleList, otherTemplate.mSingleList)
+ && Objects.equals(mSectionLists, otherTemplate.mSectionLists)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+ }
+
+ private ListTemplate(Builder builder) {
+ mIsLoading = builder.mIsLoading;
+ mTitle = builder.mTitle;
+ mHeaderAction = builder.mHeaderAction;
+ mSingleList = builder.mSingleList;
+ mSectionLists = new ArrayList<>(builder.mSectionLists);
+ mActionStrip = builder.mActionStrip;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private ListTemplate() {
+ mIsLoading = false;
+ mTitle = null;
+ mHeaderAction = null;
+ mSingleList = null;
+ mSectionLists = Collections.emptyList();
+ mActionStrip = null;
+ }
+
+ /** A builder of {@link ListTemplate}. */
+ public static final class Builder {
+ private boolean mIsLoading;
+ @Nullable
+ private ItemList mSingleList;
+ private final List<SectionedItemList> mSectionLists = new ArrayList<>();
+ @Nullable
+ private CarText mTitle;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+ private boolean mHasSelectableList;
+
+ /**
+ * Sets whether the template is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+ * would be otherwise. The caller is expected to call {@link
+ * androidx.car.app.Screen#invalidate()} and send the new template content
+ * to the host once the data is ready.
+ *
+ * <p>If set to {@code false}, the UI will display the contents of the {@link ItemList}
+ * instance(s) added via {@link #setSingleList} or {@link #addList}.
+ */
+ @NonNull
+ public Builder setLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null}
+ * to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either one of {@link Action#APP_ICON} and
+ * {@link Action#BACK} as
+ * a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+ * show a
+ * title.
+ */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /** Resets any list(s) that were added via {@link #setSingleList} or {@link #addList}. */
+ @NonNull
+ public Builder clearAllLists() {
+ mSingleList = null;
+ mSectionLists.clear();
+ mHasSelectableList = false;
+ return this;
+ }
+
+ /**
+ * Sets a single {@link ItemList} to show in the template.
+ *
+ * <p>Note that this list cannot be mixed with others added via {@link #addList}. If
+ * multiple
+ * lists were previously added, they will be cleared.
+ *
+ * @throws NullPointerException if {@code list} is null.
+ * @see #addList(ItemList, CharSequence)
+ */
+ @NonNull
+ public Builder setSingleList(@NonNull ItemList list) {
+ mSingleList = requireNonNull(list);
+ mSectionLists.clear();
+ mHasSelectableList = false;
+ return this;
+ }
+
+ /**
+ * Adds an {@link ItemList} to display in the template.
+ *
+ * <p>Use this method to add multiple {@link ItemList}s to the template. Each
+ * {@link ItemList}
+ * will be grouped under the given {@code header}. These lists cannot be mixed with an
+ * {@link
+ * ItemList} added via {@link #setSingleList}. If a single list was previously added, it
+ * will be
+ * cleared.
+ *
+ * <p>If the added {@link ItemList} contains a {@link ItemList.OnSelectedListener}, then it
+ * cannot be added alongside other {@link ItemList}(s).
+ *
+ * @throws NullPointerException if {@code list} is null.
+ * @throws IllegalArgumentException if {@code list} is empty.
+ * @throws IllegalArgumentException if {@code list}'s {@link
+ * ItemList.OnItemVisibilityChangedListener} is set.
+ * @throws NullPointerException if {@code header} is null.
+ * @throws IllegalArgumentException if {@code header} is empty.
+ * @throws IllegalArgumentException if a selectable list is added alongside other lists.
+ */
+ @NonNull
+ // TODO(shiufai): consider rename to match getter's name.
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder addList(@NonNull ItemList list, @NonNull CharSequence header) {
+ if (requireNonNull(header).length() == 0) {
+ throw new IllegalArgumentException("Header cannot be empty");
+ }
+ CarText headerText = CarText.create(header);
+
+ boolean isSelectableList = list.getOnSelectedListener() != null;
+ if (mHasSelectableList || (isSelectableList && !mSectionLists.isEmpty())) {
+ throw new IllegalArgumentException(
+ "A selectable list cannot be added alongside any other lists");
+ }
+ mHasSelectableList = isSelectableList;
+
+ if (list.getItems().isEmpty()) {
+ throw new IllegalArgumentException("List cannot be empty");
+ }
+
+ if (list.getOnItemsVisibilityChangeListener() != null) {
+ throw new IllegalArgumentException(
+ "OnItemVisibilityChangedListener in the list is disallowed");
+ }
+
+ mSingleList = null;
+ mSectionLists.add(SectionedItemList.create(list, headerText));
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public Builder addListForTesting(@NonNull ItemList list, @NonNull CharSequence header) {
+ mSingleList = null;
+ mSectionLists.add(SectionedItemList.create(list, CarText.create(header)));
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+ * ActionStrip}.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+ * {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Constructs the template defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 6 {@link Row}s total in the {@link ItemList}(s). The host will
+ * ignore any items over that limit. Each {@link Row}s can add up to 2 lines of texts via
+ * {@link Row.Builder#addText}.
+ *
+ * <p>Either a header {@link Action} or the title must be set on the template.
+ *
+ * @throws IllegalStateException if the template is in a loading state but there are
+ * lists added, or vice versa.
+ * @throws IllegalArgumentException if the added {@link ItemList}(s) do not meet the
+ * template's requirements.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link Action} set.
+ */
+ @NonNull
+ public ListTemplate build() {
+ boolean hasList = mSingleList != null || !mSectionLists.isEmpty();
+ if (mIsLoading == hasList) {
+ throw new IllegalStateException(
+ "Template is in a loading state but lists are added, or vice versa");
+ }
+
+ if (hasList) {
+ if (!mSectionLists.isEmpty()) {
+ ROW_LIST_CONSTRAINTS_FULL_LIST.validateOrThrow(mSectionLists);
+ } else if (mSingleList != null) {
+ ROW_LIST_CONSTRAINTS_FULL_LIST.validateOrThrow(mSingleList);
+ }
+ }
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new ListTemplate(this);
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public ListTemplate buildForTesting() {
+ return new ListTemplate(this);
+ }
+
+ private Builder() {
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
new file mode 100644
index 0000000..90beda5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
@@ -0,0 +1,339 @@
+/*
+ * 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.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template for displaying a message and associated actions.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is
+ * considered a refresh of a previous one if the title and messages have not changed.
+ */
+public final class MessageTemplate implements Template {
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final CarText mMessage;
+ @Keep
+ @Nullable
+ private final CarText mDebugMessage;
+ @Keep
+ @Nullable
+ private final CarIcon mIcon;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ActionList mActionList;
+
+ /** Constructs a new builder of {@link MessageTemplate}. */
+ @NonNull
+ public static Builder builder(@NonNull CharSequence message) {
+ return new Builder(requireNonNull(message));
+ }
+
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @NonNull
+ public CarText getMessage() {
+ return Objects.requireNonNull(mMessage);
+ }
+
+ @Nullable
+ public CarText getDebugMessage() {
+ return mDebugMessage;
+ }
+
+ @Nullable
+ public CarIcon getIcon() {
+ return mIcon;
+ }
+
+ @Nullable
+ public ActionList getActionList() {
+ return mActionList;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ requireNonNull(oldTemplate);
+
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ MessageTemplate old = (MessageTemplate) oldTemplate;
+ return Objects.equals(old.getTitle(), getTitle())
+ && Objects.equals(old.getDebugMessage(), getDebugMessage())
+ && Objects.equals(old.getMessage(), getMessage());
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "MessageTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mMessage, mDebugMessage, mHeaderAction, mActionList, mIcon);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MessageTemplate)) {
+ return false;
+ }
+ MessageTemplate otherTemplate = (MessageTemplate) other;
+
+ return Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mMessage, otherTemplate.mMessage)
+ && Objects.equals(mDebugMessage, otherTemplate.mDebugMessage)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mActionList, otherTemplate.mActionList)
+ && Objects.equals(mIcon, otherTemplate.mIcon);
+ }
+
+ private MessageTemplate(Builder builder) {
+ mTitle = builder.mTitle;
+ mMessage = builder.mMessage;
+ mDebugMessage = builder.mDebugMessage;
+ mIcon = builder.mIcon;
+ mHeaderAction = builder.mHeaderAction;
+ mActionList = builder.mActionList;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private MessageTemplate() {
+ mTitle = null;
+ mMessage = null;
+ mDebugMessage = null;
+ mIcon = null;
+ mHeaderAction = null;
+ mActionList = null;
+ }
+
+ /** A builder of {@link MessageTemplate}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ private CarText mMessage;
+ @Nullable
+ private CarText mDebugMessage;
+ @Nullable
+ private CarIcon mIcon;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionList mActionList;
+ @Nullable
+ private Throwable mDebugCause;
+ @Nullable
+ private String mDebugString;
+
+ /**
+ * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+ * show a
+ * title.
+ */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets the {@link CharSequence} to display as the message in the template.
+ *
+ * @throws NullPointerException if {@code message} is null.
+ */
+ @NonNull
+ public Builder setMessage(@NonNull CharSequence message) {
+ this.mMessage = CarText.create(requireNonNull(message));
+ return this;
+ }
+
+ /**
+ * Sets a {@link Throwable} for debugging purposes, or {@code null} to not show it.
+ *
+ * <p>The cause will be displayed along with the message set in {@link #setDebugMessage}.
+ *
+ * <p>The host may choose to not display this debugging information if it doesn't deem it
+ * appropriate, for example, when running on a production environment rather than in a
+ * simulator
+ * such as the Desktop Head Unit.
+ */
+ @NonNull
+ // Suppress as the cause is transformed into a message before transport.
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setDebugCause(@Nullable Throwable cause) {
+ this.mDebugCause = cause;
+ return this;
+ }
+
+ /**
+ * Sets a debug message for debugging purposes, or {@code null} to not show a debug message.
+ *
+ * <p>The debug message will be displayed along with the cause set in
+ * {@link #setDebugCause}.
+ *
+ * <p>The host may choose to not display this debugging information if it doesn't deem it
+ * appropriate, for example, when running on a production environment rather than in a
+ * simulator
+ * such as the Desktop Head Unit.
+ */
+ @NonNull
+ public Builder setDebugMessage(@Nullable String debugMessage) {
+ this.mDebugString = debugMessage;
+ return this;
+ }
+
+ /**
+ * Sets the icon to be displayed along with the message, or {@code null} to not display any
+ * icons.
+ *
+ * <h4>Icon Sizing Guidance</h4>
+ *
+ * The provided icon should have a maximum size of 64 x 64 dp. If the icon exceeds this
+ * maximum
+ * size in either one of the dimensions, it will be scaled down and centered inside the
+ * bounding
+ * box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that
+ * work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setIcon(@Nullable CarIcon icon) {
+ CarIconConstraints.DEFAULT.validateOrThrow(icon);
+ this.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null}
+ * to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets a list of {@link Action}s to display along with the message.
+ *
+ * <p>Any actions above the maximum limit of 2 will be ignored.
+ *
+ * @throws NullPointerException if {@code actions} is {@code null}.
+ */
+ @NonNull
+ // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setActions(@NonNull List<Action> actions) {
+ mActionList = ActionList.create(requireNonNull(actions));
+ return this;
+ }
+
+ /**
+ * Constructs the {@link MessageTemplate} defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * A non-empty message must be set on the template with {@link
+ * Builder#setMessage(CharSequence)}.
+ *
+ * <p>Either a header {@link Action} or title must be set on the template.
+ *
+ * @throws IllegalStateException if the message is empty.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link
+ * Action} set.
+ */
+ @NonNull
+ public MessageTemplate build() {
+ if (mMessage.isEmpty()) {
+ throw new IllegalStateException("Message cannot be empty");
+ }
+
+ String debugString = this.mDebugString == null ? "" : this.mDebugString;
+ if (!debugString.isEmpty() && mDebugCause != null) {
+ debugString += "\n";
+ }
+ debugString += Log.getStackTraceString(mDebugCause);
+ if (!debugString.isEmpty()) {
+ mDebugMessage = CarText.create(debugString);
+ }
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new MessageTemplate(this);
+ }
+
+ private Builder(CharSequence message) {
+ this.mMessage = CarText.create(message);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Metadata.java b/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
new file mode 100644
index 0000000..429aab4
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
@@ -0,0 +1,122 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/** A metadata class used for attaching additional properties to models. */
+public class Metadata {
+ /** An empty {@link Metadata} instance. */
+ public static final Metadata EMPTY_METADATA = new Builder().build();
+
+ @Keep
+ @Nullable
+ private final Place mPlace;
+
+ /** Constructs a new builder of a {@link Metadata} instance. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Constructs a new instance of {@link Metadata} containing a {@link Place}.
+ *
+ * @throws NullPointerException if {@code place} is {@code null}.
+ * @see Builder#setPlace(Place)
+ */
+ @NonNull
+ public static Metadata ofPlace(@NonNull Place place) {
+ return new Builder().setPlace(requireNonNull(place)).build();
+ }
+
+ /** Returns a new {@link Builder} with the data from this {@link Metadata} instance. */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ @Nullable
+ public Place getPlace() {
+ return mPlace;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mPlace);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Metadata)) {
+ return false;
+ }
+ Metadata otherMetadata = (Metadata) other;
+
+ return Objects.equals(mPlace, otherMetadata.mPlace);
+ }
+
+ private Metadata(Builder builder) {
+ mPlace = builder.mPlace;
+ }
+
+ /** Default constructor for serialization. */
+ private Metadata() {
+ mPlace = null;
+ }
+
+ /** A builder for {@link Metadata}. */
+ public static final class Builder {
+ @Nullable
+ private Place mPlace;
+
+ /**
+ * Sets a {@link Place} used for showing {@link Distance} and {@link PlaceMarker}
+ * information,
+ * or {@code null} if no {@link Place} information is available.
+ */
+ @NonNull
+ public Builder setPlace(@Nullable Place place) {
+ this.mPlace = place;
+ return this;
+ }
+
+ /**
+ * Returns a {@link Metadata} instance defined by this builder.
+ */
+ @NonNull
+ public Metadata build() {
+ return new Metadata(this);
+ }
+
+ private Builder() {
+ }
+
+ private Builder(Metadata metadata) {
+ this.mPlace = metadata.mPlace;
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ModelUtils.java b/car/app/app/src/main/java/androidx/car/app/model/ModelUtils.java
new file mode 100644
index 0000000..240e097
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ModelUtils.java
@@ -0,0 +1,168 @@
+/*
+ * 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 android.text.style.CharacterStyle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.CarText.SpanWrapper;
+
+import java.util.List;
+
+/**
+ * Utility class for common operations on the car app models
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public final class ModelUtils {
+ /**
+ * Checks whether all non-browsable rows have attached at least one {@link DistanceSpan} in
+ * either the title or secondary text.
+ *
+ * @throws IllegalArgumentException if any non-browsable row does not have a
+ * {@link DistanceSpan} instance.
+ */
+ public static void validateAllNonBrowsableRowsHaveDistance(@NonNull List<Object> rows) {
+ int spanSetCount = 0;
+ int nonBrowsableRowCount = 0;
+ for (Object rowObj : rows) {
+ Row row = (Row) rowObj;
+
+ if (!row.isBrowsable()) {
+ nonBrowsableRowCount++;
+ }
+
+ if (checkRowHasSpanType(row, DistanceSpan.class)) {
+ spanSetCount++;
+ }
+ }
+
+ if (nonBrowsableRowCount > spanSetCount) {
+ throw new IllegalArgumentException(
+ "All non-browsable rows must have a distance span attached to either its "
+ + "title or texts");
+ }
+ }
+
+ /**
+ * Checks whether all rows have attached at least one {@link DurationSpan} or
+ * {@link DistanceSpan }in either the title or secondary text.
+ *
+ * @throws IllegalArgumentException if any non-browsable row does not have either a {@link
+ * DurationSpan} or {@link DistanceSpan} instance.
+ */
+ public static void validateAllRowsHaveDistanceOrDuration(@NonNull List<Object> rows) {
+ for (Object rowObj : rows) {
+ Row row = (Row) rowObj;
+ if (!(checkRowHasSpanType(row, DistanceSpan.class)
+ || checkRowHasSpanType(row, DurationSpan.class))) {
+ throw new IllegalArgumentException(
+ "All rows must have either a distance or duration span attached to either"
+ + " its title or"
+ + " texts");
+ }
+ }
+ }
+
+ /**
+ * Checks whether all rows have only small-sized images if they are set.
+ *
+ * @throws IllegalArgumentException if an image set in any rows is using {@link
+ * Row#IMAGE_TYPE_LARGE}.
+ */
+ public static void validateAllRowsHaveOnlySmallImages(@NonNull List<Object> rows) {
+ for (Object rowObj : rows) {
+ Row row = (Row) rowObj;
+ if (row.getImage() != null && row.getRowImageType() == Row.IMAGE_TYPE_LARGE) {
+ throw new IllegalArgumentException("Rows must only use small-sized images");
+ }
+ }
+ }
+
+ /**
+ * Checks whether any rows have both a marker and an image.
+ *
+ * @throws IllegalArgumentException if both a marker and an image are set in a row.
+ */
+ public static void validateNoRowsHaveBothMarkersAndImages(@NonNull List<Object> rows) {
+ for (Object rowObj : rows) {
+ Row row = (Row) rowObj;
+
+ boolean hasImage = row.getImage() != null;
+ Place place = row.getMetadata().getPlace();
+ boolean hasMarker = place != null && place.getMarker() != null;
+
+ if (hasImage && hasMarker) {
+ throw new IllegalArgumentException("Rows can't have both a marker and an image");
+ }
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given row has a span of the given type, {@code false} otherwise.
+ */
+ private static boolean checkRowHasSpanType(Row row, Class<? extends CharacterStyle> spanType) {
+ CarText title = row.getTitle();
+ if (checkCarTextHasSpanType(title, spanType)) {
+ return true;
+ }
+
+ List<CarText> texts = row.getTexts();
+ for (int i = 0; i < texts.size(); i++) {
+ CarText text = texts.get(i);
+ if (checkCarTextHasSpanType(text, spanType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if the given {@link CarText} has a span of the given type, {@code false}
+ * otherwise.
+ */
+ private static boolean checkCarTextHasSpanType(
+ CarText carText, Class<? extends CharacterStyle> spanType) {
+ if (carText.isEmpty()) {
+ return false;
+ }
+ String text = requireNonNull(carText.getText());
+
+ List<SpanWrapper> spans = carText.getSpans();
+ for (int i = 0; i < spans.size(); i++) {
+ SpanWrapper wrapper = spans.get(i);
+ Object spanObj = wrapper.span;
+ if (spanType.isInstance(spanObj)
+ && wrapper.start >= 0
+ && wrapper.start != wrapper.end
+ && wrapper.start < text.length()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private ModelUtils() {
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
new file mode 100644
index 0000000..f36ffca
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/** A listener of click events. */
+public interface OnClickListener {
+ /** Notifies that a click happened. */
+ void onClick();
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
new file mode 100644
index 0000000..789dd53
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
@@ -0,0 +1,96 @@
+/*
+ * 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 android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.utils.RemoteUtils;
+
+/**
+ * Internal state object to pass additional state along with the wrapped {@code IOnClickListener}.
+ */
+// TODO(shiufai): Replace code tag with the correct AIDL wrapper.
+public class OnClickListenerWrapper {
+
+ @Keep
+ @Nullable
+ private final IOnClickListener mListener;
+ @Keep
+ private final boolean mIsParkedOnly;
+
+ /**
+ * @hide
+ */
+ // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public IOnClickListener getListener() {
+ return requireNonNull(mListener);
+ }
+
+ /**
+ * Whether the click listener is for parked-only scenarios.
+ */
+ public boolean isParkedOnly() {
+ return mIsParkedOnly;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(LIBRARY)
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public static OnClickListenerWrapper create(@NonNull OnClickListener listener) {
+ return new OnClickListenerWrapper(
+ new OnClickListenerStub(listener), listener instanceof ParkedOnlyOnClickListener);
+ }
+
+ private OnClickListenerWrapper(IOnClickListener listener, boolean isParkedOnly) {
+ this.mListener = listener;
+ this.mIsParkedOnly = isParkedOnly;
+ }
+
+ /** For serialization. */
+ private OnClickListenerWrapper() {
+ mListener = null;
+ mIsParkedOnly = false;
+ }
+
+ @Keep // We need to keep these stub for Bundler serialization logic.
+ private static class OnClickListenerStub extends IOnClickListener.Stub {
+ private final OnClickListener mOnClickListener;
+
+ private OnClickListenerStub(OnClickListener onClickListener) {
+ this.mOnClickListener = onClickListener;
+ }
+
+ @Override
+ public void onClick(IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(mOnClickListener::onClick, callback, "onClick");
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Pane.java b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
new file mode 100644
index 0000000..0670466
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
@@ -0,0 +1,220 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.utils.Logger;
+import androidx.car.app.utils.ValidationUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of rows used for displaying informational content and a set of {@link Action}s
+ * that users can perform based on such content.
+ */
+public final class Pane {
+ @Keep
+ @Nullable
+ private final ActionList mActionList;
+ @Keep
+ private final List<Object> mRows;
+ @Keep
+ private final boolean mIsLoading;
+
+ /** Constructs a new builder of {@link Pane}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the list of {@link Action}s displayed alongside the {@link Row}s in this pane.
+ */
+ @Nullable
+ public ActionList getActionList() {
+ return mActionList;
+ }
+
+ /**
+ * Returns the list of {@link Row} objects that make up the {@link Pane}.
+ */
+ @NonNull
+ public List<Object> getRows() {
+ return mRows;
+ }
+
+ /**
+ * Returns the {@code true} if the {@link Pane} is loading.*
+ */
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ /**
+ * Returns {@code true} if this {@link Pane} instance is determined to be a refresh of the given
+ * pane, or {@code false} otherwise.
+ *
+ * <p>A pane is considered a refresh if:
+ *
+ * <ul>
+ * <li>The other pane is in a loading state, or
+ * <li>The row size and string contents of the two panes are the same.
+ * </ul>
+ */
+ public boolean isRefresh(@Nullable Pane other, @NonNull Logger logger) {
+ if (other == null) {
+ return false;
+ } else if (other.isLoading()) {
+ return true;
+ } else if (isLoading()) {
+ return false;
+ }
+
+ return ValidationUtils.itemsHaveSameContent(other.getRows(), getRows(), logger);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[ rows: "
+ + (mRows != null ? mRows.toString() : null)
+ + ", action list: "
+ + mActionList
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRows, mActionList, mIsLoading);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Pane)) {
+ return false;
+ }
+ Pane otherPane = (Pane) other;
+
+ return mIsLoading == otherPane.mIsLoading
+ && Objects.equals(mActionList, otherPane.mActionList)
+ && Objects.equals(mRows, otherPane.mRows);
+ }
+
+ private Pane(Builder builder) {
+ mRows = new ArrayList<>(builder.mRows);
+ mActionList = builder.mActionList;
+ mIsLoading = builder.mIsLoading;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Pane() {
+ mRows = Collections.emptyList();
+ mActionList = null;
+ mIsLoading = false;
+ }
+
+ /** A builder of {@link Pane}. */
+ public static final class Builder {
+ private final List<Object> mRows = new ArrayList<>();
+ @Nullable
+ private ActionList mActionList;
+ private boolean mIsLoading;
+
+ /**
+ * Sets whether the {@link Pane} is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+ * would be otherwise. The caller is expected to call {@link
+ * androidx.car.app.Screen#invalidate()} and send the new template content
+ * to the host once the data is ready. If set to {@code false}, the UI shows the actual row
+ * contents.
+ *
+ * @see #build
+ */
+ @NonNull
+ public Builder setLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Adds a row to display in the list.
+ *
+ * @throws NullPointerException if {@code row} is {@code null}.
+ */
+ @NonNull
+ public Builder addRow(@NonNull Row row) {
+ mRows.add(requireNonNull(row));
+ return this;
+ }
+
+ /** Clears any rows that may have been added with {@link #addRow(Row)} up to this point. */
+ @NonNull
+ public Builder clearRows() {
+ mRows.clear();
+ return this;
+ }
+
+ /**
+ * Sets multiple {@link Action}s to display alongside the rows in the pane.
+ *
+ * <p>By default, no actions are displayed.
+ *
+ * @throws NullPointerException if {@code actions} is {@code null}.
+ */
+ @NonNull
+ // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setActions(@NonNull List<Action> actions) {
+ mActionList = ActionList.create(requireNonNull(actions));
+ return this;
+ }
+
+ /**
+ * Constructs the row list defined by this builder.
+ *
+ * @throws IllegalStateException if the pane is in loading state and also contains rows, or
+ * vice-versa.
+ */
+ @NonNull
+ public Pane build() {
+ int size = size();
+ if (size > 0 == mIsLoading) {
+ throw new IllegalStateException(
+ "The pane is set to loading but is not empty, or vice versa");
+ }
+
+ return new Pane(this);
+ }
+
+ private int size() {
+ return mRows.size();
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
new file mode 100644
index 0000000..37597b6
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
@@ -0,0 +1,263 @@
+/*
+ * 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 androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_PANE;
+
+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.utils.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A template that displays a {@link Pane}.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * previous one if:
+ *
+ * <ul>
+ * <li>The template title has not changed, and
+ * <li>The previous template is in a loading state (see {@link Pane.Builder#setLoading}, or the
+ * number of rows and the string contents (title, texts, not counting spans) of each row
+ * between the previous and new {@link Pane}s have not changed.
+ * </ul>
+ */
+public final class PaneTemplate implements Template {
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final Pane mPane;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+
+ /**
+ * Constructs a new builder of {@link PaneTemplate}.
+ *
+ * @throws NullPointerException if {@code pane} is {@code null}
+ */
+ @NonNull
+ public static Builder builder(@NonNull Pane pane) {
+ return new Builder(requireNonNull(pane));
+ }
+
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @NonNull
+ public Pane getPane() {
+ return requireNonNull(mPane);
+ }
+
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ PaneTemplate old = (PaneTemplate) oldTemplate;
+ return Objects.equals(old.getTitle(), getTitle()) && getPane().isRefresh(old.getPane(),
+ logger);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "PaneTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mPane, mHeaderAction, mActionStrip);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PaneTemplate)) {
+ return false;
+ }
+ PaneTemplate otherTemplate = (PaneTemplate) other;
+
+ return Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mPane, otherTemplate.mPane)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+ }
+
+ private PaneTemplate(Builder builder) {
+ mTitle = builder.mTitle;
+ mPane = builder.mPane;
+ mHeaderAction = builder.mHeaderAction;
+ mActionStrip = builder.mActionStrip;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private PaneTemplate() {
+ mTitle = null;
+ mPane = null;
+ mHeaderAction = null;
+ mActionStrip = null;
+ }
+
+ /** A builder of {@link PaneTemplate}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ private Pane mPane;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+
+ private Builder(Pane pane) {
+ this.mPane = pane;
+ }
+
+ /**
+ * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+ * show a
+ * title.
+ */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null}
+ * to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Pane} to display in the template.
+ *
+ * @throws NullPointerException if {@code pane} is {@code null}.
+ */
+ @NonNull
+ public Builder setPane(@NonNull Pane pane) {
+ this.mPane = requireNonNull(pane);
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+ * {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}.
+ * Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Constructs the template defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Row}s and 2 {@link Action}s in the {@link Pane}.
+ * The host
+ * will ignore any rows over that limit. Each {@link Row}s can add up to 2 lines of texts
+ * via
+ * {@link Row.Builder#addText} and cannot contain either a {@link Toggle} or a {@link
+ * OnClickListener}.
+ *
+ * <p>Either a header {@link Action} or title must be set on the template.
+ *
+ * @throws IllegalArgumentException if the {@link Pane} does not meet the requirements.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link
+ * Action} set.
+ */
+ @NonNull
+ public PaneTemplate build() {
+ ROW_LIST_CONSTRAINTS_PANE.validateOrThrow(mPane);
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new PaneTemplate(this);
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public PaneTemplate buildForTesting() {
+ return new PaneTemplate(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ParkedOnlyOnClickListener.java b/car/app/app/src/main/java/androidx/car/app/model/ParkedOnlyOnClickListener.java
new file mode 100644
index 0000000..951e525
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ParkedOnlyOnClickListener.java
@@ -0,0 +1,68 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+
+/**
+ * An {@link OnClickListener} that wraps another one and executes its {@link #onClick} method only
+ * when the car is parked.
+ *
+ * <p>When the car is not parked, the handler won't be executed and the host will display a message
+ * to the user indicating that the action can only be used while parked.
+ *
+ * <p>Actions that direct the users to their phones must only execute while parked. This class
+ * should be used for wrapping any click listeners that invoke such actions.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * builder.setOnClickListener(ParkedOnlyOnClickListener.create(
+ * () -> myClickAction()));
+ * }</pre>
+ */
+// Lint check wants this to be renamed *Callback.
+@SuppressLint("ListenerInterface")
+public final class ParkedOnlyOnClickListener implements OnClickListener {
+ @Keep
+ private final OnClickListener mListener;
+
+ @Override
+ public void onClick() {
+ mListener.onClick();
+ }
+
+ /**
+ * Constructs a new instance of a {@link ParkedOnlyOnClickListener}.
+ *
+ * @throws NullPointerException if {@code listener} is {@code null}.
+ */
+ @NonNull
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public static ParkedOnlyOnClickListener create(@NonNull OnClickListener listener) {
+ return new ParkedOnlyOnClickListener(requireNonNull(listener));
+ }
+
+ private ParkedOnlyOnClickListener(OnClickListener listener) {
+ this.mListener = listener;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Place.java b/car/app/app/src/main/java/androidx/car/app/model/Place.java
new file mode 100644
index 0000000..cdef8bd
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Place.java
@@ -0,0 +1,144 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/** Represents a geographical location and additional information on how to display it. */
+public class Place {
+ @Keep
+ @Nullable
+ private final LatLng mLatLng;
+ @Keep
+ @Nullable
+ private final PlaceMarker mMarker;
+
+ /**
+ * Create a builder for a {@link Place} instance.
+ *
+ * @param latLng the geographical location associated with the place.
+ * @throws NullPointerException if {@code latLng} is {@code null}.
+ */
+ @NonNull
+ public static Builder builder(@NonNull LatLng latLng) {
+ return new Builder(requireNonNull(latLng));
+ }
+
+ /** Returns a {@link Builder} instance with the same data as this {@link Place} instance. */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ @Nullable
+ public PlaceMarker getMarker() {
+ return mMarker;
+ }
+
+ @NonNull
+ public LatLng getLatLng() {
+ return requireNonNull(mLatLng);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[ latlng: " + mLatLng + ", marker: " + mMarker + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mLatLng, mMarker);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Place)) {
+ return false;
+ }
+ Place otherPlace = (Place) other;
+
+ return Objects.equals(mLatLng, otherPlace.mLatLng) && Objects.equals(mMarker,
+ otherPlace.mMarker);
+ }
+
+ private Place(Builder builder) {
+ mLatLng = builder.mLatLng;
+ mMarker = builder.mMarker;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Place() {
+ mLatLng = null;
+ mMarker = null;
+ }
+
+ /** A builder of {@link Place}. */
+ public static final class Builder {
+ private LatLng mLatLng;
+ @Nullable
+ private PlaceMarker mMarker;
+
+ private Builder(LatLng latLng) {
+ this.mLatLng = latLng;
+ }
+
+ private Builder(Place place) {
+ mLatLng = requireNonNull(place.mLatLng);
+ mMarker = place.mMarker;
+ }
+
+ /**
+ * Sets the geographical location associated with this place.
+ *
+ * @throws NullPointerException if {@code latLng} is {@code null}.
+ */
+ @NonNull
+ public Builder setLatLng(@NonNull LatLng latLng) {
+ this.mLatLng = requireNonNull(latLng);
+ return this;
+ }
+
+ /**
+ * Sets the {@link PlaceMarker} that specifies how this place is to be displayed on a
+ * map, or
+ * {@code null} to not display a marker for this place.
+ *
+ * <p>By default and unless otherwise set in this method, a marker will not be displayed.
+ */
+ @NonNull
+ public Builder setMarker(@Nullable PlaceMarker marker) {
+ this.mMarker = marker;
+ return this;
+ }
+
+ /** Constructs the {@link Place} defined by this builder. */
+ @NonNull
+ public Place build() {
+ return new Place(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
new file mode 100644
index 0000000..1cec76f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
@@ -0,0 +1,404 @@
+/*
+ * 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 androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+
+import static java.util.Objects.requireNonNull;
+
+import android.Manifest.permission;
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template that displays a map along with a list of places.
+ *
+ * <p>The map can display markers corresponding to the places in the list. See {@link
+ * Builder#setItemList} for details.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * previous one if:
+ *
+ * <ul>
+ * <li>The template title has not changed, and
+ * <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
+ * number of rows and the string contents (title, texts, not counting spans) of each row
+ * between the previous and new {@link ItemList}s have not changed.
+ * </ul>
+ */
+public final class PlaceListMapTemplate implements Template {
+ @Keep
+ private final boolean mIsLoading;
+ @Keep
+ private final boolean mShowCurrentLocation;
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final ItemList mItemList;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+ @Keep
+ @Nullable
+ private final Place mAnchor;
+
+ /** Constructs a new builder of {@link PlaceListMapTemplate}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public boolean isCurrentLocationEnabled() {
+ return mShowCurrentLocation;
+ }
+
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Nullable
+ public ItemList getItemList() {
+ return mItemList;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ @Nullable
+ public Place getAnchor() {
+ return mAnchor;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ requireNonNull(oldTemplate);
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ PlaceListMapTemplate old = (PlaceListMapTemplate) oldTemplate;
+ if (!Objects.equals(old.getTitle(), getTitle())) {
+ return false;
+ }
+
+ if (old.mIsLoading) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (mIsLoading) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
+ }
+
+ @Override
+ public void checkPermissions(@NonNull Context context) {
+ if (isCurrentLocationEnabled()) {
+ CarAppPermission.checkHasPermission(context, permission.ACCESS_FINE_LOCATION);
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "PlaceListMapTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mShowCurrentLocation, mIsLoading, mTitle, mItemList, mHeaderAction, mActionStrip,
+ mAnchor);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PlaceListMapTemplate)) {
+ return false;
+ }
+ PlaceListMapTemplate otherTemplate = (PlaceListMapTemplate) other;
+
+ return mShowCurrentLocation == otherTemplate.mShowCurrentLocation
+ && mIsLoading == otherTemplate.mIsLoading
+ && Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mItemList, otherTemplate.mItemList)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+ && Objects.equals(mAnchor, otherTemplate.mAnchor);
+ }
+
+ private PlaceListMapTemplate(Builder builder) {
+ mShowCurrentLocation = builder.mShowCurrentLocation;
+ mIsLoading = builder.mIsLoading;
+ mTitle = builder.mTitle;
+ mItemList = builder.mItemList;
+ mHeaderAction = builder.mHeaderAction;
+ mActionStrip = builder.mActionStrip;
+ mAnchor = builder.mAnchor;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private PlaceListMapTemplate() {
+ mShowCurrentLocation = false;
+ mIsLoading = false;
+ mTitle = null;
+ mItemList = null;
+ mHeaderAction = null;
+ mActionStrip = null;
+ mAnchor = null;
+ }
+
+ /** A builder of {@link PlaceListMapTemplate}. */
+ public static final class Builder {
+ private boolean mShowCurrentLocation;
+ private boolean mIsLoading;
+ @Nullable
+ private CarText mTitle;
+ @Nullable
+ private ItemList mItemList;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+ @Nullable
+ private Place mAnchor;
+
+ /**
+ * Sets whether to show the current location in the map.
+ *
+ * <p>The map template will show the user's current location on the map, which is normally
+ * indicated by a blue dot.
+ *
+ * <p>This functionality requires the app to have the {@code ACCESS_FINE_LOCATION}
+ * permission.
+ */
+ @NonNull
+ public Builder setCurrentLocationEnabled(boolean isEnabled) {
+ this.mShowCurrentLocation = isEnabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the template is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+ * would be otherwise. The caller is expected to call {@link
+ * androidx.car.app.Screen#invalidate()} and send the new template content
+ * to the host once the data is ready. If set to {@code false}, the UI shows the {@link
+ * ItemList} contents added via {@link #setItemList}.
+ */
+ @NonNull
+ public Builder setLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null}
+ * to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+ * display
+ * a title.
+ */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets an {@link ItemList} to show in a list view along with the map, or {@code null} to
+ * not
+ * display a list.
+ *
+ * <p>To show a marker corresponding to a point of interest represented by a row, set the
+ * {@link
+ * Place} instance via {@link Row.Builder#setMetadata}. The host will display the {@link
+ * PlaceMarker} in both the map and the list view as the row becomes visible.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
+ * ignore any
+ * items over that limit. The list itself cannot be selectable as set via {@link
+ * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
+ * {@link
+ * Row.Builder#addText} and cannot contain a {@link Toggle}.
+ *
+ * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
+ *
+ * <p>Rows are not allowed to have both and an image and a place marker.
+ *
+ * <p>All non-browsable rows must have a {@link DistanceSpan} attached to either its
+ * title or
+ * texts to indicate the distance of the point of interest from the current location. A
+ * row is
+ * browsable when it's configured like so with {@link Row.Builder#setBrowsable(boolean)}.
+ *
+ * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setItemList(@Nullable ItemList itemList) {
+ if (itemList != null) {
+ List<Object> items = itemList.getItems();
+ ROW_LIST_CONSTRAINTS_SIMPLE.validateOrThrow(itemList);
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(items);
+ ModelUtils.validateAllRowsHaveOnlySmallImages(items);
+ ModelUtils.validateNoRowsHaveBothMarkersAndImages(items);
+ }
+ this.mItemList = itemList;
+
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public Builder setItemListForTesting(@Nullable ItemList itemList) {
+ this.mItemList = itemList;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+ * ActionStrip}.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+ * {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}.
+ * Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Sets the anchor maker on the map, or {@code null} to not display an anchor marker.
+ *
+ * <p>The anchor marker is displayed differently from other markers by the host.
+ *
+ * <p>If not {@code null}, an anchor marker will be shown at the specified {@link LatLng}
+ * on the
+ * map. The camera will adapt to always have the anchor marker visible within its viewport,
+ * along with other places' markers from {@link Row} that are currently visible in the
+ * {@link
+ * Pane}. This can be used to provide a reference point on the map (e.g. the center of a
+ * search
+ * region) as the user pages through the {@link Pane}'s markers, for example.
+ */
+ @NonNull
+ public Builder setAnchor(@Nullable Place anchor) {
+ this.mAnchor = anchor;
+ return this;
+ }
+
+ /**
+ * Constructs the template defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * Either a header {@link Action} or title must be set on the template.
+ *
+ * @throws IllegalArgumentException if the template is in a loading state but the list is
+ * set,
+ * or vice versa.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link Action} set.
+ */
+ @NonNull
+ public PlaceListMapTemplate build() {
+ boolean hasList = mItemList != null;
+ if (mIsLoading == hasList) {
+ throw new IllegalArgumentException(
+ "Template is in a loading state but a list is set, or vice versa.");
+ }
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new PlaceListMapTemplate(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
new file mode 100644
index 0000000..173ea25
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
@@ -0,0 +1,296 @@
+/*
+ * 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 androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarColorConstraints;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Describes how a place is to be displayed on a map. */
+public class PlaceMarker {
+ /**
+ * Describes the type of image a marker icon represents.
+ *
+ * @hide
+ */
+ @IntDef(value = {TYPE_ICON, TYPE_IMAGE})
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY)
+ public @interface MarkerIconType {
+ }
+
+ /**
+ * Represents a marker icon.
+ *
+ * <p>Icons always have a tint applied to them.
+ */
+ public static final int TYPE_ICON = 0;
+
+ /**
+ * Represents a marker image.
+ *
+ * <p>No background will be applied.
+ */
+ public static final int TYPE_IMAGE = 1;
+
+ private static final PlaceMarker DEFAULT_INSTANCE = PlaceMarker.builder().build();
+ private static final int MAX_LABEL_LENGTH = 3;
+
+ @Keep
+ @Nullable
+ private final CarIcon mIcon;
+ @Keep
+ @Nullable
+ private final CarText mLabel;
+ @Keep
+ @Nullable
+ private final CarColor mColor;
+ @Keep
+ @MarkerIconType
+ private final int mIconType;
+
+ /**
+ * Returns an instance of {@link PlaceMarker} that uses the default values of the attributes
+ * specified through a {@link Builder}.
+ */
+ @NonNull
+ public static PlaceMarker getDefault() {
+ return DEFAULT_INSTANCE;
+ }
+
+ /**
+ * Returns a {@link Builder} for a {@link PlaceMarker}.
+ */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns {@code true} if {@code marker} is a default marker, {@code false} otherwise.
+ */
+ public static boolean isDefaultMarker(@Nullable PlaceMarker marker) {
+ return marker != null && marker.getIcon() == null && marker.getLabel() == null;
+ }
+
+ /**
+ * Returns the {@link CarIcon} associated with this marker.
+ */
+ @Nullable
+ public CarIcon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Returns the type of icon used with this marker.
+ */
+ @MarkerIconType
+ public int getIconType() {
+ return mIconType;
+ }
+
+ /**
+ * If set, the text that should be rendered as the marker's content, {@code null} otherwise.
+ *
+ * <p>Note that a {@link PlaceMarker} can only display either an icon or a text label. If
+ * both are
+ * set, then {@link #getIcon()} will take precedence.
+ */
+ @Nullable
+ public CarText getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Returns the marker color or {@code null} if not set.
+ *
+ * <p>See {@link Builder#setColor} on rules related to how the color is applied.
+ */
+ @Nullable
+ public CarColor getColor() {
+ return mColor;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "["
+ + (mIcon != null
+ ? mIcon.toString()
+ : mLabel != null ? CarText.toShortString(mLabel) : super.toString())
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIcon, mLabel, mColor, mIconType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PlaceMarker)) {
+ return false;
+ }
+ PlaceMarker otherMarker = (PlaceMarker) other;
+
+ return Objects.equals(mIcon, otherMarker.mIcon)
+ && Objects.equals(mLabel, otherMarker.mLabel)
+ && Objects.equals(mColor, otherMarker.mColor)
+ && mIconType == otherMarker.mIconType;
+ }
+
+ private PlaceMarker(@NonNull Builder builder) {
+ mIcon = builder.mIcon;
+ mIconType = builder.mIconType;
+ mLabel = builder.mLabel;
+ mColor = builder.mColor;
+ }
+
+ /** Private empty constructor used by serialization code. */
+ private PlaceMarker() {
+ mIcon = null;
+ mIconType = TYPE_ICON;
+ mLabel = null;
+ mColor = null;
+ }
+
+ /** A builder of {@link PlaceMarker}. */
+ public static final class Builder {
+ @Nullable
+ private CarIcon mIcon;
+ @Nullable
+ private CarText mLabel;
+ @Nullable
+ private CarColor mColor;
+ @MarkerIconType
+ private int mIconType = TYPE_ICON;
+
+ /**
+ * Sets the icon to display in the marker, or {@code null} to not display one.
+ *
+ * <p>If a label is specified with {@link #setLabel}, the icon will take precedence over it.
+ *
+ * <h4>Icon Sizing Guidance</h4>
+ *
+ * <ul>
+ * <li>For {@link #TYPE_IMAGE}, the provided image should be 36 x 36 dp. The host
+ * applies 4 dp rounded corners before the icon is rendered on either the map or
+ * the list.
+ * <li>For {@link #TYPE_ICON}, the provided icon should be 32 x 32 dp and have its tint
+ * value set via {@link CarIcon.Builder#setTint}. Otherwise, a default tint color as
+ * determined by the host will be applied.
+ * </ul>
+ *
+ * <p>If the size of the provided icon exceeds the size requirements described above in
+ * either one of the dimensions, it will be scaled down and centered inside the bounding
+ * box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ *
+ * @param icon the {@link CarIcon} to display inside the marker.
+ * @param iconType one of {@link #TYPE_ICON} or {@link #TYPE_IMAGE}.
+ */
+ @NonNull
+ public Builder setIcon(@Nullable CarIcon icon, @MarkerIconType int iconType) {
+ CarIconConstraints.DEFAULT.validateOrThrow(icon);
+ this.mIcon = icon;
+ this.mIconType = iconType;
+ return this;
+ }
+
+ /**
+ * Sets the text that should be displayed as the marker's content.
+ *
+ * <p>If an icon is specified with {@link #setIcon}, the icon will take precedence.
+ *
+ * @param label the text to display inside of the marker. The string must have a maximum
+ * size of 3 characters. Set to {@code null} to let the host choose a
+ * labelling scheme (for example, using a sequence of numbers).
+ */
+ @NonNull
+ public Builder setLabel(@Nullable CharSequence label) {
+ if (label != null && label.length() > MAX_LABEL_LENGTH) {
+ throw new IllegalArgumentException(
+ "Marker label cannot contain more than " + MAX_LABEL_LENGTH
+ + " characters");
+ }
+
+ this.mLabel = label == null ? null : CarText.create(label);
+ return this;
+ }
+
+ /**
+ * Sets the color that should be used for the marker on the map.
+ *
+ * <p>This color is applied in the following cases:
+ *
+ * <ul>
+ * <li>When the {@link PlaceMarker} is displayed on the map, the pin enclosing the icon or
+ * label will be painted using the given color.
+ * <li>When the {@link PlaceMarker} is displayed on the list, the color will be applied
+ * if the content is a label. A label rendered inside a map's pin cannot be color
+ * and will always use the default color as chosen by the host.
+ * </ul>
+ *
+ * <p>When this is set to {@code null}, the host will use a default color. The host may also
+ * ignore this color and use the default instead if the color does not pass the contrast
+ * requirements.
+ *
+ * <p>A color cannot be set if the marker's icon type is of {@link #TYPE_IMAGE}.
+ */
+ @NonNull
+ public Builder setColor(@Nullable CarColor color) {
+ if (color != null) {
+ CarColorConstraints.UNCONSTRAINED.validateOrThrow(color);
+ }
+ this.mColor = color;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link PlaceMarker} defined by this builder.
+ *
+ * @throws IllegalStateException if the icon is of the type {@link #TYPE_IMAGE} and a a
+ * color is set.
+ */
+ @NonNull
+ public PlaceMarker build() {
+ if (mColor != null && (mIcon != null && mIconType == TYPE_IMAGE)) {
+ throw new IllegalStateException("Color cannot be set for icon set with TYPE_IMAGE");
+ }
+
+ return new PlaceMarker(this);
+ }
+
+ private Builder() {
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
new file mode 100644
index 0000000..9379c55
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -0,0 +1,588 @@
+/*
+ * 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 androidx.car.app.model.Metadata.EMPTY_METADATA;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a row with a title, several lines of text, an optional image, and an optional action
+ * or switch.
+ */
+public class Row implements Item {
+ /**
+ * Represents flags that control some attributes of the row.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @RestrictTo(LIBRARY)
+ @IntDef(
+ value = {ROW_FLAG_NONE, ROW_FLAG_SHOW_DIVIDERS, ROW_FLAG_SECTION_HEADER},
+ flag = true)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RowFlags {
+ }
+
+ /**
+ * The type of images supported within rows.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @RestrictTo(LIBRARY)
+ @IntDef(value = {IMAGE_TYPE_SMALL, IMAGE_TYPE_ICON, IMAGE_TYPE_LARGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RowImageType {
+ }
+
+ /**
+ * No flags applied to the row.
+ */
+ public static final int ROW_FLAG_NONE = (1 << 0);
+
+ /**
+ * Whether to show dividers around the row.
+ */
+ public static final int ROW_FLAG_SHOW_DIVIDERS = (1 << 1);
+
+ /**
+ * Whether the row is a section header.
+ *
+ * <p>Sections are used to group rows in the UI, for example, by showing them all within a block
+ * of the same background color.
+ *
+ * <p>A section header is a string of text above the section with a title for it.
+ */
+ public static final int ROW_FLAG_SECTION_HEADER = (1 << 2);
+
+ /**
+ * Represents a small image to be displayed in the row.
+ *
+ * <p>If necessary, small images will be scaled down to fit within a 36 x 36 dp bounding box,
+ * preserving their aspect ratio.
+ */
+ public static final int IMAGE_TYPE_SMALL = (1 << 0);
+
+ /**
+ * Represents a large image to be displayed in the row.
+ *
+ * <p>If necessary, large images will be scaled down to fit within a 64 x 64 dp bounding box,
+ * preserving their aspect ratio.
+ */
+ public static final int IMAGE_TYPE_LARGE = (1 << 1);
+
+ /**
+ * Represents a small image to be displayed in the row.
+ *
+ * <p>If necessary, icons will be scaled down to fit within a 44 x 44 dp bounding box,
+ * preserving
+ * their aspect ratios.
+ *
+ * <p>A tint color is expected to be provided via {@link CarIcon.Builder#setTint}. Otherwise, a
+ * default tint color as determined by the host will be applied.
+ */
+ public static final int IMAGE_TYPE_ICON = (1 << 2);
+
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final List<CarText> mTexts;
+ @Keep
+ @Nullable
+ private final CarIcon mImage;
+ @Keep
+ @Nullable
+ private final Toggle mToggle;
+ @Keep
+ @Nullable
+ private final OnClickListenerWrapper mOnClickListener;
+ @Keep
+ private final Metadata mMetadata;
+ @Keep
+ @RowFlags
+ private final int mFlags;
+ @Keep
+ private final boolean mIsBrowsable;
+ @Keep
+ @RowImageType
+ private final int mRowImageType;
+
+ /** Constructs a new builder of {@link Row}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Returns the title of the row. */
+ @NonNull
+ public CarText getTitle() {
+ return requireNonNull(mTitle);
+ }
+
+ /** Returns the list of text below the title. */
+ @NonNull
+ public List<CarText> getTexts() {
+ Objects.requireNonNull(mTexts);
+ return mTexts;
+ }
+
+ /** Returns the image of the row. */
+ @Nullable
+ public CarIcon getImage() {
+ return mImage;
+ }
+
+ /** Returns the type of the image in the row. */
+ @RowImageType
+ public int getRowImageType() {
+ return mRowImageType;
+ }
+
+ /**
+ * Returns the {@link Toggle} in the row or {@code null} if the row does not contain a
+ * toggle.
+ */
+ @Nullable
+ public Toggle getToggle() {
+ return mToggle;
+ }
+
+ /**
+ * Returns {@code true} if the row is browsable, {@code false} otherwise.
+ *
+ * <p>If a row is browsable, then no {@link Action} or {@link Toggle} can be added to it.
+ */
+ public boolean isBrowsable() {
+ return mIsBrowsable;
+ }
+
+ /**
+ * Returns the {@link OnClickListener} to be called back when the row is clicked, or {@code
+ * null} if the row is non-clickable.
+ */
+ @Nullable
+ public OnClickListenerWrapper getOnClickListener() {
+ return mOnClickListener;
+ }
+
+ /**
+ * Returns the {@link Metadata} associated with the row.
+ */
+ @NonNull
+ public Metadata getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Returns the flags for the row.
+ */
+ @RowFlags
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Rows your boat.
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@code
+ * row.row().row().yourBoat(); // gently down the stream
+ * }</pre>
+ */
+ public void yourBoat() {
+ }
+
+ /** Returns a {@link Row} for rowing {@link #yourBoat()} */
+ @NonNull
+ public Row row() {
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[title: "
+ + CarText.toShortString(mTitle)
+ + ", text count: "
+ + (mTexts != null ? mTexts.size() : 0)
+ + ", image: "
+ + mImage
+ + ", isBrowsable: "
+ + mIsBrowsable
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTitle,
+ mTexts,
+ mImage,
+ mToggle,
+ mOnClickListener == null,
+ mMetadata,
+ mFlags,
+ mIsBrowsable,
+ mRowImageType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Row)) {
+ return false;
+ }
+ Row otherRow = (Row) other;
+
+ // Don't compare listener, only the fact whether it's present.
+ return Objects.equals(mTitle, otherRow.mTitle)
+ && Objects.equals(mTexts, otherRow.mTexts)
+ && Objects.equals(mImage, otherRow.mImage)
+ && Objects.equals(mToggle, otherRow.mToggle)
+ && Objects.equals(mOnClickListener == null, otherRow.mOnClickListener == null)
+ && Objects.equals(mMetadata, otherRow.mMetadata)
+ && mFlags == otherRow.mFlags
+ && mIsBrowsable == otherRow.mIsBrowsable
+ && mRowImageType == otherRow.mRowImageType;
+ }
+
+ private Row(Builder builder) {
+ mTitle = builder.mTitle;
+ mTexts = new ArrayList<>(builder.mTexts);
+ mImage = builder.mImage;
+ mToggle = builder.mToggle;
+ mOnClickListener = builder.mOnClickListener;
+ mMetadata = builder.mMetadata;
+ mIsBrowsable = builder.mIsBrowsable;
+ mFlags = builder.mFlags;
+ mRowImageType = builder.mRowImageType;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Row() {
+ mTitle = null;
+ mTexts = null;
+ mImage = null;
+ mToggle = null;
+ mOnClickListener = null;
+ mMetadata = EMPTY_METADATA;
+ mIsBrowsable = false;
+ mFlags = ROW_FLAG_NONE;
+ mRowImageType = IMAGE_TYPE_SMALL;
+ }
+
+ /** A builder of {@link Row}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ private final List<CarText> mTexts = new ArrayList<>();
+ @Nullable
+ private CarIcon mImage;
+ @Nullable
+ private Toggle mToggle;
+ @Nullable
+ private OnClickListenerWrapper mOnClickListener;
+ private Metadata mMetadata = EMPTY_METADATA;
+ private boolean mIsBrowsable;
+ @RowFlags
+ private int mFlags = ROW_FLAG_NONE;
+ @RowImageType
+ private int mRowImageType = IMAGE_TYPE_SMALL;
+
+ /**
+ * Sets the title of the row.
+ *
+ * @throws NullPointerException if {@code title} is {@code null}.
+ * @throws IllegalArgumentException if {@code title} is empty.
+ */
+ @NonNull
+ public Builder setTitle(@NonNull CharSequence title) {
+ CarText titleText = CarText.create(requireNonNull(title));
+ if (titleText.isEmpty()) {
+ throw new IllegalArgumentException("The title cannot be null or empty");
+ }
+ this.mTitle = titleText;
+ return this;
+ }
+
+ /**
+ * Sets the title of the row, or {@code null} to not show a title.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public Builder setTitle(@Nullable CarText title) {
+ this.mTitle = title;
+ return this;
+ }
+
+ /**
+ * Adds a text string to the row below the title.
+ *
+ * <p>The text's color can be customized with {@link ForegroundCarColorSpan} instances.
+ *
+ * <p>Most templates allow up to 2 text strings, but this may vary. This limit is
+ * documented in each individual template.
+ *
+ * <h4>Text Wrapping</h4>
+ *
+ * Each string added with {@link #addText} will not wrap more than 1 line in the UI, with
+ * one exception: if the template allows a maximum number of text strings larger than 1, and
+ * the app adds a single text string, then this string will wrap up to the maximum.
+ *
+ * <p>For example, assuming 2 lines are allowed in the template where the row will be
+ * used, this code:
+ *
+ * <pre>{@code
+ * rowBuilder
+ * .addText("This is a rather long line of text")
+ * .addText("More text")
+ * }</pre>
+ *
+ * <p>would wrap the text like this:
+ *
+ * <pre>
+ * This is a rather long li...
+ * More text
+ * </pre>
+ *
+ * In contrast, this code:
+ *
+ * <pre>{@code
+ * rowBuilder
+ * .addText("This is a rather long line of text. More text")
+ * }</pre>
+ *
+ * <p>would wrap the single line of text at a maximum of 2 lines, producing a different
+ * result:
+ *
+ * <pre>
+ * This is a rather long line
+ * of text. More text
+ * </pre>
+ *
+ * <p>Note that when using a single line, a line break character can be used to break it
+ * into two, but the results may be unpredictable depending on the width the text is
+ * wrapped at:
+ *
+ * <pre>{@code
+ * rowBuilder
+ * .addText("This is a rather long line of text\nMore text")
+ * }</pre>
+ *
+ * <p>would produce a result that may loose the "More text" string:
+ *
+ * <pre>
+ * This is a rather long line
+ * of text
+ * </pre>
+ *
+ * @throws NullPointerException if {@code text} is {@code null}.
+ * @see ForegroundCarColorSpan
+ */
+ @NonNull
+ public Builder addText(@NonNull CharSequence text) {
+ this.mTexts.add(CarText.create(requireNonNull(text)));
+ return this;
+ }
+
+ /**
+ * Clears any rows that may have been added with {@link #addText(CharSequence)} up to this
+ * point.
+ */
+ @NonNull
+ public Builder clearText() {
+ mTexts.clear();
+ return this;
+ }
+
+ /**
+ * Adds a line text of the row below the title.
+ *
+ * @throws NullPointerException if {@code text} is {@code null}.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public Builder addText(@NonNull CarText text) {
+ this.mTexts.add(requireNonNull(text));
+ return this;
+ }
+
+ /**
+ * Sets an image to show in the row with the default size {@link #IMAGE_TYPE_SMALL}, or
+ * {@code null} to not display an image in the row.
+ *
+ * @see #setImage(CarIcon, int)
+ */
+ @NonNull
+ public Builder setImage(@Nullable CarIcon image) {
+ return setImage(image, IMAGE_TYPE_SMALL);
+ }
+
+ /**
+ * Sets an image to show in the row with the given image type, or {@code null} to not
+ * display an image in the row.
+ *
+ * <p>For a custom {@link CarIcon}, its {@link androidx.core.graphics.drawable.IconCompat}
+ * instance can be of {@link androidx.core.graphics.drawable.IconCompat#TYPE_BITMAP},
+ * {@link androidx.core.graphics.drawable.IconCompat#TYPE_RESOURCE}, or
+ * {@link androidx.core.graphics.drawable.IconCompat#TYPE_URI}.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * <p>If the input image's size exceeds the sizing requirements for the given image type in
+ * either one of the dimensions, it will be scaled down to be centered inside the
+ * bounding box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ *
+ * @param image the {@link CarIcon} to display, or {@code null} to not display one.
+ * @param imageType one of {@link #IMAGE_TYPE_ICON}, {@link #IMAGE_TYPE_SMALL} or {@link
+ * #IMAGE_TYPE_LARGE}
+ */
+ @NonNull
+ public Builder setImage(@Nullable CarIcon image, @RowImageType int imageType) {
+ CarIconConstraints.UNCONSTRAINED.validateOrThrow(image);
+ this.mImage = image;
+ this.mRowImageType = imageType;
+ return this;
+ }
+
+ /**
+ * Sets a {@link Toggle} to show in the row, or {@code null} to not display a toggle in
+ * the row.
+ */
+ @NonNull
+ public Builder setToggle(@Nullable Toggle toggle) {
+ this.mToggle = toggle;
+ return this;
+ }
+
+ /**
+ * Shows an icon at the end of the row that indicates that the row is browsable.
+ *
+ * <p>Browsable rows can be used, for example, to represent the parent row in a hierarchy of
+ * lists with child lists.
+ *
+ * <p>If a row is browsable, then no {@link Action} or {@link Toggle} can be added to it.
+ */
+ @NonNull
+ public Builder setBrowsable(boolean isBrowsable) {
+ this.mIsBrowsable = isBrowsable;
+ return this;
+ }
+
+ /**
+ * Sets the {@link OnClickListener} to be called back when the row is clicked, or {@code
+ * null} to make the row non-clickable.
+ */
+ @NonNull
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+ if ( null) {
+ this.mOnClickListener = null;
+ } else {
+ this.mOnClickListener = OnClickListenerWrapper.create(onClickListener);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the {@link Metadata} associated with the row.
+ *
+ * @param metadata The metadata to set with the row. Pass {@link Metadata#EMPTY_METADATA}
+ * to not associate any metadata with the row.
+ */
+ @NonNull
+ public Builder setMetadata(@NonNull Metadata metadata) {
+ this.mMetadata = metadata;
+ return this;
+ }
+
+ /**
+ * Sets flags for the row.
+ */
+ @NonNull
+ public Builder setFlags(@RowFlags int flags) {
+ this.mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link Row} defined by this builder.
+ *
+ * @throws IllegalStateException if the row's title is not set.
+ * @throws IllegalStateException if the row is a browsable row and has a {@link Toggle}.
+ * @throws IllegalStateException if the row is a browsable row but does not have a {@link
+ * OnClickListener}.
+ * @throws IllegalStateException if the row has both a {@link OnClickListener} and a {@link
+ * Toggle}.
+ */
+ @NonNull
+ public Row build() {
+ if (mTitle == null) {
+ throw new IllegalStateException("A title must be set on the row");
+ }
+
+ if (mIsBrowsable) {
+ if (mToggle != null) {
+ throw new IllegalStateException("A browsable row must not have a toggle set");
+ }
+ if (mOnClickListener == null) {
+ throw new IllegalStateException(
+ "A browsable row must have its onClickListener set");
+ }
+ }
+
+ if (mToggle != null && mOnClickListener != null) {
+ throw new IllegalStateException(
+ "If a row contains a toggle, it must not have a onClickListener set");
+ }
+
+ return new Row(this);
+ }
+
+ private Builder() {
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
new file mode 100644
index 0000000..b2048b4
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
@@ -0,0 +1,415 @@
+/*
+ * 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 androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.ISearchListener;
+import androidx.car.app.Screen;
+import androidx.car.app.SearchListener;
+import androidx.car.app.utils.Logger;
+import androidx.car.app.utils.RemoteUtils;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A model that allows the user to enter text searches, and can display results in a list.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template
+ * supports any content changes as refreshes. This allows apps to interactively update the search
+ * results as the user types without the templates being counted against the quota.
+ */
+public final class SearchTemplate implements Template {
+ @Keep
+ private final boolean mIsLoading;
+ @Keep
+ private final ISearchListener mSearchListener;
+ @Keep
+ @Nullable
+ private final String mInitialSearchText;
+ @Keep
+ @Nullable
+ private final String mSearchHint;
+ @Keep
+ @Nullable
+ private final ItemList mItemList;
+ @Keep
+ private final boolean mShowKeyboardByDefault;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+
+ /**
+ * Constructs a new builder of {@link SearchTemplate}.
+ *
+ * @param listener the listener to be invoked for events such as when the user types new
+ * text, or
+ * submits a search.
+ */
+ @NonNull
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public static Builder builder(@NonNull SearchListener listener) {
+ return new Builder(listener);
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ /**
+ * Returns the {@link ActionStrip} instance set in the template.
+ */
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ /**
+ * Returns the optional initial search text.
+ *
+ * @see Builder#setInitialSearchText
+ */
+ @Nullable
+ public String getInitialSearchText() {
+ return mInitialSearchText;
+ }
+
+ /**
+ * Returns the optional search hint.
+ *
+ * @see Builder#setSearchHint
+ */
+ @Nullable
+ public String getSearchHint() {
+ return mSearchHint;
+ }
+
+ /**
+ * Returns the optional {@link ItemList} for search results.
+ *
+ * @see Builder#getItemList
+ */
+ @Nullable
+ public ItemList getItemList() {
+ return mItemList;
+ }
+
+ /**
+ * Returns the {@link SearchListener} for search callbacks.
+ *
+ * @hide
+ */
+ // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public ISearchListener getSearchListener() {
+ return mSearchListener;
+ }
+
+ /**
+ * Returns whether to show the keyboard by default.
+ *
+ * @see Builder#setShowKeyboardByDefault
+ */
+ public boolean isShowKeyboardByDefault() {
+ return mShowKeyboardByDefault;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ // Always allow updating on search templates. Search results needs to be updated on the fly
+ // as user searches.
+ return oldTemplate.getClass() == this.getClass();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "SearchTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mInitialSearchText,
+ mIsLoading,
+ mSearchHint,
+ mItemList,
+ mShowKeyboardByDefault,
+ mHeaderAction,
+ mActionStrip);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SearchTemplate)) {
+ return false;
+ }
+ SearchTemplate otherTemplate = (SearchTemplate) other;
+
+ // Don't compare listener.
+ return mIsLoading == otherTemplate.mIsLoading
+ && Objects.equals(mInitialSearchText, otherTemplate.mInitialSearchText)
+ && Objects.equals(mSearchHint, otherTemplate.mSearchHint)
+ && Objects.equals(mItemList, otherTemplate.mItemList)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+ && mShowKeyboardByDefault == otherTemplate.mShowKeyboardByDefault;
+ }
+
+ private SearchTemplate(Builder builder) {
+ mInitialSearchText = builder.mInitialSearchText;
+ mSearchHint = builder.mSearchHint;
+ mIsLoading = builder.mIsLoading;
+ mItemList = builder.mItemList;
+ mSearchListener = builder.mSearchListener;
+ mShowKeyboardByDefault = builder.mShowKeyboardByDefault;
+ mHeaderAction = builder.mHeaderAction;
+ mActionStrip = builder.mActionStrip;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private SearchTemplate() {
+ mInitialSearchText = null;
+ mSearchHint = null;
+ mIsLoading = false;
+ mItemList = null;
+ mHeaderAction = null;
+ mActionStrip = null;
+ mSearchListener = new SearchListenerStub(
+ new SearchListener() {
+ @Override
+ public void onSearchTextChanged(@NonNull String searchText) {
+ }
+
+ @Override
+ public void onSearchSubmitted(@NonNull String searchText) {
+ }
+ });
+ mShowKeyboardByDefault = true;
+ }
+
+ /** A builder of {@link SearchTemplate}. */
+ public static final class Builder {
+ private final ISearchListener mSearchListener;
+ @Nullable
+ private String mInitialSearchText;
+ @Nullable
+ private String mSearchHint;
+ private boolean mIsLoading;
+ @Nullable
+ private ItemList mItemList;
+ private boolean mShowKeyboardByDefault = true;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+
+ private Builder(SearchListener listener) {
+ mSearchListener = new SearchListenerStub(listener);
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null}
+ * to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+ * ActionStrip}.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+ * {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Sets the initial search text to display in the search box, or {@code null} to not
+ * display any initial search text.
+ *
+ * <p>Defaults to {@code null}.
+ */
+ @NonNull
+ public Builder setInitialSearchText(@Nullable String initialSearchText) {
+ this.mInitialSearchText = initialSearchText;
+ return this;
+ }
+
+ /**
+ * Sets the text hint to display in the search box when it is empty, or {@code null} to
+ * use a default search hint.
+ *
+ * <p>This is not the actual search text, and will disappear if user types any value into
+ * the search.
+ *
+ * <p>If a non empty text is set via {@link #setInitialSearchText}, the {@code searchHint
+ * } will not show, unless the user erases the search text.
+ *
+ * <p>Defaults to {@code null}.
+ */
+ @NonNull
+ public Builder setSearchHint(@Nullable String searchHint) {
+ this.mSearchHint = searchHint;
+ return this;
+ }
+
+ /**
+ * Sets whether the template is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+ * would be otherwise. The caller is expected to call {@link
+ * androidx.car.app.Screen#invalidate()} and send the new template content
+ * to the host once the data is ready. If set to {@code false}, the UI shows the {@link
+ * ItemList} contents added via {@link #setItemList}.
+ */
+ @NonNull
+ public Builder setLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ItemList} to show for search results, or {@code null} if there are no
+ * results.
+ *
+ * <p>The list will be shown below the search box, allowing users to click on individual
+ * search results.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
+ * ignore any items over that limit. The list itself cannot be selectable as set via {@link
+ * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
+ * {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
+ *
+ * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setItemList(@Nullable ItemList itemList) {
+ if (itemList != null) {
+ ROW_LIST_CONSTRAINTS_SIMPLE.validateOrThrow(itemList);
+ }
+
+ this.mItemList = itemList;
+ return this;
+ }
+
+ /**
+ * Sets if the keyboard should be displayed by default, instead of waiting until user
+ * interacts with the search box.
+ *
+ * <p>Defaults to {@code true}.
+ */
+ @NonNull
+ public Builder setShowKeyboardByDefault(boolean showKeyboardByDefault) {
+ this.mShowKeyboardByDefault = showKeyboardByDefault;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link SearchTemplate} model.
+ *
+ * @throws IllegalArgumentException if the template is in a loading state but the list is
+ * set.
+ */
+ @NonNull
+ public SearchTemplate build() {
+ if (mIsLoading && mItemList != null) {
+ throw new IllegalArgumentException(
+ "Template is in a loading state but a list is set.");
+ }
+
+ return new SearchTemplate(this);
+ }
+ }
+
+ @Keep // We need to keep these stub for Bundler serialization logic.
+ private static class SearchListenerStub extends ISearchListener.Stub {
+ private final SearchListener mSearchListener;
+
+ private SearchListenerStub(SearchListener searchListener) {
+ mSearchListener = searchListener;
+ }
+
+ @Override
+ public void onSearchTextChanged(String text, IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(
+ () -> mSearchListener.onSearchTextChanged(text), callback,
+ "onSearchTextChanged");
+ }
+
+ @Override
+ public void onSearchSubmitted(String text, IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(
+ () -> mSearchListener.onSearchSubmitted(text), callback, "onSearchSubmitted");
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SectionedItemList.java b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemList.java
new file mode 100644
index 0000000..b9ce5b9
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemList.java
@@ -0,0 +1,95 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Represents an {@link ItemList} that is contained inside a section, for internal use only.
+ */
+public class SectionedItemList {
+ @Keep
+ @Nullable
+ private final ItemList mItemList;
+ @Keep
+ @Nullable
+ private final CarText mHeader;
+
+ /**
+ * Creates an instance of a {@link SectionedItemList} with the given {@code itemList} and
+ * {@code sectionHeader}.
+ */
+ @NonNull
+ public static SectionedItemList create(
+ @NonNull ItemList itemList, @NonNull CarText sectionHeader) {
+ return new SectionedItemList(requireNonNull(itemList), requireNonNull(sectionHeader));
+ }
+
+ /** Returns the {@link ItemList} for the section. */
+ @NonNull
+ public ItemList getItemList() {
+ return requireNonNull(mItemList);
+ }
+
+ /** Returns the title of the section. */
+ @NonNull
+ public CarText getHeader() {
+ return requireNonNull(mHeader);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[ items: " + mItemList + ", has header: " + (mHeader != null) + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mItemList, mHeader);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SectionedItemList)) {
+ return false;
+ }
+ SectionedItemList otherList = (SectionedItemList) other;
+
+ return Objects.equals(mItemList, otherList.mItemList) && Objects.equals(mHeader,
+ otherList.mHeader);
+ }
+
+ private SectionedItemList(@Nullable ItemList itemList, @Nullable CarText header) {
+ this.mItemList = itemList;
+ this.mHeader = header;
+ }
+
+ /** For serialization. */
+ private SectionedItemList() {
+ this.mItemList = null;
+ this.mHeader = null;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Template.java b/car/app/app/src/main/java/androidx/car/app/model/Template.java
new file mode 100644
index 0000000..46eb37a
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Template.java
@@ -0,0 +1,42 @@
+/*
+ * 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 android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.utils.Logger;
+
+/** An interface used to denote a model that can act as a root for a tree of other models. */
+public interface Template {
+
+ /**
+ * Returns {@code true} if this {@link Template} instance is determined to be a refresh compared
+ * to the input template.
+ */
+ default boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ return false;
+ }
+
+ /**
+ * Checks that the application has the required permissions for this template.
+ *
+ * @throws SecurityException if the application is missing any required permission.
+ */
+ default void checkPermissions(@NonNull Context context) {
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/TemplateInfo.java b/car/app/app/src/main/java/androidx/car/app/model/TemplateInfo.java
new file mode 100644
index 0000000..7084bba
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/TemplateInfo.java
@@ -0,0 +1,77 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Stores information about {@link Template} returned from a {@link
+ * androidx.car.app.Screen}.
+ */
+public final class TemplateInfo {
+ @Keep
+ @Nullable
+ private final Class<? extends Template> mTemplateClass;
+ @Keep
+ @Nullable
+ private final String mTemplateId;
+
+ public TemplateInfo(@NonNull Template template, @NonNull String templateId) {
+ this.mTemplateClass = template.getClass();
+ this.mTemplateId = templateId;
+ }
+
+ private TemplateInfo() {
+ this.mTemplateClass = null;
+ this.mTemplateId = null;
+ }
+
+ @NonNull
+ public Class<? extends Template> getTemplateClass() {
+ return requireNonNull(mTemplateClass);
+ }
+
+ @NonNull
+ public String getTemplateId() {
+ return requireNonNull(mTemplateId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTemplateClass, mTemplateId);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof TemplateInfo)) {
+ return false;
+ }
+ TemplateInfo otherInfo = (TemplateInfo) other;
+
+ return Objects.equals(mTemplateClass, otherInfo.mTemplateClass)
+ && Objects.equals(mTemplateId, otherInfo.mTemplateId);
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
new file mode 100644
index 0000000..a26e151
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
@@ -0,0 +1,211 @@
+/*
+ * 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 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.
+ */
+public final class TemplateWrapper {
+ @Keep
+ private Template mTemplate;
+ @Keep
+ private String mId;
+ @Keep
+ private List<TemplateInfo> mTemplateInfoForScreenStack = new ArrayList<>();
+
+ /** The current step in a task that the template is in. For internal, host-side use only. */
+ private int mCurrentTaskStep;
+
+ /**
+ * Whether the template wrapper is a refresh of the current template. For internal, host-side
+ * use
+ * only.
+ */
+ 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() {
+ return requireNonNull(mTemplate);
+ }
+
+ /** Returns the ID associated with the wrapped {@link Template}. */
+ @NonNull
+ public String getId() {
+ return 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) {
+ this.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.
+ */
+ @Nullable
+ public List<TemplateInfo> getTemplateInfosForScreenStack() {
+ return mTemplateInfoForScreenStack;
+ }
+
+ /**
+ * Retrieves the current task step that the template is in. For internal, host-side use only.
+ */
+ public int getCurrentTaskStep() {
+ return mCurrentTaskStep;
+ }
+
+ /**
+ * Sets the current task step that the template is in. For internal, host-side use only.
+ */
+ public void setCurrentTaskStep(int currentTaskStep) {
+ this.mCurrentTaskStep = currentTaskStep;
+ }
+
+ /** Sets whether the template is a refresh of the current template. */
+ public void setRefresh(boolean isRefresh) {
+ this.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. For internal,
+ * host-side use only.
+ */
+ public void setTemplate(@NonNull Template template) {
+ this.mTemplate = template;
+ }
+
+ /**
+ * Updates the ID associated with the wrapped {@link Template}. For internal, host-side use
+ * only.
+ */
+ public void setId(@NonNull String id) {
+ this.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) {
+ this.mTemplate = template;
+ this.mId = id;
+ }
+
+ private TemplateWrapper() {
+ mTemplate = null;
+ mId = "";
+ }
+
+ private static String createRandomId() {
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Toggle.java b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
new file mode 100644
index 0000000..b7b849e
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
@@ -0,0 +1,174 @@
+/*
+ * 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 android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnCheckedChangeListener;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.utils.RemoteUtils;
+
+/** Represents a toggle that can have either a checked or unchecked state. */
+public class Toggle {
+ /** A listener for handling checked state change events. */
+ public interface OnCheckedChangeListener {
+ /** Notifies that the checked state has changed. */
+ void onCheckedChange(boolean isChecked);
+ }
+
+ @Keep
+ @Nullable
+ private final IOnCheckedChangeListener mOnCheckedChangeListener;
+ @Keep
+ private final boolean mIsChecked;
+
+ /**
+ * Constructs a new builder of {@link Toggle}.
+ *
+ * @throws NullPointerException if {@code onCheckedChangeListener} is {@code null}.
+ */
+ @NonNull
+ @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+ public static Builder builder(@NonNull OnCheckedChangeListener onCheckedChangeListener) {
+ return new Builder(requireNonNull(onCheckedChangeListener));
+ }
+
+ /**
+ * Returns {@code true} if the toggle is checked.
+ */
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ /**
+ * Returns the {@link OnCheckedChangeListener} that is called when the checked state of the
+ * {@link Toggle}is changed.
+ *
+ * @hide
+ */
+ // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public IOnCheckedChangeListener getOnCheckedChangeListener() {
+ return requireNonNull(mOnCheckedChangeListener);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[ isChecked: " + mIsChecked + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Boolean.valueOf(mIsChecked).hashCode();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Toggle)) {
+ return false;
+ }
+ Toggle otherToggle = (Toggle) other;
+
+ // Don't compare listener.
+ return mIsChecked == otherToggle.mIsChecked;
+ }
+
+ private Toggle(Builder builder) {
+ mIsChecked = builder.mIsChecked;
+ mOnCheckedChangeListener = builder.mOnCheckedChangeListener;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Toggle() {
+ mOnCheckedChangeListener = null;
+ mIsChecked = false;
+ }
+
+ /** A builder of {@link Toggle}. */
+ public static final class Builder {
+ private IOnCheckedChangeListener mOnCheckedChangeListener;
+ private boolean mIsChecked;
+
+ /**
+ * Sets the initial checked state for {@link Toggle}.
+ *
+ * <p>The default state of a {@link Toggle} is unchecked.
+ */
+ @NonNull
+ public Builder setChecked(boolean checked) {
+ this.mIsChecked = checked;
+ return this;
+ }
+
+ /**
+ * Sets the {@link OnCheckedChangeListener} to call when the checked state of the
+ * {@link Toggle}
+ * is changed.
+ *
+ * @throws NullPointerException if {@code onCheckedChangeListener} is {@code null}.
+ */
+ @NonNull
+ // TODO(shiufai): remove MissingGetterMatchingBuilder once listener is properly exposed.
+ @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+ public Builder setCheckedChangeListener(
+ @NonNull OnCheckedChangeListener onCheckedChangeListener) {
+ this.mOnCheckedChangeListener =
+ new OnCheckedChangeListenerStub(requireNonNull(onCheckedChangeListener));
+ return this;
+ }
+
+ private Builder(OnCheckedChangeListener onCheckedChangeListener) {
+ this.mOnCheckedChangeListener = new OnCheckedChangeListenerStub(
+ onCheckedChangeListener);
+ }
+
+ /** Constructs the {@link Toggle} defined by this builder. */
+ @NonNull
+ public Toggle build() {
+ return new Toggle(this);
+ }
+ }
+
+ @Keep // We need to keep these stub for Bundler serialization logic.
+ private static class OnCheckedChangeListenerStub extends IOnCheckedChangeListener.Stub {
+ private final OnCheckedChangeListener mOnCheckedChangeListener;
+
+ private OnCheckedChangeListenerStub(OnCheckedChangeListener onCheckedChangeListener) {
+ this.mOnCheckedChangeListener = onCheckedChangeListener;
+ }
+
+ @Override
+ public void onCheckedChange(boolean isChecked, IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(
+ () -> mOnCheckedChangeListener.onCheckedChange(isChecked), callback,
+ "onCheckedChange");
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
new file mode 100644
index 0000000..89857f8
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
@@ -0,0 +1,248 @@
+/*
+ * 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.constraints;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.Action.ActionType;
+import androidx.car.app.model.CarText;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Encapsulates the constraints to apply when rendering a list of {@link Action}s on a template.
+ */
+public class ActionsConstraints {
+
+ /** Conservative constraints for most template types. */
+ @NonNull
+ private static final ActionsConstraints ACTIONS_CONSTRAINTS_CONSERVATIVE =
+ ActionsConstraints.builder().setMaxActions(2).build();
+
+ /**
+ * Constraints for template headers, where only the special-purpose back and app-icon standard
+ * actions are allowed.
+ */
+ @NonNull
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_HEADER =
+ ActionsConstraints.builder().setMaxActions(1).addDisallowedActionType(
+ Action.TYPE_CUSTOM).build();
+
+ /**
+ * Default constraints that should be applied to most templates (2 actions, 1 can have
+ * title).
+ */
+ @NonNull
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE =
+ ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxCustomTitles(1).build();
+
+ /** Constraints for navigation templates. */
+ @NonNull
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION =
+ ACTIONS_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setMaxActions(4)
+ .setMaxCustomTitles(1)
+ .addRequiredActionType(Action.TYPE_CUSTOM)
+ .build();
+
+ private final int mMaxActions;
+ private final int mMaxCustomTitles;
+ private final Set<Integer> mRequiredActionTypes;
+ private final Set<Integer> mDisallowedActionTypes;
+
+ /** Returns a builder of {@link ActionsConstraints}. */
+ @VisibleForTesting
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns a new builder that contains the same data as this {@link ActionsConstraints}
+ * instance.
+ */
+ @VisibleForTesting
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /** Returns the max number of actions allowed. */
+ public int getMaxActions() {
+ return mMaxActions;
+ }
+
+ /** Returns the max number of actions with custom titles allowed. */
+ public int getMaxCustomTitles() {
+ return mMaxCustomTitles;
+ }
+
+ /** Adds the set of required action types. */
+ @NonNull
+ public Set<Integer> getRequiredActionTypes() {
+ return mRequiredActionTypes;
+ }
+
+ /** Adds the set of disallowed action types. */
+ @NonNull
+ public Set<Integer> getDisallowedActionTypes() {
+ return mDisallowedActionTypes;
+ }
+
+ /**
+ * Validates the input list of {@link Action}s against this {@link ActionsConstraints} instance.
+ *
+ * @throws IllegalArgumentException if the actions has more actions than allowed.
+ * @throws IllegalArgumentException if the actions has more actions with custom titles than
+ * allowed.
+ * @throws IllegalArgumentException if the actions does not contain all required types.
+ * @throws IllegalArgumentException if the actions contain any disallowed types.
+ */
+ public void validateOrThrow(@NonNull List<Object> actions) {
+ int maxAllowedActions = mMaxActions;
+ int maxAllowedCustomTitles = mMaxCustomTitles;
+
+ Set<Integer> requiredTypes =
+ mRequiredActionTypes.isEmpty()
+ ? Collections.emptySet()
+ : new HashSet<>(this.mRequiredActionTypes);
+
+ for (Object object : actions) {
+ if (object instanceof Action) {
+ Action action = (Action) object;
+
+ if (mDisallowedActionTypes.contains(action.getType())) {
+ throw new IllegalArgumentException(
+ Action.typeToString(action.getType()) + " is disallowed");
+ }
+
+ requiredTypes.remove(action.getType());
+
+ CarText title = action.getTitle();
+ if (title != null && !title.isEmpty()) {
+ if (--maxAllowedCustomTitles < 0) {
+ throw new IllegalArgumentException(
+ "Action strip exceeded max number of "
+ + mMaxCustomTitles
+ + " actions with custom titles");
+ }
+ }
+
+ if (--maxAllowedActions < 0) {
+ throw new IllegalArgumentException(
+ "Action strip exceeded max number of " + mMaxActions + " actions");
+ }
+
+ } else {
+ throw new IllegalArgumentException("Unsupported action: " + object);
+ }
+ }
+
+ if (!requiredTypes.isEmpty()) {
+ StringBuilder missingTypeError = new StringBuilder();
+ for (@ActionType int type : requiredTypes) {
+ missingTypeError.append(Action.typeToString(type)).append(",");
+ }
+ throw new IllegalArgumentException(
+ "Missing required action types: " + missingTypeError);
+ }
+ }
+
+ private ActionsConstraints(Builder builder) {
+ mMaxActions = builder.mMaxActions;
+ mMaxCustomTitles = builder.mMaxCustomTitles;
+ mRequiredActionTypes = new HashSet<>(builder.mRequiredActionTypes);
+
+ if (!builder.mDisallowedActionTypes.isEmpty()) {
+ Set<Integer> disallowedActionTypes = new HashSet<>(builder.mDisallowedActionTypes);
+ disallowedActionTypes.retainAll(mRequiredActionTypes);
+ if (!disallowedActionTypes.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Disallowed action types cannot also be in the required set.");
+ }
+ }
+ mDisallowedActionTypes = new HashSet<>(builder.mDisallowedActionTypes);
+
+ if (mRequiredActionTypes.size() > mMaxActions) {
+ throw new IllegalArgumentException(
+ "Required action types exceeded max allowed actions.");
+ }
+ }
+
+ /**
+ * A builder of {@link ActionsConstraints}.
+ */
+ @VisibleForTesting
+ public static final class Builder {
+ private int mMaxActions = Integer.MAX_VALUE;
+ private int mMaxCustomTitles;
+ private final Set<Integer> mRequiredActionTypes = new HashSet<>();
+ private final Set<Integer> mDisallowedActionTypes = new HashSet<>();
+
+ /** Sets the maximum number of actions allowed. */
+ @NonNull
+ public Builder setMaxActions(int maxActions) {
+ this.mMaxActions = maxActions;
+ return this;
+ }
+
+ /** Sets the maximum number of actions with custom titles allowed. */
+ @NonNull
+ public Builder setMaxCustomTitles(int maxCustomTitles) {
+ this.mMaxCustomTitles = maxCustomTitles;
+ return this;
+ }
+
+ /** Adds an action type to the set of required types. */
+ @NonNull
+ public Builder addRequiredActionType(@ActionType int actionType) {
+ mRequiredActionTypes.add(actionType);
+ return this;
+ }
+
+ /** Adds an action type to the set of disallowed types. */
+ @NonNull
+ public Builder addDisallowedActionType(@ActionType int actionType) {
+ mDisallowedActionTypes.add(actionType);
+ return this;
+ }
+
+ /**
+ * Returns an {@link ActionsConstraints} instance defined by this builder.
+ */
+ @NonNull
+ public ActionsConstraints build() {
+ return new ActionsConstraints(this);
+ }
+
+ private Builder() {
+ }
+
+ private Builder(ActionsConstraints constraints) {
+ this.mMaxActions = constraints.mMaxActions;
+ this.mMaxCustomTitles = constraints.mMaxCustomTitles;
+ this.mRequiredActionTypes.addAll(constraints.mRequiredActionTypes);
+ this.mDisallowedActionTypes.addAll(constraints.mDisallowedActionTypes);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarColorConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarColorConstraints.java
new file mode 100644
index 0000000..489022b
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarColorConstraints.java
@@ -0,0 +1,82 @@
+/*
+ * 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.constraints;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarColor.CarColorType;
+
+import java.util.HashSet;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link CarColor} on a template.
+ */
+public class CarColorConstraints {
+
+ @NonNull
+ public static final CarColorConstraints UNCONSTRAINED =
+ CarColorConstraints.create(
+ new int[]{
+ CarColor.TYPE_CUSTOM,
+ CarColor.TYPE_DEFAULT,
+ CarColor.TYPE_PRIMARY,
+ CarColor.TYPE_SECONDARY,
+ CarColor.TYPE_RED,
+ CarColor.TYPE_GREEN,
+ CarColor.TYPE_BLUE,
+ CarColor.TYPE_YELLOW
+ });
+
+ @NonNull
+ public static final CarColorConstraints STANDARD_ONLY =
+ CarColorConstraints.create(
+ new int[]{
+ CarColor.TYPE_DEFAULT,
+ CarColor.TYPE_PRIMARY,
+ CarColor.TYPE_SECONDARY,
+ CarColor.TYPE_RED,
+ CarColor.TYPE_GREEN,
+ CarColor.TYPE_BLUE,
+ CarColor.TYPE_YELLOW
+ });
+
+ @CarColorType
+ private final HashSet<Integer> mAllowedTypes;
+
+ private static CarColorConstraints create(int[] allowedColorTypes) {
+ return new CarColorConstraints(allowedColorTypes);
+ }
+
+ /**
+ * Returns {@code true} if the {@link CarColor} meets the constraints' requirement.
+ *
+ * @throws IllegalArgumentException if the color type is not allowed.
+ */
+ public void validateOrThrow(@NonNull CarColor carColor) {
+ @CarColorType int type = carColor.getType();
+ if (!mAllowedTypes.contains(type)) {
+ throw new IllegalArgumentException("Car color type is not allowed: " + carColor);
+ }
+ }
+
+ private CarColorConstraints(int[] allowedColorTypes) {
+ this.mAllowedTypes = new HashSet<>();
+ for (int type : allowedColorTypes) {
+ this.mAllowedTypes.add(type);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarIconConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarIconConstraints.java
new file mode 100644
index 0000000..2b9353c
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarIconConstraints.java
@@ -0,0 +1,96 @@
+/*
+ * 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.constraints;
+
+import android.content.ContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link CarIcon} on a template.
+ */
+public class CarIconConstraints {
+ /** Allow all custom icon types. */
+ @NonNull
+ public static final CarIconConstraints UNCONSTRAINED =
+ CarIconConstraints.create(
+ new int[]{
+ IconCompat.TYPE_BITMAP,
+ IconCompat.TYPE_RESOURCE,
+ IconCompat.TYPE_URI
+ });
+
+ /** By default, do not allow custom icon types that would load asynchronously in the host. */
+ @NonNull
+ public static final CarIconConstraints DEFAULT =
+ CarIconConstraints.create(new int[]{IconCompat.TYPE_BITMAP, IconCompat.TYPE_RESOURCE});
+
+ private final int[] mAllowedTypes;
+
+ private static CarIconConstraints create(int[] allowedCustomIconTypes) {
+ return new CarIconConstraints(allowedCustomIconTypes);
+ }
+
+ /**
+ * Returns {@code true} if the {@link CarIcon} meets the constraints' requirement.
+ *
+ * @throws IllegalStateException if the custom icon does not have a backing
+ * {@link IconCompat}
+ * instance.
+ * @throws IllegalArgumentException if the custom icon type is not allowed.
+ */
+ public void validateOrThrow(@Nullable CarIcon carIcon) {
+ if (carIcon == null || carIcon.getType() != CarIcon.TYPE_CUSTOM) {
+ return;
+ }
+
+ IconCompat iconCompat = carIcon.getIcon();
+ if (iconCompat == null) {
+ throw new IllegalStateException("Custom icon does not have a backing IconCompat");
+ }
+
+ checkSupportedIcon(iconCompat);
+ }
+
+ /**
+ * Checks whether the given icon is supported.
+ *
+ * @throws IllegalArgumentException if the given icon type is unsupported.
+ */
+ @NonNull
+ public IconCompat checkSupportedIcon(@NonNull IconCompat iconCompat) {
+ int type = iconCompat.getType();
+ for (int allowedType : mAllowedTypes) {
+ if (type == allowedType) {
+ if (type == IconCompat.TYPE_URI
+ && !ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(
+ iconCompat.getUri().getScheme())) {
+ throw new IllegalArgumentException("Unsupported URI scheme for: " + iconCompat);
+ }
+ return iconCompat;
+ }
+ }
+ throw new IllegalArgumentException("Custom icon type is not allowed: " + type);
+ }
+
+ private CarIconConstraints(int[] allowedCustomIconTypes) {
+ this.mAllowedTypes = allowedCustomIconTypes;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
new file mode 100644
index 0000000..28b8051
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
@@ -0,0 +1,264 @@
+/*
+ * 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.constraints;
+
+import static androidx.car.app.model.Row.ROW_FLAG_SHOW_DIVIDERS;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Row.RowFlags;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link
+ * androidx.car.app.model.Row} in different contexts.
+ */
+public class RowConstraints {
+ @NonNull
+ public static final RowConstraints UNCONSTRAINED = RowConstraints.builder().build();
+
+ /** Conservative constraints for a row. */
+ @NonNull
+ public static final RowConstraints ROW_CONSTRAINTS_CONSERVATIVE =
+ RowConstraints.builder()
+ .setMaxActionsExclusive(0)
+ .setImageAllowed(false)
+ .setMaxTextLinesPerRow(1)
+ .setOnClickListenerAllowed(true)
+ .setToggleAllowed(false)
+ .build();
+
+ /** The constraints for a full-width row in a pane. */
+ @NonNull
+ public static final RowConstraints ROW_CONSTRAINTS_PANE =
+ RowConstraints.builder()
+ .setMaxActionsExclusive(2)
+ .setImageAllowed(true)
+ .setMaxTextLinesPerRow(2)
+ .setToggleAllowed(false)
+ .setOnClickListenerAllowed(false)
+ .build();
+
+ /** The constraints for a simple row (2 rows of text and 1 image */
+ @NonNull
+ public static final RowConstraints ROW_CONSTRAINTS_SIMPLE =
+ RowConstraints.builder()
+ .setFlagOverrides(ROW_FLAG_SHOW_DIVIDERS)
+ .setMaxActionsExclusive(0)
+ .setImageAllowed(true)
+ .setMaxTextLinesPerRow(2)
+ .setToggleAllowed(false)
+ .setOnClickListenerAllowed(true)
+ .build();
+
+ /** The constraints for a full-width row in a list (simple + toggle support). */
+ @NonNull
+ public static final RowConstraints ROW_CONSTRAINTS_FULL_LIST =
+ ROW_CONSTRAINTS_SIMPLE.newBuilder().setToggleAllowed(true).build();
+
+ private final int mMaxTextLinesPerRow;
+ private final int mMaxActionsExclusive;
+ private final boolean mIsImageAllowed;
+ private final boolean mIsToggleAllowed;
+ private final boolean mIsOnClickListenerAllowed;
+ @RowFlags
+ private final int mFlagOverrides;
+ private final CarIconConstraints mCarIconConstraints;
+
+ /**
+ * Returns a new {@link Builder}.
+ */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns a new builder that contains the same data as this {@link RowConstraints} instance.
+ */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /** Returns whether the row can have a click listener associated with it. */
+ public boolean isOnClickListenerAllowed() {
+ return mIsOnClickListenerAllowed;
+ }
+
+ /** Returns the maximum number lines of text, excluding the title, to render in the row. */
+ public int getMaxTextLinesPerRow() {
+ return mMaxTextLinesPerRow;
+ }
+
+ /** Returns the maximum number actions to allowed in a row that consists only of actions. */
+ public int getMaxActionsExclusive() {
+ return mMaxActionsExclusive;
+ }
+
+ /** Returns whether a toggle can be added to the row. */
+ public boolean isToggleAllowed() {
+ return mIsToggleAllowed;
+ }
+
+ /** Returns whether an image can be added to the row. */
+ public boolean isImageAllowed() {
+ return mIsImageAllowed;
+ }
+
+ /**
+ * The flags that will be forced on each row, on top of whatever flags come from the client
+ * side.
+ */
+ @RowFlags
+ public int getFlagOverrides() {
+ return mFlagOverrides;
+ }
+
+ /** Returns the {@link CarIconConstraints} enforced for the row images. */
+ @NonNull
+ public CarIconConstraints getCarIconConstraints() {
+ return mCarIconConstraints;
+ }
+
+ /**
+ * Validates that the given row satisfies this {@link RowConstraints} instance.
+ *
+ * @throws IllegalArgumentException if the constraints are not met.
+ */
+ public void validateOrThrow(@NonNull Object rowObj) {
+ Row row = (Row) rowObj;
+
+ if (!mIsOnClickListenerAllowed && row.getOnClickListener() != null) {
+ throw new IllegalArgumentException("A click listener is not allowed on the row");
+ }
+
+ if (!mIsToggleAllowed && row.getToggle() != null) {
+ throw new IllegalArgumentException("A toggle is not allowed on the row");
+ }
+
+ CarIcon image = row.getImage();
+ if (image != null) {
+ if (!mIsImageAllowed) {
+ throw new IllegalArgumentException("An image is not allowed on the row");
+ }
+
+ mCarIconConstraints.validateOrThrow(image);
+ }
+
+ if (row.getTexts().size() > mMaxTextLinesPerRow) {
+ throw new IllegalArgumentException(
+ "The number of lines of texts for the row exceeded the supported max of "
+ + mMaxTextLinesPerRow);
+ }
+ }
+
+ private RowConstraints(Builder builder) {
+ mIsOnClickListenerAllowed = builder.mIsOnClickListenerAllowed;
+ mMaxTextLinesPerRow = builder.mMaxTextLines;
+ mMaxActionsExclusive = builder.mMaxActionsExclusive;
+ mIsToggleAllowed = builder.mIsToggleAllowed;
+ mIsImageAllowed = builder.mIsImageAllowed;
+ mFlagOverrides = builder.mFlagOverrides;
+ mCarIconConstraints = builder.mCarIconConstraints;
+ }
+
+ /** A builder of {@link RowConstraints}. */
+ public static final class Builder {
+ private boolean mIsOnClickListenerAllowed = true;
+ private boolean mIsToggleAllowed = true;
+ private int mMaxTextLines = Integer.MAX_VALUE;
+ private int mMaxActionsExclusive = Integer.MAX_VALUE;
+ private boolean mIsImageAllowed = true;
+ @RowFlags
+ private int mFlagOverrides;
+ private CarIconConstraints mCarIconConstraints = CarIconConstraints.UNCONSTRAINED;
+
+ /** Sets whether the row can have a click listener associated with it. */
+ @NonNull
+ public Builder setOnClickListenerAllowed(boolean isOnClickListenerAllowed) {
+ this.mIsOnClickListenerAllowed = isOnClickListenerAllowed;
+ return this;
+ }
+
+ /** Sets the maximum number lines of text, excluding the title, to render in the row. */
+ @NonNull
+ public Builder setMaxTextLinesPerRow(int maxTextLinesPerRow) {
+ this.mMaxTextLines = maxTextLinesPerRow;
+ return this;
+ }
+
+ /** Sets the maximum number actions to allowed in a row that consists only of actions. */
+ @NonNull
+ public Builder setMaxActionsExclusive(int maxActionsExclusive) {
+ this.mMaxActionsExclusive = maxActionsExclusive;
+ return this;
+ }
+
+ /** Sets whether an image can be added to the row. */
+ @NonNull
+ public Builder setImageAllowed(boolean imageAllowed) {
+ this.mIsImageAllowed = imageAllowed;
+ return this;
+ }
+
+ /** Sets whether a toggle can be added to the row. */
+ @NonNull
+ public Builder setToggleAllowed(boolean toggleAllowed) {
+ this.mIsToggleAllowed = toggleAllowed;
+ return this;
+ }
+
+ /**
+ * Sets the flags that will be forced on each row, on top of whatever flags come from
+ * the client side.
+ */
+ @NonNull
+ public Builder setFlagOverrides(@RowFlags int flagOverrides) {
+ this.mFlagOverrides = flagOverrides;
+ return this;
+ }
+
+ /** Sets the {@link CarIconConstraints} enforced for the row images. */
+ @NonNull
+ public Builder setCarIconConstraints(@NonNull CarIconConstraints carIconConstraints) {
+ this.mCarIconConstraints = carIconConstraints;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link RowConstraints} defined by this builder.
+ */
+ @NonNull
+ public RowConstraints build() {
+ return new RowConstraints(this);
+ }
+
+ private Builder() {
+ }
+
+ private Builder(RowConstraints constraints) {
+ mIsOnClickListenerAllowed = constraints.mIsOnClickListenerAllowed;
+ mMaxTextLines = constraints.mMaxTextLinesPerRow;
+ mMaxActionsExclusive = constraints.mMaxActionsExclusive;
+ mIsToggleAllowed = constraints.mIsToggleAllowed;
+ mIsImageAllowed = constraints.mIsImageAllowed;
+ mFlagOverrides = constraints.mFlagOverrides;
+ mCarIconConstraints = constraints.mCarIconConstraints;
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
new file mode 100644
index 0000000..5d20a94
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
@@ -0,0 +1,271 @@
+/*
+ * 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.constraints;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_CONSERVATIVE;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_FULL_LIST;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_PANE;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_SIMPLE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.ActionList;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.SectionedItemList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Encapsulates the constraints to apply when rendering a row list under different contexts.
+ */
+public class RowListConstraints {
+ /**
+ * The RowList that is used for max row.
+ *
+ * @hide
+ */
+ // TODO(shiufai): investigate how to expose IntDefs if needed.
+ @IntDef(value = {DEFAULT_LIST, PANE, ROUTE_PREVIEW})
+ @RestrictTo(LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ListType {
+ }
+
+ @ListType
+ public static final int DEFAULT_LIST = 0;
+
+ @ListType
+ public static final int PANE = 1;
+
+ @ListType
+ public static final int ROUTE_PREVIEW = 2;
+
+ /** Conservative constraints for all types lists. */
+ @NonNull
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE =
+ RowListConstraints.builder()
+ .setRowListType(DEFAULT_LIST)
+ .setMaxActions(0)
+ .setRowConstraints(ROW_CONSTRAINTS_CONSERVATIVE)
+ .setAllowSelectableLists(false)
+ .build();
+
+ /** Default constraints for heterogeneous pane of items, full width. */
+ @NonNull
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_PANE =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setMaxActions(2)
+ .setRowListType(PANE)
+ .setRowConstraints(ROW_CONSTRAINTS_PANE)
+ .setAllowSelectableLists(false)
+ .build();
+
+ /** Default constraints for uniform lists of items, no toggles. */
+ @NonNull
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+ .build();
+
+ /** Default constraints for the route preview card. */
+ @NonNull
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setRowListType(ROUTE_PREVIEW)
+ .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+ .setAllowSelectableLists(true)
+ .build();
+
+ /** Default constraints for uniform lists of items, full width (simple + toggle support). */
+ @NonNull
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setRowConstraints(ROW_CONSTRAINTS_FULL_LIST)
+ .setAllowSelectableLists(true)
+ .build();
+
+ @ListType
+ private final int mRowListType;
+ private final int mMaxActions;
+ private final RowConstraints mRowConstraints;
+ private final boolean mAllowSelectableLists;
+
+ /** A builder of {@link RowListConstraints}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Return a a new builder for this {@link RowListConstraints} instance. */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /** Returns the row list type for this constraint. */
+ @ListType
+ public int getRowListType() {
+ return mRowListType;
+ }
+
+ /** Returns the maximum number of actions allowed to be added alongside the list. */
+ public int getMaxActions() {
+ return mMaxActions;
+ }
+
+ /** Returns the constraints to apply on individual rows. */
+ @NonNull
+ public RowConstraints getRowConstraints() {
+ return mRowConstraints;
+ }
+
+ /** Returns whether selectable lists are allowed. */
+ public boolean isAllowSelectableLists() {
+ return mAllowSelectableLists;
+ }
+
+ /**
+ * Validates that the {@link ItemList} satisfies this {@link RowListConstraints} instance.
+ *
+ * @throws IllegalArgumentException if the constraints are not met.
+ */
+ public void validateOrThrow(@NonNull ItemList itemList) {
+ if (itemList.getOnSelectedListener() != null && !mAllowSelectableLists) {
+ throw new IllegalArgumentException("Selectable lists are not allowed");
+ }
+
+ validateRows(itemList.getItems());
+ }
+
+ /**
+ * Validates that the list of {@link SectionedItemList}s satisfies this
+ * {@link RowListConstraints}
+ * instance.
+ *
+ * @throws IllegalArgumentException if the constraints are not met.
+ */
+ public void validateOrThrow(@NonNull List<SectionedItemList> sections) {
+ List<Object> combinedLists = new ArrayList<>();
+
+ for (SectionedItemList section : sections) {
+ ItemList sectionList = section.getItemList();
+ if (sectionList.getOnSelectedListener() != null && !mAllowSelectableLists) {
+ throw new IllegalArgumentException("Selectable lists are not allowed");
+ }
+
+ combinedLists.addAll(sectionList.getItems());
+ }
+
+ validateRows(combinedLists);
+ }
+
+ /**
+ * Validates that the {@link Pane} satisfies this {@link RowListConstraints} instance.
+ *
+ * @throws IllegalArgumentException if the constraints are not met.
+ */
+ public void validateOrThrow(@NonNull Pane pane) {
+ ActionList actions = pane.getActionList();
+ if (actions != null && actions.getList().size() > mMaxActions) {
+ throw new IllegalArgumentException(
+ "The number of actions on the pane exceeded the supported max of "
+ + mMaxActions);
+ }
+
+ validateRows(pane.getRows());
+ }
+
+ private void validateRows(List<Object> rows) {
+ for (Object rowObj : rows) {
+ mRowConstraints.validateOrThrow(rowObj);
+ }
+ }
+
+ private RowListConstraints(Builder builder) {
+ mMaxActions = builder.mMaxActions;
+ mRowConstraints = builder.mRowConstraints;
+ mAllowSelectableLists = builder.mAllowSelectableLists;
+ mRowListType = builder.mRowListType;
+ }
+
+ /**
+ * A builder of {@link RowListConstraints}.
+ */
+ public static final class Builder {
+ @ListType
+ private int mRowListType;
+ private int mMaxActions;
+ private RowConstraints mRowConstraints = RowConstraints.UNCONSTRAINED;
+ private boolean mAllowSelectableLists;
+
+ /** Sets the row list type for this constraint. */
+ @NonNull
+ public Builder setRowListType(@ListType int rowListType) {
+ this.mRowListType = rowListType;
+ return this;
+ }
+
+ /** Sets the maximum number of actions allowed to be added alongside the list. */
+ @NonNull
+ public Builder setMaxActions(int maxActions) {
+ this.mMaxActions = maxActions;
+ return this;
+ }
+
+ /** Sets the constraints to apply on individual rows. */
+ @NonNull
+ public Builder setRowConstraints(@NonNull RowConstraints rowConstraints) {
+ this.mRowConstraints = rowConstraints;
+ return this;
+ }
+
+ /** Sets whether selectable lists are allowed. */
+ @NonNull
+ public Builder setAllowSelectableLists(boolean allowSelectableLists) {
+ this.mAllowSelectableLists = allowSelectableLists;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link RowListConstraints} defined by this builder.
+ */
+ @NonNull
+ public RowListConstraints build() {
+ return new RowListConstraints(this);
+ }
+
+ private Builder() {
+ }
+
+ private Builder(RowListConstraints constraints) {
+ this.mMaxActions = constraints.mMaxActions;
+ this.mRowConstraints = constraints.mRowConstraints;
+ this.mAllowSelectableLists = constraints.mAllowSelectableLists;
+ this.mRowListType = constraints.mRowListType;
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
new file mode 100644
index 0000000..fd22326
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -0,0 +1,274 @@
+/*
+ * 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.navigation;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.car.app.utils.ThreadUtils.checkMainThread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarContext;
+import androidx.car.app.HostDispatcher;
+import androidx.car.app.HostException;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.navigation.model.TravelEstimate;
+import androidx.car.app.navigation.model.Trip;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.car.app.utils.RemoteUtils;
+
+/**
+ * Manager for communicating navigation related events with the host.
+ *
+ * <p>Navigation apps must use this interface to coordinate with the car system for navigation
+ * specific resources such as vehicle cluster and heads-up displays.
+ *
+ * <p>When a navigation app receives a user action to start navigating, it should call {@link
+ * #navigationStarted()} to indicate it is currently navigating. When the app receives a user action
+ * to end navigation or when the destination is reached, {@link #navigationEnded()} should be
+ * called.
+ *
+ * <p>Navigation apps must also register a {@link NavigationManagerListener} to handle callbacks to
+ * {@link NavigationManagerListener#stopNavigation()} issued by the host.
+ */
+public class NavigationManager {
+ private final INavigationManager.Stub mNavigationmanager;
+ private final HostDispatcher mHostDispatcher;
+
+ // Guarded by main thread access.
+ @Nullable
+ private NavigationManagerListener mListener;
+ private boolean mIsNavigating;
+ private boolean mIsAutoDriveEnabled;
+
+ /**
+ * Sends the destinations, steps, and trip estimates to the host.
+ *
+ * <p>The data <b>may</b> be rendered at different places in the car such as the instrument
+ * cluster screen or the heads-up display.
+ *
+ * <p>This method should only be invoked once the navigation app has called {@link
+ * #navigationStarted()}, or else the updates will be dropped by the host. Once the app has
+ * called {@link #navigationEnded()} or received
+ * {@link NavigationManagerListener#stopNavigation()} it should stop sending updates.
+ *
+ * <p>As the location changes, and in accordance with speed and rounded distance changes, the
+ * {@link TravelEstimate}s in the provided {@link Trip} should be rebuilt and this method called
+ * again. For example, when the next step is greater than 10 kilometers away and the display
+ * unit is kilometers, updates should occur roughly every kilometer.
+ *
+ * <p>Data provided to the cluster display depends on the vehicle capabilities. In some
+ * instances the information may not be shown at all. On some vehicles {@link
+ * androidx.car.app.navigation.model.Maneuver}s of unknown type may be skipped while on other
+ * displays the associated icon may be shown.
+ *
+ * @throws HostException if the call is invoked by an app that is not declared as
+ * a navigation app in the manifest.
+ * @throws IllegalStateException if the call occurs when navigation is not started. See
+ * {@link #navigationStarted()} for more info.
+ * @throws IllegalArgumentException if any of the destinations, steps, or trip position is
+ * not well formed.
+ * @throws IllegalStateException if the current thread is not the main thread.
+ */
+ @MainThread
+ public void updateTrip(@NonNull Trip trip) {
+ checkMainThread();
+ if (!mIsNavigating) {
+ throw new IllegalStateException("Navigation is not started");
+ }
+
+ Bundleable bundle;
+ try {
+ bundle = Bundleable.create(trip);
+ } catch (BundlerException e) {
+ throw new IllegalArgumentException("Serialization failure", e);
+ }
+
+ mHostDispatcher.dispatch(
+ CarContext.NAVIGATION_SERVICE,
+ (INavigationHost service) -> {
+ service.updateTrip(bundle);
+ return null;
+ },
+ "updateTrip");
+ }
+
+ /**
+ * Sets a listener to start receiving navigation manager events, or {@code null} to clear the
+ * listener.
+ *
+ * @throws IllegalStateException if {@code null} is passed in while navigation is started. See
+ * {@link #navigationStarted()} for more info.
+ * @throws IllegalStateException if the current thread is not the main thread.
+ */
+ // TODO(rampara): Add Executor parameter.
+ @SuppressLint("ExecutorRegistration")
+ @MainThread
+ public void setListener(@Nullable NavigationManagerListener listener) {
+ checkMainThread();
+ if (mIsNavigating && listener == null) {
+ throw new IllegalStateException("Removing listener while navigating");
+ }
+ this.mListener = listener;
+ if (mIsAutoDriveEnabled && listener != null) {
+ listener.onAutoDriveEnabled();
+ }
+ }
+
+ /**
+ * Notifies the host that the app has started active navigation.
+ *
+ * <p>Only one app may be actively navigating in the car at any time and ownership is managed by
+ * the host. The app must call this method to inform the system that it has started
+ * navigation in response to user action.
+ *
+ * <p>This function can only called if {@link #setListener(NavigationManagerListener)} has been
+ * called with a non-{@code null} value. The listener is required so that a signal to stop
+ * navigation from the host can be handled using
+ * {@link NavigationManagerListener#stopNavigation()}.
+ *
+ * <p>This method is idempotent.
+ *
+ * @throws IllegalStateException if no navigation manager listener has been set.
+ * @throws IllegalStateException if the current thread is not the main thread.
+ */
+ @MainThread
+ public void navigationStarted() {
+ checkMainThread();
+ if (mIsNavigating) {
+ return;
+ }
+ if (mListener == null) {
+ throw new IllegalStateException("No listener has been set");
+ }
+ mIsNavigating = true;
+ mHostDispatcher.dispatch(
+ CarContext.NAVIGATION_SERVICE,
+ (INavigationHost service) -> {
+ service.navigationStarted();
+ return null;
+ },
+ "navigationStarted");
+ }
+
+ /**
+ * Notifies the host that the app has ended active navigation.
+ *
+ * <p>Only one app may be actively navigating in the car at any time and ownership is managed by
+ * the host. The app must call this method to inform the system that it has ended navigation,
+ * for example, in response to the user cancelling navigation or upon reaching the destination.
+ *
+ * <p>This method is idempotent.
+ *
+ * @throws IllegalStateException if the current thread is not the main thread.
+ */
+ @MainThread
+ public void navigationEnded() {
+ checkMainThread();
+ if (!mIsNavigating) {
+ return;
+ }
+ mIsNavigating = false;
+ mHostDispatcher.dispatch(
+ CarContext.NAVIGATION_SERVICE,
+ (INavigationHost service) -> {
+ service.navigationEnded();
+ return null;
+ },
+ "navigationEnded");
+ }
+
+ /**
+ * Creates an instance of {@link NavigationManager}.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public static NavigationManager create(@NonNull HostDispatcher hostDispatcher) {
+ return new NavigationManager(hostDispatcher);
+ }
+
+ /**
+ * Returns the {@code INavigationManager.Stub} binder object.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ public INavigationManager.Stub getIInterface() {
+ return mNavigationmanager;
+ }
+
+ /**
+ * Tells the app to stop navigating.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @MainThread
+ public void stopNavigation() {
+ checkMainThread();
+ if (!mIsNavigating) {
+ return;
+ }
+ mIsNavigating = false;
+ requireNonNull(mListener).stopNavigation();
+ }
+
+ /**
+ * Signifies that from this point, until {@link
+ * androidx.car.app.CarAppService#onCarAppFinished} is called, any navigation
+ * should automatically start driving to the destination as if the user was moving.
+ *
+ * <p>This is used in a testing environment, allowing testing the navigation app's navigation
+ * capabilities without being in a car.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @MainThread
+ public void onAutoDriveEnabled() {
+ checkMainThread();
+ mIsAutoDriveEnabled = true;
+ if (mListener != null) {
+ mListener.onAutoDriveEnabled();
+ }
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
+ @SuppressWarnings({"methodref.receiver.bound.invalid"})
+ protected NavigationManager(@NonNull HostDispatcher hostDispatcher) {
+ this.mHostDispatcher = requireNonNull(hostDispatcher);
+ mNavigationmanager =
+ new INavigationManager.Stub() {
+ @Override
+ public void stopNavigation(IOnDoneCallback callback) {
+ RemoteUtils.dispatchHostCall(
+ NavigationManager.this::stopNavigation, callback, "stopNavigation");
+ }
+ };
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
new file mode 100644
index 0000000..ee4c29f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
@@ -0,0 +1,54 @@
+/*
+ * 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.navigation;
+
+import android.annotation.SuppressLint;
+
+import androidx.car.app.navigation.model.Trip;
+
+/**
+ * Listener of events from the {@link NavigationManager}.
+ *
+ * @see NavigationManager
+ */
+public interface NavigationManagerListener {
+ /**
+ * Notifies the app to stop active navigation, which may occurs when another source such as the
+ * car head unit starts navigating.
+ *
+ * <p>When receiving this callback, the app must stop all routing including navigation voice
+ * guidance, routing-related notifications, and updating trip information via {@link
+ * NavigationManager#updateTrip(Trip)}.
+ */
+
+ // TODO(rampara): Listener method names must follow the on<Something> style. Consider
+ // onShouldStopNavigation.
+ @SuppressLint("CallbackMethodName")
+ void stopNavigation();
+
+ /**
+ * Notifies the app that, from this point onwards, when the user chooses to navigate to a
+ * destination, the app should start simulating a drive towards that destination.
+ *
+ * <p>This mode should remain active until {@link
+ * androidx.car.app.CarAppService#onCarAppFinished} is called.
+ *
+ * <p>This functionality is used to allow verifying the app's navigation capabilities without
+ * being in an actual car.
+ */
+ void onAutoDriveEnabled();
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
new file mode 100644
index 0000000..efcd9e6
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
@@ -0,0 +1,189 @@
+/*
+ * 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.navigation.model;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.util.Objects;
+
+/** A class representing information related to a destination. */
+public final class Destination {
+ @Keep
+ @Nullable
+ private final CarText mName;
+ @Keep
+ @Nullable
+ private final CarText mAddress;
+ @Keep
+ @Nullable
+ private final CarIcon mImage;
+
+ /**
+ * Constructs a new builder of {@link Destination} with the given name and address.
+ *
+ * @throws NullPointerException if {@code name} is {@code null}.
+ * @throws NullPointerException if {@code address} is {@code null}.
+ */
+ @NonNull
+ public static Builder builder(@NonNull CharSequence name, @NonNull CharSequence address) {
+ return builder().setName(name).setAddress(address);
+ }
+
+ /** Constructs a new builder of {@link Destination}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Nullable
+ public CarText getName() {
+ return mName;
+ }
+
+ @Nullable
+ public CarText getAddress() {
+ return mAddress;
+ }
+
+ @Nullable
+ public CarIcon getImage() {
+ return mImage;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[name: "
+ + CarText.toShortString(mName)
+ + ", address: "
+ + CarText.toShortString(mAddress)
+ + ", image: "
+ + mImage
+ + "]";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof Destination)) {
+ return false;
+ }
+
+ Destination otherDestination = (Destination) other;
+ return Objects.equals(mName, otherDestination.mName)
+ && Objects.equals(mAddress, otherDestination.mAddress)
+ && Objects.equals(mImage, otherDestination.mImage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mName, mAddress, mImage);
+ }
+
+ private Destination(Builder builder) {
+ this.mName = builder.mName;
+ this.mAddress = builder.mAddress;
+ this.mImage = builder.mImage;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Destination() {
+ mName = null;
+ mAddress = null;
+ mImage = null;
+ }
+
+ /** A builder of {@link Destination}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mName;
+ @Nullable
+ private CarText mAddress;
+ @Nullable
+ private CarIcon mImage;
+
+ /**
+ * Sets the destination name formatted for the user's current locale, or {@code null} to not
+ * display a destination name.
+ */
+ @NonNull
+ public Builder setName(@Nullable CharSequence name) {
+ this.mName = name == null ? null : CarText.create(name);
+ return this;
+ }
+
+ /**
+ * Sets the destination address formatted for the user's current locale, or {@code null}
+ * to not
+ * display an address.
+ */
+ @NonNull
+ public Builder setAddress(@Nullable CharSequence address) {
+ this.mAddress = address == null ? null : CarText.create(address);
+ return this;
+ }
+
+ /**
+ * Sets the destination image to display, or {@code null} to not display an image.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * The provided image should have a maximum size of 64 x 64 dp. If the image exceeds this
+ * maximum size in either one of the dimensions, it will be scaled down and centered
+ * inside the
+ * bounding box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that
+ * work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setImage(@Nullable CarIcon image) {
+ CarIconConstraints.DEFAULT.validateOrThrow(image);
+ this.mImage = image;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link Destination} defined by this builder.
+ *
+ * <p>At least one of the name or the address must be set and not empty.
+ *
+ * @throws IllegalStateException if both the name and the address are {@code null} or empty.
+ * @see #setName(CharSequence)
+ * @see #setAddress(CharSequence)
+ */
+ @NonNull
+ public Destination build() {
+ if ((mName == null || mName.isEmpty()) && (mAddress == null || mAddress.isEmpty())) {
+ throw new IllegalStateException("Both name and address cannot be null or empty");
+ }
+ return new Destination(this);
+ }
+
+ private Builder() {
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Lane.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Lane.java
new file mode 100644
index 0000000..335035d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Lane.java
@@ -0,0 +1,115 @@
+/*
+ * 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.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Configuration of a single lane of a road at a particular point in the navigation.
+ *
+ * <p>A {@link Lane} object describes all possible directions the driver could go from this lane,
+ * and indicates which directions the driver could take to stay on the navigation route.
+ */
+public final class Lane {
+ @Keep
+ private final List<LaneDirection> mDirections;
+
+ /** Constructs a new builder of {@link Lane}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @NonNull
+ public List<LaneDirection> getDirections() {
+ return mDirections;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[direction count: " + (mDirections != null ? mDirections.size() : 0) + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mDirections);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Lane)) {
+ return false;
+ }
+
+ Lane otherLane = (Lane) other;
+ return Objects.equals(mDirections, otherLane.mDirections);
+ }
+
+ private Lane(List<LaneDirection> directions) {
+ this.mDirections = new ArrayList<>(directions);
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Lane() {
+ mDirections = Collections.emptyList();
+ }
+
+ /** A builder of {@link Lane}. */
+ public static final class Builder {
+ private final List<LaneDirection> mDirections = new ArrayList<>();
+
+ /**
+ * Adds a direction a driver can take from this lane.
+ *
+ * @throws NullPointerException if {@code direction} is {@code null}.
+ */
+ @NonNull
+ public Builder addDirection(@NonNull LaneDirection direction) {
+ mDirections.add(requireNonNull(direction));
+ return this;
+ }
+
+ /**
+ * Clears any directions that may have been added with
+ * {@link #addDirection(LaneDirection)} up to this point.
+ */
+ @NonNull
+ public Builder clearDirections() {
+ mDirections.clear();
+ return this;
+ }
+
+ /** Constructs the {@link Lane} defined by this builder. */
+ @NonNull
+ public Lane build() {
+ return new Lane(mDirections);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/LaneDirection.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/LaneDirection.java
new file mode 100644
index 0000000..e06a6d5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/LaneDirection.java
@@ -0,0 +1,173 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Defines the possible directions a driver can go when using a particular lane at a particular step
+ * in the navigation.
+ *
+ * <p>These directions can be combined and sent to the host to display a lane configuration to the
+ * user.
+ */
+public final class LaneDirection {
+ /**
+ * Turn amount and direction.
+ *
+ * @hide
+ */
+ @IntDef({
+ SHAPE_UNKNOWN,
+ SHAPE_STRAIGHT,
+ SHAPE_SLIGHT_LEFT,
+ SHAPE_SLIGHT_RIGHT,
+ SHAPE_NORMAL_LEFT,
+ SHAPE_NORMAL_RIGHT,
+ SHAPE_SHARP_LEFT,
+ SHAPE_SHARP_RIGHT,
+ SHAPE_U_TURN_LEFT,
+ SHAPE_U_TURN_RIGHT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY)
+ public @interface Shape {
+ }
+
+ /** The shape is unknown, in which case no lane information should be shown. */
+ @Shape
+ public static final int SHAPE_UNKNOWN = 1;
+
+ /** No turn. */
+ @Shape
+ public static final int SHAPE_STRAIGHT = 2;
+
+ /** Slight left turn, from 10 (included) to 45 (excluded) degrees. */
+ @Shape
+ public static final int SHAPE_SLIGHT_LEFT = 3;
+
+ /** Slight right turn, from 10 (included) to 45 (excluded) degrees. */
+ @Shape
+ public static final int SHAPE_SLIGHT_RIGHT = 4;
+
+ /** Regular left turn, from 45 (included) to 135 (excluded) degrees. */
+ @Shape
+ public static final int SHAPE_NORMAL_LEFT = 5;
+
+ /** Regular right turn, from 45 (included) to 135 (excluded) degrees. */
+ @Shape
+ public static final int SHAPE_NORMAL_RIGHT = 6;
+
+ /** Sharp left turn, from 135 (included) to 175 (excluded) degrees. */
+ @Shape
+ public static final int SHAPE_SHARP_LEFT = 7;
+
+ /** Sharp right turn, from 135 (included) to 175 (excluded) degrees. */
+ @Shape
+ public static final int SHAPE_SHARP_RIGHT = 8;
+
+ /**
+ * A left turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+ * degrees
+ */
+ @Shape
+ public static final int SHAPE_U_TURN_LEFT = 9;
+
+ /**
+ * A right turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+ * degrees
+ */
+ @Shape
+ public static final int SHAPE_U_TURN_RIGHT = 10;
+
+ @Keep
+ @Shape
+ private final int mShape;
+ @Keep
+ private final boolean mIsHighlighted;
+
+ /**
+ * Constructs a new instance of a {@link LaneDirection}.
+ *
+ * @param shape one of the {@code SHAPE_*} static constants defined in this class.
+ * @param isHighlighted indicates whether the {@link LaneDirection} is the one the driver should
+ * take in order to stay on the navigation route.
+ */
+ @NonNull
+ public static LaneDirection create(@Shape int shape, boolean isHighlighted) {
+ return new LaneDirection(shape, isHighlighted);
+ }
+
+ /** Returns shape of this lane direction. */
+ @Shape
+ public int getShape() {
+ return mShape;
+ }
+
+ /**
+ * Returns whether this is a direction the driver should take in order to stay on the navigation
+ * route.
+ */
+ public boolean isHighlighted() {
+ return mIsHighlighted;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[shape: " + mShape + ", isHighlighted: " + mIsHighlighted + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mShape, mIsHighlighted);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof LaneDirection)) {
+ return false;
+ }
+
+ LaneDirection otherDirection = (LaneDirection) other;
+ return mShape == otherDirection.mShape && mIsHighlighted == otherDirection.mIsHighlighted;
+ }
+
+ private LaneDirection(@Shape int shape, boolean isHighlighted) {
+ this.mShape = shape;
+ this.mIsHighlighted = isHighlighted;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private LaneDirection() {
+ mShape = SHAPE_UNKNOWN;
+ mIsHighlighted = false;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
new file mode 100644
index 0000000..8f1e1f0
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
@@ -0,0 +1,645 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Information about a maneuver that the driver will be required to perform. */
+// TODO: Update when Embedded updates or a scheme for auto sync is established.
+public final class Maneuver {
+ /**
+ * Possible maneuver types.
+ *
+ * @hide
+ */
+ @IntDef({
+ TYPE_UNKNOWN,
+ TYPE_DEPART,
+ TYPE_NAME_CHANGE,
+ TYPE_KEEP_LEFT,
+ TYPE_KEEP_RIGHT,
+ TYPE_TURN_SLIGHT_LEFT,
+ TYPE_TURN_SLIGHT_RIGHT,
+ TYPE_TURN_NORMAL_LEFT,
+ TYPE_TURN_NORMAL_RIGHT,
+ TYPE_TURN_SHARP_LEFT,
+ TYPE_TURN_SHARP_RIGHT,
+ TYPE_U_TURN_LEFT,
+ TYPE_U_TURN_RIGHT,
+ TYPE_ON_RAMP_SLIGHT_LEFT,
+ TYPE_ON_RAMP_SLIGHT_RIGHT,
+ TYPE_ON_RAMP_NORMAL_LEFT,
+ TYPE_ON_RAMP_NORMAL_RIGHT,
+ TYPE_ON_RAMP_SHARP_LEFT,
+ TYPE_ON_RAMP_SHARP_RIGHT,
+ TYPE_ON_RAMP_U_TURN_LEFT,
+ TYPE_ON_RAMP_U_TURN_RIGHT,
+ TYPE_OFF_RAMP_SLIGHT_LEFT,
+ TYPE_OFF_RAMP_SLIGHT_RIGHT,
+ TYPE_OFF_RAMP_NORMAL_LEFT,
+ TYPE_OFF_RAMP_NORMAL_RIGHT,
+ TYPE_FORK_LEFT,
+ TYPE_FORK_RIGHT,
+ TYPE_MERGE_LEFT,
+ TYPE_MERGE_RIGHT,
+ TYPE_MERGE_SIDE_UNSPECIFIED,
+ TYPE_ROUNDABOUT_ENTER,
+ TYPE_ROUNDABOUT_EXIT,
+ TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW,
+ TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE,
+ TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW,
+ TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE,
+ TYPE_STRAIGHT,
+ TYPE_FERRY_BOAT,
+ TYPE_FERRY_TRAIN,
+ TYPE_DESTINATION,
+ TYPE_DESTINATION_STRAIGHT,
+ TYPE_DESTINATION_LEFT,
+ TYPE_DESTINATION_RIGHT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY)
+ public @interface Type {
+ }
+
+ // LINT.IfChange(enums)
+ /**
+ * Maneuver type is unknown, no maneuver information should be displayed.
+ *
+ * <p>{@link #TYPE_UNKNOWN} may be interpreted differently depending on the consumer. In some
+ * cases the previous maneuver will continue to be shown while in others no maneuver will be
+ * shown at all.
+ */
+ @Type
+ public static final int TYPE_UNKNOWN = 0;
+
+ /**
+ * Starting point of the navigation.
+ *
+ * <p>For example, "Start driving on Main St."
+ */
+ @Type
+ public static final int TYPE_DEPART = 1;
+
+ /**
+ * No turn, but the street name changes.
+ *
+ * <p>For example, "Continue on Main St."
+ */
+ @Type
+ public static final int TYPE_NAME_CHANGE = 2;
+
+ /**
+ * No turn, from 0 (included) to 10 (excluded) degrees.
+ *
+ * <p>This is used in contrast to {@link #TYPE_STRAIGHT} for disambiguating cases where there is
+ * more than one option to go into the same general direction.
+ */
+ @Type
+ public static final int TYPE_KEEP_LEFT = 3;
+
+ /**
+ * No turn, from 0 (included) to 10 (excluded) degrees.
+ *
+ * <p>This is used in contrast to {@link #TYPE_STRAIGHT} for disambiguating cases where there is
+ * more than one option to go into the same general direction.
+ */
+ @Type
+ public static final int TYPE_KEEP_RIGHT = 4;
+
+ /** Slight left turn at an intersection, from 10 (included) to 45 (excluded) degrees. */
+ @Type
+ public static final int TYPE_TURN_SLIGHT_LEFT = 5;
+
+ /** Slight right turn at an intersection, from 10 (included) to 45 (excluded) degrees. */
+ @Type
+ public static final int TYPE_TURN_SLIGHT_RIGHT = 6;
+
+ /** Regular left turn at an intersection, from 45 (included) to 135 (excluded) degrees. */
+ @Type
+ public static final int TYPE_TURN_NORMAL_LEFT = 7;
+
+ /** Regular right turn at an intersection, from 45 (included) to 135 (excluded) degrees. */
+ @Type
+ public static final int TYPE_TURN_NORMAL_RIGHT = 8;
+
+ /** Sharp left turn at an intersection, from 135 (included) to 175 (excluded) degrees. */
+ @Type
+ public static final int TYPE_TURN_SHARP_LEFT = 9;
+
+ /** Sharp right turn at an intersection, from 135 (included) to 175 (excluded) degrees. */
+ @Type
+ public static final int TYPE_TURN_SHARP_RIGHT = 10;
+
+ /**
+ * Left turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_U_TURN_LEFT = 11;
+
+ /**
+ * A right turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_U_TURN_RIGHT = 12;
+
+ /**
+ * Slight left turn to enter a turnpike or freeway, from 10 (included) to 45 (excluded) degrees.
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13;
+
+ /**
+ * Slight right turn to enter a turnpike or freeway, from 10 (included) to 45 (excluded)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14;
+
+ /**
+ * Regular left turn to enter a turnpike or freeway, from 45 (included) to 135 (excluded)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15;
+
+ /**
+ * Regular right turn to enter a turnpike or freeway, from 45 (included) to 135 (excluded)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16;
+
+ /**
+ * Sharp left turn to enter a turnpike or freeway, from 135 (included) to 175 (excluded)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_SHARP_LEFT = 17;
+
+ /**
+ * Sharp right turn to enter a turnpike or freeway, from 135 (included) to 175 (excluded)
+ * degrees.
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18;
+
+ /**
+ * Left turn onto the opposite side of the same street to enter a turnpike or freeway, from 175
+ * (included) to 180 (included).
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19;
+
+ /**
+ * Right turn onto the opposite side of the same street to enter a turnpike or freeway, from 175
+ * (included) to 180 (included).
+ */
+ @Type
+ public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20;
+
+ /** A left turn to exit a turnpike or freeway, from 10 (included) to 45 (excluded) degrees. */
+ @Type
+ public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21;
+
+ /** A right turn to exit a turnpike or freeway, from 10 (included) to 45 (excluded) degrees. */
+ @Type
+ public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22;
+
+ /** A left turn to exit a turnpike or freeway, from 45 (included) to 135 (excluded) degrees. */
+ @Type
+ public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23;
+
+ /** A left right to exit a turnpike or freeway, from 45 (included) to 135 (excluded) degrees. */
+ @Type
+ public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24;
+
+ /**
+ * Keep to the left as the road diverges.
+ *
+ * <p>For example, this is used to indicate "Keep left at the fork".
+ */
+ @Type
+ public static final int TYPE_FORK_LEFT = 25;
+
+ /**
+ * Keep to the right as the road diverges.
+ *
+ * <p>For example, this is used to indicate "Keep right at the fork".
+ */
+ @Type
+ public static final int TYPE_FORK_RIGHT = 26;
+
+ /**
+ * Current road joins another on the left.
+ *
+ * <p>For example, this is used to indicate "Merge left onto Main St.".
+ */
+ @Type
+ public static final int TYPE_MERGE_LEFT = 27;
+
+ /**
+ * Current road joins another on the right.
+ *
+ * <p>For example, this is used to indicate "Merge left onto Main St.".
+ */
+ @Type
+ public static final int TYPE_MERGE_RIGHT = 28;
+
+ /**
+ * Current road joins another without direction specified.
+ *
+ * <p>For example, this is used to indicate "Merge onto Main St.".
+ */
+ @Type
+ public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29;
+
+ /**
+ * Roundabout entrance on which the current road ends.
+ *
+ * <p>For example, this is used to indicate "Enter the roundabout".
+ */
+ @Type
+ public static final int TYPE_ROUNDABOUT_ENTER = 30;
+
+ /**
+ * Used when leaving a roundabout when the step starts in it.
+ *
+ * <p>For example, this is used to indicate "Exit the roundabout".
+ */
+ @Type
+ public static final int TYPE_ROUNDABOUT_EXIT = 31;
+
+ /**
+ * Enter a clockwise roundabout and take the Nth exit.
+ *
+ * <p>The exit number must be passed when created the maneuver.
+ *
+ * <p>For example, this is used to indicate "At the roundabout, take the Nth exit".
+ */
+ @Type
+ public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32;
+
+ /**
+ * Enter a clockwise roundabout and take the Nth exit after angle A degrees.
+ *
+ * <p>The exit number and angle must be passed when creating the maneuver.
+ *
+ * <p>For example, this is used to indicate "At the roundabout, take the Nth exit".
+ */
+ @Type
+ public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33;
+
+ /**
+ * Enter a counter-clockwise roundabout and take the Nth exit.
+ *
+ * <p>The exit number must be passed when created the maneuver.
+ *
+ * <p>For example, this is used to indicate "At the roundabout, take the Nth exit".
+ */
+ @Type
+ public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34;
+
+ /**
+ * Enter a counter-clockwise roundabout and take the Nth exit after angle A degrees.
+ *
+ * <p>The exit number and angle must be passed when creating the maneuver.
+ *
+ * <p>For example, this is used to indicate "At the roundabout, take a sharp right at the Nth
+ * exit".
+ */
+ @Type
+ public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35;
+
+ /** Driver should steer straight. */
+ @Type
+ public static final int TYPE_STRAIGHT = 36;
+
+ /**
+ * Drive towards a boat ferry for vehicles.
+ *
+ * <p>For example, this is used to indicate "Take the ferry".
+ */
+ @Type
+ public static final int TYPE_FERRY_BOAT = 37;
+
+ /** Drive towards a train ferry for vehicles (e.g. "Take the train"). */
+ @Type
+ public static final int TYPE_FERRY_TRAIN = 38;
+
+ /** Arrival at a destination. */
+ @Type
+ public static final int TYPE_DESTINATION = 39;
+
+ /** Arrival to a destination located straight ahead. */
+ @Type
+ public static final int TYPE_DESTINATION_STRAIGHT = 40;
+
+ /** Arrival to a destination located to the left side of the road. */
+ @Type
+ public static final int TYPE_DESTINATION_LEFT = 41;
+
+ /** Arrival to a destination located to the right side of the road. */
+ @Type
+ public static final int TYPE_DESTINATION_RIGHT = 42;
+ // LINT.ThenChange(:enumTypeChecks)
+
+ @Keep
+ @Type
+ private final int mType;
+ @Keep
+ private final int mRoundaboutExitNumber;
+ @Keep
+ private final int mRoundaboutExitAngle;
+ @Keep
+ @Nullable
+ private final CarIcon mIcon;
+
+ /**
+ * Constructs a new builder of {@link Maneuver}.
+ *
+ * <p>The type should be chosen to reflect the closest semantic meaning of the maneuver. In some
+ * cases, an exact type match is not possible, but choosing a similar or slightly more general
+ * type is preferred. Using {@link #TYPE_UNKNOWN} is allowed, but some headunits will not
+ * display any information in that case.
+ *
+ * @param type one of the {@code TYPE_*} static constants defined in this class.
+ * @throws IllegalArgumentException if {@code type} is not a valid maneuver type.
+ */
+ @NonNull
+ public static Builder builder(@Type int type) {
+ if (!isValidType(type)) {
+ throw new IllegalArgumentException("Maneuver must have a valid type");
+ }
+ return new Builder(type);
+ }
+
+ /**
+ * Returns the maneuver type.
+ *
+ * <p>Required to be set at all times.
+ */
+ @Type
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the roundabout exit number, starting from 1 to designate the first exit after joining
+ * the roundabout, and increasing in circulation order. Only relevant if the type is any
+ * variation of {@code TYPE_ROUNDABOUT_ENTER_AND_EXIT_*}.
+ *
+ * <p>For example, if the driver is joining a counter-clockwise roundabout with 4 exits, then
+ * the exit to the right would be exit #1, the one straight ahead would be exit #2, the one
+ * to the left would be exit #3 and the one used by the driver to join the roundabout would
+ * be exit #4.
+ *
+ * <p>Required when the type is a roundabout.
+ */
+ public int getRoundaboutExitNumber() {
+ return mRoundaboutExitNumber;
+ }
+
+ /**
+ * Returns the roundabout exit angle in degrees to designate the amount of distance to travel
+ * around the roundabout. Only relevant if the type is {@link
+ * #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE} or {@link
+ * #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE}.
+ *
+ * <p>For example, if the drive is joining a counter-clockwise roundabout with equally spaced
+ * exits then the exit to the right would be at 45 degrees, the one straight ahead would be
+ * at 90 degrees, the one to the left would at 270 degrees and the one used by the driver to
+ * join the roundabout would be at 360 degrees.
+ *
+ * <p>The angle can also be set for irregular roundabouts. For example a roundabout with three
+ * exits at 90, 270 and 360 degrees could also have the desired exit angle specified.
+ *
+ * <p>Required with the type is a roundabout with an angle.
+ */
+ public int getRoundaboutExitAngle() {
+ return mRoundaboutExitAngle;
+ }
+
+ /**
+ * Returns the icon for the maneuver.
+ *
+ * <p>Optional field that when not set may be shown in the target display by a generic image
+ * representing the specific maneuver.
+ */
+ @Nullable
+ public CarIcon getIcon() {
+ return mIcon;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[type: "
+ + mType
+ + ", exit #: "
+ + mRoundaboutExitNumber
+ + ", exit angle: "
+ + mRoundaboutExitAngle
+ + ", icon: "
+ + mIcon
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mRoundaboutExitNumber, mRoundaboutExitAngle, mIcon);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Maneuver)) {
+ return false;
+ }
+
+ Maneuver otherManeuver = (Maneuver) other;
+ return mType == otherManeuver.mType
+ && mRoundaboutExitNumber == otherManeuver.mRoundaboutExitNumber
+ && mRoundaboutExitAngle == otherManeuver.mRoundaboutExitAngle
+ && Objects.equals(mIcon, otherManeuver.mIcon);
+ }
+
+ private Maneuver(
+ @Type int type, int roundaboutExitNumber, int roundaboutExitAngle,
+ @Nullable CarIcon icon) {
+ this.mType = type;
+ this.mRoundaboutExitNumber = roundaboutExitNumber;
+ this.mRoundaboutExitAngle = roundaboutExitAngle;
+ CarIconConstraints.DEFAULT.validateOrThrow(icon);
+ this.mIcon = icon;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Maneuver() {
+ mType = TYPE_UNKNOWN;
+ mRoundaboutExitNumber = 0;
+ mRoundaboutExitAngle = 0;
+ mIcon = null;
+ }
+
+ private static boolean isValidType(@Type int type) {
+ return (type >= TYPE_UNKNOWN && type <= TYPE_DESTINATION_RIGHT);
+ }
+
+ private static boolean isValidTypeWithExitNumber(@Type int type) {
+ return (type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
+ || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
+ || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE
+ || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE);
+ }
+
+ private static boolean isValidTypeWithExitAngle(@Type int type) {
+ return (type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE
+ || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE);
+ }
+
+ /** A builder of {@link Maneuver}. */
+ public static final class Builder {
+ @Type
+ private final int mType;
+ private boolean mIsRoundaboutExitNumberSet;
+ private int mRoundaboutExitNumber;
+ private boolean mIsRoundaboutExitAngleSet;
+ private int mRoundaboutExitAngle;
+ @Nullable
+ private CarIcon mIcon;
+
+ private Builder(@Type int type) {
+ this.mType = type;
+ }
+
+ /**
+ * Sets an image representing the maneuver, or {@code null} to not set an image for the
+ * maneuver.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * The provided image should have a maximum size of 64 x 64 dp. If the image exceeds this
+ * maximum size in either one of the dimensions, it will be scaled down and centered
+ * inside the bounding box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setIcon(@Nullable CarIcon icon) {
+ this.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets an exit number for roundabout maneuvers.
+ *
+ * <p>Use for when {@code type} is {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW}, {@link
+ * #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW},
+ * {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE} or
+ * {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE}. The {@code
+ * roundaboutExitNumber} starts from 1 to designate the first exit after joining the
+ * roundabout, and increases in circulation order.
+ *
+ * <p>For example, if the driver is joining a counter-clockwise roundabout with 4 exits,
+ * then the exit to the right would be exit #1, the one straight ahead would be exit #2,
+ * the one to the left would be exit #3 and the one used by the driver to join the
+ * roundabout would be exit #4.
+ *
+ * @throws IllegalArgumentException if {@code type} does not include a exit number.
+ * @throws IllegalArgumentException if {@code roundaboutExitNumber} is not greater than
+ * zero.
+ */
+ @NonNull
+ public Builder setRoundaboutExitNumber(int roundaboutExitNumber) {
+ if (!isValidTypeWithExitNumber(mType)) {
+ throw new IllegalArgumentException(
+ "Maneuver does not include roundaboutExitNumber");
+ }
+ if (roundaboutExitNumber < 1) {
+ throw new IllegalArgumentException("Maneuver must include a valid exit number");
+ }
+ this.mIsRoundaboutExitNumberSet = true;
+ this.mRoundaboutExitNumber = roundaboutExitNumber;
+ return this;
+ }
+
+ /**
+ * Sets an exit angle for roundabout maneuvers.
+ *
+ * <p>Use for when {@code type} is {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE} or
+ * {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE}. The {@code roundaboutExitAngle}
+ * represents the degrees traveled in circulation from the entrance to the exit.
+ *
+ * <p>For example, in a 4 exit example, if all the exits are equally spaced then exit 1
+ * would be at 90 degrees, exit 2 at 180, exit 3 at 270 and exit 4 at 360. However if the
+ * exits are irregular then a different angle could be provided.
+ *
+ * @throws IllegalArgumentException if {@code type} does not include a exit angle.
+ * @throws IllegalArgumentException if {@code roundaboutExitAngle} is not greater than
+ * zero and less than or equal to 360 degrees.
+ */
+ @NonNull
+ public Builder setRoundaboutExitAngle(int roundaboutExitAngle) {
+ if (!isValidTypeWithExitAngle(mType)) {
+ throw new IllegalArgumentException("Maneuver does not include roundaboutExitAngle");
+ }
+ if (roundaboutExitAngle < 1 || roundaboutExitAngle > 360) {
+ throw new IllegalArgumentException("Maneuver must include a valid exit angle");
+ }
+ this.mIsRoundaboutExitAngleSet = true;
+ this.mRoundaboutExitAngle = roundaboutExitAngle;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link Maneuver} defined by this builder.
+ *
+ * @throws IllegalArgumentException if {@code type} includes an exit number and one has
+ * not been set.
+ * @throws IllegalArgumentException if {@code type} includes an exit angle and one has
+ * not been set.
+ */
+ @NonNull
+ public Maneuver build() {
+ if (isValidTypeWithExitNumber(mType) && !mIsRoundaboutExitNumberSet) {
+ throw new IllegalArgumentException("Maneuver missing roundaboutExitNumber");
+ }
+ if (isValidTypeWithExitAngle(mType) && !mIsRoundaboutExitAngleSet) {
+ throw new IllegalArgumentException("Maneuver missing roundaboutExitAngle");
+ }
+ return new Maneuver(mType, mRoundaboutExitNumber, mRoundaboutExitAngle, mIcon);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
new file mode 100644
index 0000000..490d300
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
@@ -0,0 +1,155 @@
+/*
+ * 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.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo;
+
+import java.util.Objects;
+
+/** Represents a message that can be shown in the {@link NavigationTemplate}. */
+public class MessageInfo implements NavigationInfo {
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final CarText mText;
+ @Keep
+ @Nullable
+ private final CarIcon mImage;
+
+ /**
+ * Constructs a new builder of {@link MessageInfo}.
+ *
+ * @throws NullPointerException if {@code title} is {@code null}.
+ */
+ @NonNull
+ public static Builder builder(@NonNull CharSequence title) {
+ return new Builder(title);
+ }
+
+ @NonNull
+ public CarText getTitle() {
+ return requireNonNull(mTitle);
+ }
+
+ @Nullable
+ public CarText getText() {
+ return mText;
+ }
+
+ @Nullable
+ public CarIcon getImage() {
+ return mImage;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "MessageInfo";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mText, mImage);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MessageInfo)) {
+ return false;
+ }
+ MessageInfo otherInfo = (MessageInfo) other;
+
+ return Objects.equals(mTitle, otherInfo.mTitle)
+ && Objects.equals(mText, otherInfo.mText)
+ && Objects.equals(mImage, otherInfo.mImage);
+ }
+
+ private MessageInfo(Builder builder) {
+ mTitle = builder.mTitle;
+ mText = builder.mText;
+ mImage = builder.mImage;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private MessageInfo() {
+ mTitle = null;
+ mText = null;
+ mImage = null;
+ }
+
+ /** A builder of {@link MessageInfo}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ @Nullable
+ private CarText mText;
+ @Nullable
+ private CarIcon mImage;
+
+ private Builder(@NonNull CharSequence title) {
+ this.mTitle = CarText.create(requireNonNull(title));
+ }
+
+ /**
+ * Sets the title of the message.
+ *
+ * @throws NullPointerException if {@code message} is {@code null}.
+ */
+ @NonNull
+ public Builder setTitle(@NonNull CharSequence title) {
+ this.mTitle = CarText.create(requireNonNull(title));
+ return this;
+ }
+
+ /** Sets additional text on the message or {@code null} to not set any additional text. */
+ @NonNull
+ public Builder setText(@Nullable CharSequence text) {
+ this.mText = text == null ? null : CarText.create(text);
+ return this;
+ }
+
+ /**
+ * Sets the image to display along with the message, or {@code null} to not display an
+ * image.
+ */
+ @NonNull
+ public Builder setImage(@Nullable CarIcon image) {
+ CarIconConstraints.DEFAULT.validateOrThrow(image);
+ this.mImage = image;
+ return this;
+ }
+
+ /** Constructs the {@link MessageInfo} defined by this builder. */
+ @NonNull
+ public MessageInfo build() {
+ return new MessageInfo(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
new file mode 100644
index 0000000..22e6df5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
@@ -0,0 +1,263 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION;
+import static androidx.car.app.model.constraints.CarColorConstraints.UNCONSTRAINED;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.Screen;
+import androidx.car.app.SurfaceListener;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.Template;
+import androidx.car.app.utils.Logger;
+
+import java.util.Objects;
+
+// TODO(rampara): Update code reference to CarAppExtender is javadoc to link
+
+/**
+ * A template for showing navigation information.
+ *
+ * <p>This template has two independent sections which can be updated:
+ *
+ * <ul>
+ * <li>Navigation information such as routing instructions or navigation-related messages.
+ * <li>Travel estimates to the destination.
+ * </ul>
+ *
+ * <p>To update the template as the user navigates, call {@link Screen#invalidate} to provide the
+ * host with a new template with the updated information.
+ *
+ * <p>The template itself does not expose a drawing surface. In order to draw on the canvas, use
+ * {@link androidx.car.app.AppManager#setSurfaceListener(SurfaceListener)}.
+ *
+ * <p>See {@code androidx.car.app.notification.CarAppExtender} for how to show
+ * alerts with notifications. Frequent alert notifications distract the driver and are discouraged.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regard to template refreshes, as described in {@link Screen#getTemplate()}, this template
+ * supports any content changes as refreshes. This allows apps to interactively update the
+ * turn-by-turn instructions without the templates being counted against the template quota.
+ *
+ * <p>Further, this template is considered a view that the user will stay and consume contents from,
+ * and the host will reset the template quota once an app reaches this template.
+ *
+ * <p>In order to use this template your car app <b>MUST</b> declare that it uses the {@code
+ * androidx.car.app.NAVIGATION_TEMPLATES} permission in the manifest.
+ */
+public class NavigationTemplate implements Template {
+
+ /**
+ * Represents navigation information such as routing instructions or navigation-related
+ * messages.
+ */
+ public interface NavigationInfo {
+ }
+
+ @Keep
+ @Nullable
+ private final NavigationInfo mNavigationInfo;
+ @Keep
+ @Nullable
+ private final CarColor mBackgroundColor;
+ @Keep
+ @Nullable
+ private final TravelEstimate mDestinationTravelEstimate;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+
+ /** Constructs a new builder of {@link NavigationTemplate}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Nullable
+ public NavigationInfo getNavigationInfo() {
+ return mNavigationInfo;
+ }
+
+ @Nullable
+ public CarColor getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ @Nullable
+ public TravelEstimate getDestinationTravelEstimate() {
+ return mDestinationTravelEstimate;
+ }
+
+ @NonNull
+ public ActionStrip getActionStrip() {
+ return requireNonNull(mActionStrip);
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ requireNonNull(oldTemplate);
+
+ // Always allow updating on navigation templates.
+ return oldTemplate.getClass() == this.getClass();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "NavigationTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNavigationInfo, mBackgroundColor, mDestinationTravelEstimate,
+ mActionStrip);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof NavigationTemplate)) {
+ return false;
+ }
+ NavigationTemplate otherTemplate = (NavigationTemplate) other;
+
+ return Objects.equals(mNavigationInfo, otherTemplate.mNavigationInfo)
+ && Objects.equals(mBackgroundColor, otherTemplate.mBackgroundColor)
+ && Objects.equals(mDestinationTravelEstimate,
+ otherTemplate.mDestinationTravelEstimate)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+ }
+
+ @Override
+ public void checkPermissions(@NonNull Context context) {
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+ }
+
+ private NavigationTemplate(Builder builder) {
+ mNavigationInfo = builder.mNavigationInfo;
+ mBackgroundColor = builder.mBackgroundColor;
+ mDestinationTravelEstimate = builder.mDestinationTravelEstimate;
+ mActionStrip = builder.mActionStrip;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private NavigationTemplate() {
+ mNavigationInfo = null;
+ mBackgroundColor = null;
+ mDestinationTravelEstimate = null;
+ mActionStrip = null;
+ }
+
+ /** A builder of {@link NavigationTemplate}. */
+ public static final class Builder {
+ @Nullable
+ private NavigationInfo mNavigationInfo;
+ @Nullable
+ private CarColor mBackgroundColor;
+ @Nullable
+ private TravelEstimate mDestinationTravelEstimate;
+ private ActionStrip mActionStrip;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the navigation information to display on the template, or {@code null} to not
+ * display navigation information on top of the map.
+ */
+ @NonNull
+ public Builder setNavigationInfo(@Nullable NavigationInfo navigationInfo) {
+ this.mNavigationInfo = navigationInfo;
+ return this;
+ }
+
+ /**
+ * Sets the background color to use for the navigation information, or {@code null} to
+ * use the default.
+ *
+ * <p>The host may ignore this color and use a default color instead if the color does
+ * not pass the contrast requirements.
+ */
+ @NonNull
+ public Builder setBackgroundColor(@Nullable CarColor backgroundColor) {
+ if (backgroundColor != null) {
+ UNCONSTRAINED.validateOrThrow(backgroundColor);
+ }
+ this.mBackgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * Sets the {@link TravelEstimate} to the final destination, or {@code null} to not show any
+ * travel estimate information.
+ */
+ @NonNull
+ public Builder setDestinationTravelEstimate(
+ @Nullable TravelEstimate destinationTravelEstimate) {
+ this.mDestinationTravelEstimate = destinationTravelEstimate;
+ return this;
+ }
+
+ /**
+ * Sets an {@link ActionStrip} with a list of template-scoped actions for this template.
+ *
+ * <h4>Requirements</h4>
+ *
+ * Besides {@link Action#APP_ICON} and {@link Action#BACK}, this template requires at
+ * least 1 and up to 4 {@link Action}s in its {@link ActionStrip}. Of the 4 allowed
+ * {@link Action}s, only one can contain a title as set via
+ * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+ * requirements.
+ * @throws NullPointerException if {@code actionStrip} is {@code null}.
+ */
+ @NonNull
+ public Builder setActionStrip(@NonNull ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_NAVIGATION.validateOrThrow(
+ requireNonNull(actionStrip).getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link NavigationTemplate} defined by this builder.
+ *
+ * @throws IllegalStateException if an {@link ActionStrip} is not set on this template.
+ */
+ @NonNull
+ public NavigationTemplate build() {
+ if (mActionStrip == null) {
+ throw new IllegalStateException("Action strip for this template must be set.");
+ }
+ return new NavigationTemplate(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
new file mode 100644
index 0000000..51a304d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
@@ -0,0 +1,350 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.Screen;
+import androidx.car.app.SurfaceListener;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ModelUtils;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template that supports showing a list of places alongside a custom drawn map.
+ *
+ * <p>The template itself does not expose a drawing surface. In order to draw on the canvas, use
+ * {@link androidx.car.app.AppManager#setSurfaceListener(SurfaceListener)}.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template is
+ * considered a refresh of a previous one if:
+ *
+ * <ul>
+ * <li>The template title has not changed, and
+ * <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ * number of rows and the string contents (title, texts, not counting spans) of each row
+ * between the previous and new {@link ItemList}s have not changed.
+ * </ul>
+ *
+ * <p>In order to use this template your car app <b>MUST</b> declare that it uses the {@code
+ * androidx.car.app.NAVIGATION_TEMPLATES} permission in the manifest.
+ */
+public final class PlaceListNavigationTemplate implements Template {
+ @Keep
+ private final boolean mIsLoading;
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final ItemList mItemList;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+
+ /** Constructs a new builder of {@link PlaceListNavigationTemplate}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Nullable
+ public ItemList getItemList() {
+ return mItemList;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ requireNonNull(oldTemplate);
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ PlaceListNavigationTemplate old = (PlaceListNavigationTemplate) oldTemplate;
+ if (!Objects.equals(old.getTitle(), getTitle())) {
+ return false;
+ }
+
+ if (old.mIsLoading) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (mIsLoading) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
+ }
+
+ @Override
+ public void checkPermissions(@NonNull Context context) {
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "PlaceListNavigationTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mIsLoading, mItemList, mHeaderAction, mActionStrip);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PlaceListNavigationTemplate)) {
+ return false;
+ }
+ PlaceListNavigationTemplate otherTemplate = (PlaceListNavigationTemplate) other;
+
+ return mIsLoading == otherTemplate.mIsLoading
+ && Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mItemList, otherTemplate.mItemList)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+ }
+
+ private PlaceListNavigationTemplate(Builder builder) {
+ mTitle = builder.mTitle;
+ mIsLoading = builder.mIsLoading;
+ mItemList = builder.mItemList;
+ mHeaderAction = builder.mHeaderAction;
+ mActionStrip = builder.mActionStrip;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private PlaceListNavigationTemplate() {
+ mTitle = null;
+ mIsLoading = false;
+ mItemList = null;
+ mHeaderAction = null;
+ mActionStrip = null;
+ }
+
+ /** A builder of {@link PlaceListNavigationTemplate}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ private boolean mIsLoading;
+ @Nullable
+ private ItemList mItemList;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+
+ /** Sets the {@link CharSequence} to show as title, or {@code null} to not show a title. */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets whether the template is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will show a loading indicator where the list content
+ * would be otherwise. The caller is expected to call
+ * {@link androidx.car.app.Screen#invalidate()} and send the new template content to the
+ * host
+ * once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
+ * contents added via {@link #setItemList}.
+ */
+ // TODO(rampara): Consider renaming to setLoading()
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setIsLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null} to not display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets an {@link ItemList} to show in the list view along with the map, or {@code null}
+ * to not display a list.
+ *
+ * <p>To show a marker corresponding to a point of interest represented by a row, set the
+ * {@link Place} instance via {@link Row.Builder#setMetadata}. The host will render the
+ * {@link PlaceMarker} in the list view as the row become visible. The app should
+ * synchronize with the list's behavior by rendering the same marker on the map surface.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
+ * ignore any items over that limit. The list itself cannot be selectable as set via {@link
+ * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
+ * {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
+ *
+ * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
+ *
+ * <p>Rows are not allowed to have both and an image and a place marker.
+ *
+ * <p>All non-browsable rows must have a {@link DistanceSpan} attached to either its
+ * title or texts, to indicate the distance of the point of interest from the current
+ * location. Where in the title or text the span is attached to is up to the app.
+ *
+ * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setItemList(@Nullable ItemList itemList) {
+ if (itemList != null) {
+ List<Object> items = itemList.getItems();
+ ROW_LIST_CONSTRAINTS_SIMPLE.validateOrThrow(itemList);
+ ModelUtils.validateAllNonBrowsableRowsHaveDistance(items);
+ ModelUtils.validateAllRowsHaveOnlySmallImages(items);
+ ModelUtils.validateNoRowsHaveBothMarkersAndImages(items);
+ }
+ this.mItemList = itemList;
+
+ return this;
+ }
+
+ /**
+ * Sets an {@link ItemList} for the template. This method does not enforce the
+ * template's requirements and is only intended for testing purposes.
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @VisibleForTesting
+ @NonNull
+ public Builder setItemListForTesting(@Nullable ItemList itemList) {
+ this.mItemList = itemList;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template, or {@code null} to not show an {@link
+ * ActionStrip}.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+ * {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Constructs the template defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * Either a header {@link Action} or title must be set on the template.
+ *
+ * @throws IllegalArgumentException if the template is in a loading state but the list is
+ * set, or vice-versa.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link Action} set.
+ */
+ @NonNull
+ public PlaceListNavigationTemplate build() {
+ boolean hasList = mItemList != null;
+ if (mIsLoading == hasList) {
+ throw new IllegalArgumentException(
+ "Template is in a loading state but a list is set, or vice versa.");
+ }
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new PlaceListNavigationTemplate(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
new file mode 100644
index 0000000..aae7e95
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
@@ -0,0 +1,403 @@
+/*
+ * 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.navigation.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.Screen;
+import androidx.car.app.SurfaceListener;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ModelUtils;
+import androidx.car.app.model.OnClickListener;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A template that supports showing a list of routes alongside a custom drawn map.
+ *
+ * <p>The list must have its {@link
+ * androidx.car.app.model.ItemList.OnSelectedListener} set, and the template
+ * must have its navigate action set (see {@link Builder#setNavigateAction}). These are used in
+ * conjunction to inform the app that:
+ *
+ * <ol>
+ * <li>A route has been selected. The app should also highlight the route on the map surface.
+ * <li>A navigate action has been triggered. The app should begin navigation using the selected
+ * route.
+ * </ol>
+ *
+ * <p>The template itself does not expose a drawing surface. In order to draw on the canvas, use
+ * {@link androidx.car.app.AppManager#setSurfaceListener(SurfaceListener)}.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template is
+ * considered a refresh of a previous one if:
+ *
+ * <ul>
+ * <li>The template title has not changed, and
+ * <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ * number of rows and the string contents (title, texts, not counting spans) of each row
+ * between the previous and new {@link ItemList}s have not changed.
+ * </ul>
+ *
+ * <p>In order to use this template your car app <b>MUST</b> declare that it uses the {@code
+ * androidx.car.app.NAVIGATION_TEMPLATES} permission in the manifest.
+ */
+public final class RoutePreviewNavigationTemplate implements Template {
+ @Keep
+ private final boolean mIsLoading;
+ @Keep
+ @Nullable
+ private final CarText mTitle;
+ @Keep
+ @Nullable
+ private final Action mNavigateAction;
+ @Keep
+ @Nullable
+ private final ItemList mItemList;
+ @Keep
+ @Nullable
+ private final Action mHeaderAction;
+ @Keep
+ @Nullable
+ private final ActionStrip mActionStrip;
+
+ /** Constructs a new builder of {@link RoutePreviewNavigationTemplate}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the {@link CarText} that should be used as the title in the template.
+ */
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Nullable
+ public Action getNavigateAction() {
+ return mNavigateAction;
+ }
+
+ @Nullable
+ public ItemList getItemList() {
+ return mItemList;
+ }
+
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ @Override
+ public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+ if (oldTemplate.getClass() != this.getClass()) {
+ return false;
+ }
+
+ RoutePreviewNavigationTemplate old = (RoutePreviewNavigationTemplate) oldTemplate;
+ if (!Objects.equals(old.getTitle(), getTitle())) {
+ return false;
+ }
+
+ if (old.mIsLoading) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (mIsLoading) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
+ }
+
+ @Override
+ public void checkPermissions(@NonNull Context context) {
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "RoutePreviewNavigationTemplate";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTitle, mIsLoading, mNavigateAction, mItemList, mHeaderAction,
+ mActionStrip);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof RoutePreviewNavigationTemplate)) {
+ return false;
+ }
+ RoutePreviewNavigationTemplate otherTemplate = (RoutePreviewNavigationTemplate) other;
+
+ return mIsLoading == otherTemplate.mIsLoading
+ && Objects.equals(mTitle, otherTemplate.mTitle)
+ && Objects.equals(mNavigateAction, otherTemplate.mNavigateAction)
+ && Objects.equals(mItemList, otherTemplate.mItemList)
+ && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+ && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+ }
+
+ private RoutePreviewNavigationTemplate(Builder builder) {
+ mTitle = builder.mTitle;
+ mIsLoading = builder.mIsLoading;
+ mNavigateAction = builder.mNavigateAction;
+ mItemList = builder.mItemList;
+ mHeaderAction = builder.mHeaderAction;
+ mActionStrip = builder.mActionStrip;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private RoutePreviewNavigationTemplate() {
+ mTitle = null;
+ mIsLoading = false;
+ mNavigateAction = null;
+ mItemList = null;
+ mHeaderAction = null;
+ mActionStrip = null;
+ }
+
+ /** A builder of {@link RoutePreviewNavigationTemplate}. */
+ public static final class Builder {
+ @Nullable
+ private CarText mTitle;
+ private boolean mIsLoading;
+ @Nullable
+ private Action mNavigateAction;
+ @Nullable
+ private ItemList mItemList;
+ @Nullable
+ private Action mHeaderAction;
+ @Nullable
+ private ActionStrip mActionStrip;
+
+ /** Sets the {@link CharSequence} to show as title, or {@code null} to not show a title. */
+ @NonNull
+ public Builder setTitle(@Nullable CharSequence title) {
+ this.mTitle = title == null ? null : CarText.create(title);
+ return this;
+ }
+
+ /**
+ * Sets whether the template is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will show a loading indicator where the list content
+ * would be otherwise. The caller is expected to call
+ * {@link androidx.car.app.Screen#invalidate()} and send the new template content to the
+ * host
+ * once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
+ * contents added via {@link #setItemList}.
+ */
+ // TODO(rampara): Consider renaming to setLoading()
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setIsLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} that will be displayed in the header of the template, or
+ * {@code null} to now display an action.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template only supports either either one of {@link Action#APP_ICON} and {@link
+ * Action#BACK} as a header {@link Action}.
+ *
+ * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setHeaderAction(@Nullable Action headerAction) {
+ ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+ headerAction == null ? Collections.emptyList()
+ : Collections.singletonList(headerAction));
+ this.mHeaderAction = headerAction;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Action} to allow users to request navigation using the currently selected
+ * route.
+ *
+ * <p>This should not be {@code null} if the template is not in a loading state (see
+ * #setIsLoading}), and the {@link Action}'s title must be set.
+ *
+ * @throws NullPointerException if {@code navigateAction} is {@code null}.
+ * @throws IllegalArgumentException if {@code navigateAction}'s title is {@code null} or
+ * empty.
+ */
+ @NonNull
+ public Builder setNavigateAction(@NonNull Action navigateAction) {
+ if (CarText.isNullOrEmpty(requireNonNull(navigateAction).getTitle())) {
+ throw new IllegalArgumentException("The Action's title cannot be null or empty");
+ }
+
+ this.mNavigateAction = requireNonNull(navigateAction);
+
+ return this;
+ }
+
+ /**
+ * Sets an {@link ItemList} to show route options in a list view along with the map.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 3 {@link Row}s in the {@link ItemList}. The host will
+ * ignore any items over that limit. The list must have an {@link OnClickListener} set. Each
+ * {@link Row} can add up to 2 lines of texts via {@link Row.Builder#addText} and cannot
+ * contain a {@link Toggle}.
+ *
+ * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
+ *
+ * <p>All rows must have either a {@link
+ * androidx.car.app.model.DistanceSpan} or a {@link
+ * androidx.car.app.model.DurationSpan} attached to either its title or texts, to
+ * indicate an estimate trip distance or duration for the route it represents. Where in
+ * the title or text these spans are attached to is up to the app.
+ *
+ * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setItemList(@Nullable ItemList itemList) {
+ if (itemList != null) {
+ ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW.validateOrThrow(itemList);
+ ModelUtils.validateAllRowsHaveDistanceOrDuration(itemList.getItems());
+ ModelUtils.validateAllRowsHaveOnlySmallImages(itemList.getItems());
+
+ if (!itemList.getItems().isEmpty() && itemList.getOnSelectedListener() == null) {
+ throw new IllegalArgumentException(
+ "The OnSelectedListener must be set for the route list");
+ }
+ }
+ this.mItemList = itemList;
+
+ return this;
+ }
+
+ /**
+ * Sets an {@link ItemList} for the template. This method does not enforce the
+ * template's requirements and is only intended for testing purposes.
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @VisibleForTesting
+ @NonNull
+ public Builder setItemListForTesting(@Nullable ItemList itemList) {
+ this.mItemList = itemList;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ActionStrip} for this template, or {@code null} to not show an {@link
+ * ActionStrip}.
+ *
+ * <h4>Requirements</h4>
+ *
+ * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2
+ * allowed {@link Action}s, one of them can contain a title as set via
+ * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+ *
+ * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+ ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+ actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+ this.mActionStrip = actionStrip;
+ return this;
+ }
+
+ /**
+ * Constructs the template defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * Either a header {@link Action} or title must be set on the template.
+ *
+ * @throws IllegalStateException if the template is in a loading state but the list is
+ * set, or vice-versa.
+ * @throws IllegalStateException if the template is not loading and the navigation action
+ * is not set.
+ * @throws IllegalStateException if the template does not have either a title or header
+ * {@link Action} set.
+ */
+ @NonNull
+ public RoutePreviewNavigationTemplate build() {
+ boolean hasList = mItemList != null;
+ if (mIsLoading == hasList) {
+ throw new IllegalStateException(
+ "Template is in a loading state but a list is set, or vice versa.");
+ }
+
+ if (!mIsLoading) {
+ if (mNavigateAction == null) {
+ throw new IllegalStateException(
+ "The navigation action cannot be null when the list is not in a "
+ + "loading state");
+ }
+ }
+
+ if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+ throw new IllegalStateException("Either the title or header action must be set");
+ }
+
+ return new RoutePreviewNavigationTemplate(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
new file mode 100644
index 0000000..1de529f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
@@ -0,0 +1,265 @@
+/*
+ * 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.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo;
+
+import java.util.Objects;
+
+/**
+ * Represents routing information that can be shown in the {@link NavigationTemplate} during
+ * navigation
+ */
+public class RoutingInfo implements NavigationInfo {
+ @Keep
+ @Nullable
+ private final Step mCurrentStep;
+ @Keep
+ @Nullable
+ private final Distance mCurrentDistance;
+ @Keep
+ @Nullable
+ private final Step mNextStep;
+ @Keep
+ @Nullable
+ private final CarIcon mJunctionImage;
+ @Keep
+ private final boolean mIsLoading;
+
+ /** Constructs a new builder of {@link RoutingInfo}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Nullable
+ public Step getCurrentStep() {
+ return requireNonNull(mCurrentStep);
+ }
+
+ @Nullable
+ public Distance getCurrentDistance() {
+ return requireNonNull(mCurrentDistance);
+ }
+
+ @Nullable
+ public Step getNextStep() {
+ return mNextStep;
+ }
+
+ @Nullable
+ public CarIcon getJunctionImage() {
+ return mJunctionImage;
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "RoutingInfo";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCurrentStep, mCurrentDistance, mNextStep, mJunctionImage, mIsLoading);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof RoutingInfo)) {
+ return false;
+ }
+ RoutingInfo otherInfo = (RoutingInfo) other;
+
+ return mIsLoading == otherInfo.mIsLoading
+ && Objects.equals(mCurrentStep, otherInfo.mCurrentStep)
+ && Objects.equals(mCurrentDistance, otherInfo.mCurrentDistance)
+ && Objects.equals(mNextStep, otherInfo.mNextStep)
+ && Objects.equals(mJunctionImage, otherInfo.mJunctionImage);
+ }
+
+ private RoutingInfo(Builder builder) {
+ mCurrentStep = builder.mCurrentStep;
+ mCurrentDistance = builder.mCurrentDistance;
+ mNextStep = builder.mNextStep;
+ mJunctionImage = builder.mJunctionImage;
+ mIsLoading = builder.mIsLoading;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private RoutingInfo() {
+ mCurrentStep = null;
+ mCurrentDistance = null;
+ mNextStep = null;
+ mJunctionImage = null;
+ mIsLoading = false;
+ }
+
+ /** A builder of {@link RoutingInfo}. */
+ public static final class Builder {
+ @Nullable
+ private Step mCurrentStep;
+ @Nullable
+ private Distance mCurrentDistance;
+ @Nullable
+ private Step mNextStep;
+ @Nullable
+ private CarIcon mJunctionImage;
+ private boolean mIsLoading;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the current {@link Step} and {@link Distance} to display in the template.
+ *
+ * <p>A {@link Step} with a {@link Maneuver} of type {@link Maneuver#TYPE_UNKNOWN} will
+ * shown here with the given icon.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * Images in the cue of the {@link Step} object, set with {@link Step.Builder#setCue}, can
+ * contain image spans. If necessary, those images in the spans will be scaled down to fit
+ * within a 108 x 36 dp bounding box, while preserving their aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setCurrentStep(@NonNull Step currentStep,
+ @NonNull Distance currentDistance) {
+ this.mCurrentStep = requireNonNull(currentStep);
+ this.mCurrentDistance = requireNonNull(currentDistance);
+ return this;
+ }
+
+ /**
+ * Sets the next {@link Step} or {@code null} to not display it.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * Images in the cue of the {@link Step} object, set with {@link Step.Builder#setCue}, can
+ * contain image spans. If necessary, those images in the spans will be scaled down to fit
+ * within a 108 x 32 dp bounding box, while preserving their aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setNextStep(@Nullable Step nextStep) {
+ this.mNextStep = nextStep;
+ return this;
+ }
+
+ /**
+ * Sets an image of a junction for the maneuver or {@code null} to not show a junction
+ * image.
+ *
+ * <p>For example, a photo-realistic view of the upcoming junction that the driver can
+ * see when executing the maneuver.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * The image may be scaled down to fit a rectangle of 320 x 200 dp while preserving the
+ * aspect ratio. On smaller screens the junction image may result in the hiding of the
+ * {@link Lane}s, {@link TravelEstimate} or next {@link Step}. The aspect ratio should be
+ * greater than or equal to 1.6 in order to fit the horizontal space fully.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setJunctionImage(@Nullable CarIcon junctionImage) {
+ CarIconConstraints.DEFAULT.validateOrThrow(junctionImage);
+ this.mJunctionImage = junctionImage;
+ return this;
+ }
+
+ /**
+ * Sets whether the {@link RoutingInfo} is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI will show a loading indicator, and adding any other
+ * routing info will throw an {@link IllegalArgumentException}. The caller is expected to
+ * call {@link androidx.car.app.Screen#invalidate()} and send the new template content
+ * to the host once the data is ready. If set to {@code false}, the UI shows the actual
+ * routing info.
+ *
+ * @see #build
+ */
+ // TODO(rampara): Consider renaming to setLoading()
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setIsLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link RoutingInfo} defined by this builder.
+ *
+ * <h4>Requirements</h4>
+ *
+ * The {@link RoutingInfo} can be in a loading state by passing {@code true} to {@link
+ * #setIsLoading(boolean)}, in which case no other fields may be set. Otherwise, the current
+ * step and distance must be set. If the lane information is set with {@link
+ * Step.Builder#addLane(Lane)}, then the lane image must also be set with {@link
+ * Step.Builder#setLanesImage(CarIcon)}.
+ *
+ * @throws IllegalStateException if the {@link RoutingInfo} does not meet the template's
+ * requirements.
+ */
+ @NonNull
+ public RoutingInfo build() {
+ Step current = mCurrentStep;
+ Distance distance = mCurrentDistance;
+
+ if (mIsLoading) {
+ if (current != null || distance != null || mNextStep != null
+ || mJunctionImage != null) {
+ throw new IllegalStateException(
+ "The routing info is set to loading but is not empty");
+ }
+ } else {
+ if (current == null || distance == null) {
+ throw new IllegalStateException(
+ "Current step and distance must be set during the navigating state");
+ }
+ if (!current.getLanes().isEmpty() && current.getLanesImage() == null) {
+ // TODO(b/154660041): Remove restriction when lane image can be draw from
+ // lane info.
+ throw new IllegalStateException(
+ "Current step must have a lanes image if the lane information is set.");
+ }
+ }
+ return new RoutingInfo(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
new file mode 100644
index 0000000..7a1c100
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
@@ -0,0 +1,324 @@
+/*
+ * 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.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.utils.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a step that the driver should take in order to remain on the current navigation route.
+ *
+ * <p>Example of steps are turning onto a street, taking a highway exit and merging onto a different
+ * highway, or continuing straight through a roundabout.
+ */
+public final class Step {
+ @Keep
+ @Nullable
+ private final Maneuver mManeuver;
+ @Keep
+ private final List<Lane> mLanes;
+ @Keep
+ @Nullable
+ private final CarIcon mLanesImage;
+ @Keep
+ @Nullable
+ private final CarText mCue;
+ @Keep
+ @Nullable
+ private final CarText mRoad;
+
+ /**
+ * Constructs a new builder of {@link Step} with a cue.
+ *
+ * <p>A cue must always be set when the step is created and is used as a fallback when {@link
+ * Maneuver} is not set or is unavailable.
+ *
+ * @throws NullPointerException if {@code cue} is {@code null}.
+ * @see Builder#setCue(CharSequence)
+ */
+ @NonNull
+ public static Builder builder(@NonNull CharSequence cue) {
+ return new Builder(requireNonNull(cue));
+ }
+
+ /**
+ * Returns a new {@link Builder} instance configured with the same data as this {@link Step}
+ * instance.
+ */
+ @NonNull
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ @Nullable
+ public Maneuver getManeuver() {
+ return mManeuver;
+ }
+
+ @NonNull
+ public List<Lane> getLanes() {
+ return CollectionUtils.emptyIfNull(mLanes);
+ }
+
+ @Nullable
+ public CarIcon getLanesImage() {
+ return mLanesImage;
+ }
+
+ @Nullable
+ public CarText getCue() {
+ return mCue;
+ }
+
+ @Nullable
+ public CarText getRoad() {
+ return mRoad;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[maneuver: "
+ + mManeuver
+ + ", lane count: "
+ + (mLanes != null ? mLanes.size() : 0)
+ + ", lanes image: "
+ + mLanesImage
+ + ", cue: "
+ + CarText.toShortString(mCue)
+ + ", road: "
+ + CarText.toShortString(mRoad)
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mManeuver, mLanes, mLanesImage, mCue, mRoad);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Step)) {
+ return false;
+ }
+
+ Step otherStep = (Step) other;
+ return Objects.equals(mManeuver, otherStep.mManeuver)
+ && Objects.equals(mLanes, otherStep.mLanes)
+ && Objects.equals(mLanesImage, otherStep.mLanesImage)
+ && Objects.equals(mCue, otherStep.mCue)
+ && Objects.equals(mRoad, otherStep.mRoad);
+ }
+
+ private Step(
+ @Nullable Maneuver maneuver,
+ List<Lane> lanes,
+ @Nullable CarIcon lanesImage,
+ @Nullable CarText cue,
+ @Nullable CarText road) {
+ this.mManeuver = maneuver;
+ this.mLanes = new ArrayList<>(lanes);
+ this.mLanesImage = lanesImage;
+ this.mCue = cue;
+ this.mRoad = road;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Step() {
+ mManeuver = null;
+ mLanes = Collections.emptyList();
+ mLanesImage = null;
+ mCue = null;
+ mRoad = null;
+ }
+
+ /** A builder of {@link Step}. */
+ public static final class Builder {
+ private final List<Lane> mLanes = new ArrayList<>();
+ @Nullable
+ private Maneuver mManeuver;
+ @Nullable
+ private CarIcon mLanesImage;
+ private CarText mCue;
+ @Nullable
+ private CarText mRoad;
+
+ private Builder(CharSequence cue) {
+ this.mCue = CarText.create(cue);
+ }
+
+ private Builder(Step step) {
+ this.mManeuver = step.mManeuver;
+ this.mLanes.clear();
+ this.mLanes.addAll(step.mLanes);
+ this.mLanesImage = step.mLanesImage;
+ this.mCue = requireNonNull(step.mCue);
+ this.mRoad = step.mRoad;
+ }
+
+ /**
+ * Sets the maneuver to be performed on this step or {@code null} if this step doesn't
+ * involve a
+ * maneuver.
+ */
+ @NonNull
+ public Builder setManeuver(@Nullable Maneuver maneuver) {
+ this.mManeuver = maneuver;
+ return this;
+ }
+
+ /**
+ * Adds the information of a single road lane at the point where the driver should
+ * execute this step.
+ *
+ * <p>Lane information is primarily used when the step is passed to the vehicle cluster
+ * or heads up displays. Some vehicles may not use the information at all. The navigation
+ * template primarily uses the lanes image provided in {@link #setLanesImage}.
+ *
+ * <p>Lanes are displayed from left to right.
+ */
+ @NonNull
+ public Builder addLane(@NonNull Lane lane) {
+ requireNonNull(lane);
+ mLanes.add(lane);
+ return this;
+ }
+
+ /**
+ * Clears any lanes that may have been added with {@link #addLane(Lane)} up to this
+ * point.
+ */
+ @NonNull
+ public Builder clearLanes() {
+ mLanes.clear();
+ return this;
+ }
+
+ /**
+ * Sets an image representing all the lanes or {@code null} if no lanes image is available.
+ *
+ * <p>This image takes priority over {@link Lane}s that may have been added with {@link
+ * #addLane}. If an image is added for the lanes with this method then corresponding lane
+ * data using {@link #addLane} must also have been added in case it is shown on a display
+ * with limited resources such as the car cluster or heads-up display (HUD).
+ *
+ * <p>This image should ideally have a transparent background.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * The provided image should have a maximum size of 294 x 44 dp. If the image exceeds this
+ * maximum size in either one of the dimensions, it will be scaled down and centered
+ * inside the bounding box while preserving the aspect ratio.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ */
+ @NonNull
+ public Builder setLanesImage(@Nullable CarIcon lanesImage) {
+ this.mLanesImage = lanesImage;
+ return this;
+ }
+
+ /**
+ * Sets a text description of this maneuver.
+ *
+ * <p>Must always be set when the step is created and is used as a fallback when {@link
+ * Maneuver} is not set or is unavailable.
+ *
+ * <p>For example "Turn left", "Make a U-Turn", "Sharp Right", or "Take the exit using
+ * the left lane"
+ *
+ * <p>The {@code cue} string can contain images that replace spans of text by using {@link
+ * androidx.car.app.model.CarIconSpan}.
+ *
+ * <p>In the following example, the "520" text is replaced with an icon:
+ *
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Turn right on 520 East");
+ * string.setSpan(textWithImage.setSpan(
+ * CarIconSpan.create(CarIcon.of(
+ * IconCompat.createWithResource(getCarContext(), R.drawable.ic_520_highway))),
+ * 14, 17, SPAN_INCLUSIVE_EXCLUSIVE));
+ * }</pre>
+ *
+ * <p>The host may choose to display the string without the images, so it is important
+ * that the string content is readable without the images. This may be the case, for
+ * example, if the string is sent to a cluster display that does not support images, or
+ * if the host limits the number of images that may be allowed for one line of text.
+ *
+ * <h4>Image Sizing Guidance</h4>
+ *
+ * The size these images will be displayed at varies depending on where the {@link Step}
+ * object is used. Refer to the documentation of those APIs for details.
+ *
+ * <p>See {@link CarIcon} for more details related to providing icon and image resources
+ * that work with different car screen pixel densities.
+ *
+ * @throws NullPointerException if {@code cue} is {@code null}
+ */
+ @NonNull
+ public Builder setCue(@NonNull CharSequence cue) {
+ this.mCue = CarText.create(requireNonNull(cue));
+ return this;
+ }
+
+ /**
+ * Sets a text description of the road for the step or {@code null} if unknown.
+ *
+ * <p>This value is primarily used for vehicle cluster and heads-up displays and may not
+ * appear
+ * in the navigation template.
+ *
+ * <p>For example, a {@link Step} for a left turn might provide "State Street" for the road.
+ *
+ * @throws NullPointerException if {@code destinations} is {@code null}
+ */
+ @NonNull
+ public Builder setRoad(@NonNull CharSequence road) {
+ this.mRoad = CarText.create(requireNonNull(road));
+ return this;
+ }
+
+ /**
+ * Constructs the {@link Step} defined by this builder.
+ *
+ * @throws IllegalStateException if {@code lanesImage} was set but no lanes were added.
+ */
+ @NonNull
+ public Step build() {
+ if (mLanesImage != null && mLanes.isEmpty()) {
+ throw new IllegalStateException(
+ "A step must have lane data when the lanes image is set.");
+ }
+ return new Step(mManeuver, mLanes, mLanesImage, mCue, mRoad);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
new file mode 100644
index 0000000..cda7135
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
@@ -0,0 +1,322 @@
+/*
+ * 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.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.constraints.CarColorConstraints;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Objects;
+
+/**
+ * Represents the travel estimates to a destination of a trip or for a trip segment, including the
+ * remaining time and distance to the destination.
+ */
+@SuppressWarnings("MissingSummary")
+public final class TravelEstimate {
+ @Keep
+ @Nullable
+ private final Distance mRemainingDistance;
+ @Keep
+ private final long mRemainingTimeSeconds;
+ @Keep
+ @Nullable
+ private final DateTimeWithZone mArrivalTimeAtDestination;
+ @Keep
+ private final CarColor mRemainingTimeColor;
+ @Keep
+ private final CarColor mRemainingDistanceColor;
+
+ /**
+ * Returns a new instance of a {@link TravelEstimate} for the given time and distance
+ * parameters.
+ *
+ * @param remainingDistance The estimated remaining {@link Distance} until arriving at
+ * the destination.
+ * @param remainingTimeSeconds The estimated time remaining until arriving at the
+ * destination, in seconds.
+ * @param arrivalTimeAtDestination The arrival time with the time zone information provided
+ * for the destination.
+ * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value.
+ * @throws NullPointerException if {@code remainingDistance} is {@code null}
+ * @throws NullPointerException if {@code arrivalTimeAtDestination} is {@code null}
+ */
+ @NonNull
+ public static TravelEstimate create(
+ @NonNull Distance remainingDistance,
+ long remainingTimeSeconds,
+ @NonNull DateTimeWithZone arrivalTimeAtDestination) {
+ return builder(remainingDistance, remainingTimeSeconds, arrivalTimeAtDestination).build();
+ }
+
+ /**
+ * Returns a new instance of a {@link TravelEstimate} for the given time and distance
+ * parameters.
+ *
+ * @param remainingDistance The estimated remaining {@link Distance} until arriving at
+ * the destination.
+ * @param remainingTime The estimated time remaining until arriving at the
+ * destination.
+ * @param arrivalTimeAtDestination The arrival time with the time zone information provided for
+ * the destination.
+ * @throws IllegalArgumentException if {@code remainingTime} contains a negative duration.
+ * @throws NullPointerException if {@code remainingDistance} is {@code null}
+ * @throws NullPointerException if {@code remainingTime} is {@code null}
+ * @throws NullPointerException if {@code arrivalTimeAtDestination} is {@code null}
+ */
+ @RequiresApi(26)
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @NonNull
+ public static TravelEstimate create(
+ @NonNull Distance remainingDistance,
+ @NonNull Duration remainingTime,
+ @NonNull ZonedDateTime arrivalTimeAtDestination) {
+ return builder(remainingDistance, remainingTime, arrivalTimeAtDestination).build();
+ }
+
+ /**
+ * Constructs a new builder of {@link TravelEstimate}.
+ *
+ * @param remainingDistance The estimated remaining {@link Distance} until arriving at
+ * the destination.
+ * @param remainingTimeSeconds The estimated time remaining until arriving at the
+ * destination, in seconds.
+ * @param arrivalTimeAtDestination The arrival time with the time zone information provided
+ * for the destination.
+ * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value.
+ * @throws NullPointerException if {@code remainingDistance} is {@code null}
+ * @throws NullPointerException if {@code arrivalTimeAtDestination} is {@code null}
+ */
+ @NonNull
+ public static Builder builder(
+ @NonNull Distance remainingDistance,
+ long remainingTimeSeconds,
+ @NonNull DateTimeWithZone arrivalTimeAtDestination) {
+ return new Builder(
+ requireNonNull(remainingDistance),
+ remainingTimeSeconds,
+ requireNonNull(arrivalTimeAtDestination));
+ }
+
+ /**
+ * Constructs a new builder of {@link TravelEstimate}.
+ *
+ * @param remainingDistance The estimated remaining {@link Distance} until arriving at
+ * the destination.
+ * @param remainingTime The estimated time remaining until arriving at the
+ * destination.
+ * @param arrivalTimeAtDestination The arrival time with the time zone information provided for
+ * the destination.
+ * @throws IllegalArgumentException if {@code remainingTime} contains a negative duration.
+ * @throws NullPointerException if {@code remainingDistance} is {@code null}
+ * @throws NullPointerException if {@code remainingTime} is {@code null}
+ * @throws NullPointerException if {@code arrivalTimeAtDestination} is {@code null}
+ */
+ @NonNull
+ @RequiresApi(26)
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ public static Builder builder(
+ @NonNull Distance remainingDistance,
+ @NonNull Duration remainingTime,
+ @NonNull ZonedDateTime arrivalTimeAtDestination) {
+ return new Builder(
+ requireNonNull(remainingDistance),
+ requireNonNull(remainingTime),
+ requireNonNull(arrivalTimeAtDestination));
+ }
+
+ @NonNull
+ public Distance getRemainingDistance() {
+ return requireNonNull(mRemainingDistance);
+ }
+
+ // TODO(rampara): Returned time values must be in milliseconds
+ @SuppressWarnings("MethodNameUnits")
+ public long getRemainingTimeSeconds() {
+ return mRemainingTimeSeconds;
+ }
+
+ @Nullable
+ public DateTimeWithZone getArrivalTimeAtDestination() {
+ return mArrivalTimeAtDestination;
+ }
+
+ @NonNull
+ public CarColor getRemainingTimeColor() {
+ return mRemainingTimeColor;
+ }
+
+ @NonNull
+ public CarColor getRemainingDistanceColor() {
+ return mRemainingDistanceColor;
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ // TODO(rampara): Move API 26 calls into separate class.
+ @Override
+ @NonNull
+ @RequiresApi(26)
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ public String toString() {
+ return "[ remaining distance: "
+ + mRemainingDistance
+ + ", time: "
+ + Duration.ofSeconds(mRemainingTimeSeconds)
+ + ", ETA: "
+ + mArrivalTimeAtDestination
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mRemainingDistance,
+ mRemainingTimeSeconds,
+ mArrivalTimeAtDestination,
+ mRemainingTimeColor,
+ mRemainingDistanceColor);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof TravelEstimate)) {
+ return false;
+ }
+ TravelEstimate otherInfo = (TravelEstimate) other;
+
+ return Objects.equals(mRemainingDistance, otherInfo.mRemainingDistance)
+ && mRemainingTimeSeconds == otherInfo.mRemainingTimeSeconds
+ && Objects.equals(mArrivalTimeAtDestination, otherInfo.mArrivalTimeAtDestination)
+ && Objects.equals(mRemainingTimeColor, otherInfo.mRemainingTimeColor)
+ && Objects.equals(mRemainingDistanceColor, otherInfo.mRemainingDistanceColor);
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private TravelEstimate() {
+ mRemainingDistance = null;
+ mRemainingTimeSeconds = 0;
+ mArrivalTimeAtDestination = null;
+ mRemainingTimeColor = CarColor.DEFAULT;
+ mRemainingDistanceColor = CarColor.DEFAULT;
+ }
+
+ private TravelEstimate(Builder builder) {
+ this.mRemainingDistance = builder.mRemainingDistance;
+ this.mRemainingTimeSeconds = builder.mRemainingTimeSeconds;
+ this.mArrivalTimeAtDestination = builder.mArrivalTimeAtDestination;
+ this.mRemainingTimeColor = builder.mRemainingTimeColor;
+ this.mRemainingDistanceColor = builder.mRemainingDistanceColor;
+ }
+
+ /** A builder of {@link TravelEstimate}. */
+ public static final class Builder {
+ private final Distance mRemainingDistance;
+ private final long mRemainingTimeSeconds;
+ private final DateTimeWithZone mArrivalTimeAtDestination;
+ private CarColor mRemainingTimeColor = CarColor.DEFAULT;
+ private CarColor mRemainingDistanceColor = CarColor.DEFAULT;
+
+ private Builder(
+ Distance remainingDistance,
+ long remainingTimeSeconds,
+ DateTimeWithZone arrivalTimeAtDestination) {
+ this.mRemainingDistance = requireNonNull(remainingDistance);
+ this.mRemainingTimeSeconds = validateRemainingTime(remainingTimeSeconds);
+ this.mArrivalTimeAtDestination = requireNonNull(arrivalTimeAtDestination);
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ // TODO(rampara): Move API 26 calls into separate class.
+ @RequiresApi(26)
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ private Builder(
+ Distance remainingDistance,
+ Duration remainingTime,
+ ZonedDateTime arrivalTimeAtDestination) {
+ this.mRemainingDistance = remainingDistance;
+ this.mRemainingTimeSeconds = validateRemainingTime(remainingTime.getSeconds());
+ this.mArrivalTimeAtDestination = DateTimeWithZone.create(arrivalTimeAtDestination);
+ }
+
+ /**
+ * Sets the color of the remaining time text.
+ *
+ * <p>The host may ignore this color depending on the capabilities of the target screen.
+ *
+ * <p>If not set, {@link CarColor#DEFAULT} will be used.
+ *
+ * <p>Custom colors created with {@link CarColor#createCustom} are not supported.
+ *
+ * @throws IllegalArgumentException if {@code remainingTimeColor} is not supported.
+ * @throws NullPointerException if {@code remainingTimecolor} is {@code null}
+ */
+ @NonNull
+ public Builder setRemainingTimeColor(@NonNull CarColor remainingTimeColor) {
+ CarColorConstraints.STANDARD_ONLY.validateOrThrow(requireNonNull(remainingTimeColor));
+ this.mRemainingTimeColor = remainingTimeColor;
+ return this;
+ }
+
+ /**
+ * Sets the color of the remaining distance text.
+ *
+ * <p>The host may ignore this color depending on the capabilities of the target screen.
+ *
+ * <p>If not set, {@link CarColor#DEFAULT} will be used.
+ *
+ * <p>Custom colors created with {@link CarColor#createCustom} are not supported.
+ *
+ * @throws IllegalArgumentException if {@code remainingDistanceColor} is not supported.
+ * @throws NullPointerException if {@code remainingDistanceColor} is {@code null}.
+ */
+ @NonNull
+ public Builder setRemainingDistanceColor(@NonNull CarColor remainingDistanceColor) {
+ CarColorConstraints.STANDARD_ONLY.validateOrThrow(
+ requireNonNull(remainingDistanceColor));
+ this.mRemainingDistanceColor = remainingDistanceColor;
+ return this;
+ }
+
+ /** Constructs the {@link TravelEstimate} defined by this builder. */
+ @NonNull
+ public TravelEstimate build() {
+ return new TravelEstimate(this);
+ }
+
+ private static long validateRemainingTime(long remainingTimeSeconds) {
+ if (remainingTimeSeconds < 0) {
+ throw new IllegalArgumentException(
+ "Remaining time must be a larger than or equal to zero");
+ }
+ return remainingTimeSeconds;
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
new file mode 100644
index 0000000..d3f7b5d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
@@ -0,0 +1,297 @@
+/*
+ * 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.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents information about a trip including destinations, steps, and travel estimates.
+ *
+ * <p>This information data <b>may</b> be displayed in different places in the car such as the
+ * instrument cluster screens and heads-up display.
+ */
+public final class Trip {
+ @Keep
+ private final List<Destination> mDestinations;
+ @Keep
+ private final List<Step> mSteps;
+ @Keep
+ private final List<TravelEstimate> mDestinationTravelEstimates;
+ @Keep
+ private final List<TravelEstimate> mStepTravelEstimates;
+ @Keep
+ @Nullable
+ private final CarText mCurrentRoad;
+ @Keep
+ private final boolean mIsLoading;
+
+ /** Constructs a new builder of {@link Trip}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @NonNull
+ public List<Destination> getDestinations() {
+ return mDestinations;
+ }
+
+ @NonNull
+ public List<Step> getSteps() {
+ return mSteps;
+ }
+
+ @NonNull
+ public List<TravelEstimate> getDestinationTravelEstimates() {
+ return mDestinationTravelEstimates;
+ }
+
+ @NonNull
+ public List<TravelEstimate> getStepTravelEstimates() {
+ return mStepTravelEstimates;
+ }
+
+ @Nullable
+ public CarText getCurrentRoad() {
+ return mCurrentRoad;
+ }
+
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[ destinations : "
+ + mDestinations.toString()
+ + ", steps: "
+ + mSteps.toString()
+ + ", dest estimates: "
+ + mDestinationTravelEstimates.toString()
+ + ", step estimates: "
+ + mStepTravelEstimates.toString()
+ + ", road: "
+ + CarText.toShortString(mCurrentRoad)
+ + ", isLoading: "
+ + mIsLoading
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mDestinations, mSteps, mDestinationTravelEstimates, mStepTravelEstimates,
+ mCurrentRoad);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Trip)) {
+ return false;
+ }
+
+ Trip otherTrip = (Trip) other;
+ return Objects.equals(mDestinations, otherTrip.mDestinations)
+ && Objects.equals(mSteps, otherTrip.mSteps)
+ && Objects.equals(
+ mDestinationTravelEstimates, otherTrip.mDestinationTravelEstimates)
+ && Objects.equals(mStepTravelEstimates, otherTrip.mStepTravelEstimates)
+ && Objects.equals(mCurrentRoad, otherTrip.mCurrentRoad)
+ && Objects.equals(mIsLoading, otherTrip.mIsLoading);
+ }
+
+ private Trip(Builder builder) {
+ this.mDestinations = new ArrayList<>(builder.mDestinations);
+ this.mSteps = new ArrayList<>(builder.mSteps);
+ this.mDestinationTravelEstimates = new ArrayList<>(builder.mDestinationTravelEstimates);
+ this.mStepTravelEstimates = new ArrayList<>(builder.mStepTravelEstimates);
+ this.mCurrentRoad = builder.mCurrentRoad;
+ this.mIsLoading = builder.mIsLoading;
+ }
+
+ /** Constructs an empty instance, used by serialization code. */
+ private Trip() {
+ mDestinations = Collections.emptyList();
+ mSteps = Collections.emptyList();
+ mDestinationTravelEstimates = Collections.emptyList();
+ mStepTravelEstimates = Collections.emptyList();
+ mCurrentRoad = null;
+ mIsLoading = false;
+ }
+
+ /** A builder of {@link Trip}. */
+ public static final class Builder {
+ private final List<Destination> mDestinations = new ArrayList<>();
+ private final List<Step> mSteps = new ArrayList<>();
+ private final List<TravelEstimate> mDestinationTravelEstimates = new ArrayList<>();
+ private final List<TravelEstimate> mStepTravelEstimates = new ArrayList<>();
+ @Nullable
+ private CarText mCurrentRoad;
+ private boolean mIsLoading;
+
+ /**
+ * Adds a destination to the trip.
+ *
+ * <p>Destinations must be added in order of arrival. A destination is not required. Display
+ * surfaces may or may not use the destination and if multiple destinations are added the
+ * display may only show information about the first destination.
+ *
+ * <p>For every destination added, a corresponding {@link TravelEstimate} must be added via
+ * {@link #addDestinationTravelEstimate}.They are added separately so that travel
+ * estimates can be updated frequently based on location.
+ */
+ @NonNull
+ public Builder addDestination(@NonNull Destination destination) {
+ mDestinations.add(requireNonNull(destination));
+ return this;
+ }
+
+ /** Clears the list of destinations in the builder. */
+ @NonNull
+ public Builder clearDestinations() {
+ mDestinations.clear();
+ return this;
+ }
+
+ /**
+ * Adds a step to the trip.
+ *
+ * <p>Steps must be added in order of arrival. A step is not required. Display surfaces
+ * may or may not use the step and if multiple steps are added the display may only show
+ * information about the first step.
+ *
+ * <p>For every step added, a corresponding {@link TravelEstimate} must be added via {@link
+ * #addStepTravelEstimate}. They are added separately so that travel estimates can be
+ * updated frequently based on location.
+ */
+ @NonNull
+ public Builder addStep(@Nullable Step step) {
+ mSteps.add(requireNonNull(step));
+ return this;
+ }
+
+ /** Clears the list of steps in the builder. */
+ @NonNull
+ public Builder clearSteps() {
+ mSteps.clear();
+ return this;
+ }
+
+ /**
+ * Adds a destination travel estimate to the trip.
+ *
+ * <p>Destination travel estimates must be added in order of arrival. A destination travel
+ * estimate is not required. Display surfaces may or may not use the destination travel
+ * estimate and if multiple destination travel estimates are added the display may only show
+ * information about the first destination travel estimate.
+ *
+ * <p>For every destination travel estimate added, a corresponding destination must also be
+ * added. They are added separately so that travel estimates can be updated frequently
+ * based on location.
+ */
+ @NonNull
+ public Builder addDestinationTravelEstimate(
+ @NonNull TravelEstimate destinationTravelEstimate) {
+ mDestinationTravelEstimates.add(requireNonNull(destinationTravelEstimate));
+ return this;
+ }
+
+ /** Clears the list of destination travel estimates in the builder. */
+ @NonNull
+ public Builder clearDestinationTravelEstimates() {
+ mDestinationTravelEstimates.clear();
+ return this;
+ }
+
+ /**
+ * Adds a step travel estimate to the trip.
+ *
+ * <p>Step travel estimates must be added in order of arrival. A step travel estimate is not
+ * required. Display surfaces may or may not use the step travel estimate and if multiple
+ * step travel estimates are added the display may only show information about the first
+ * step travel estimate.
+ *
+ * <p>For every step travel estimate added, a corresponding step must also be added.
+ */
+ @NonNull
+ public Builder addStepTravelEstimate(@NonNull TravelEstimate stepTravelEstimate) {
+ mStepTravelEstimates.add(requireNonNull(stepTravelEstimate));
+ return this;
+ }
+
+ /** Clears the list of destination travel estimates in the builder. */
+ @NonNull
+ public Builder clearStepTravelEstimates() {
+ mStepTravelEstimates.clear();
+ return this;
+ }
+
+ /** Sets a text description of the current road or {@code null} if unknown. */
+ @NonNull
+ public Builder setCurrentRoad(@Nullable CharSequence currentRoad) {
+ this.mCurrentRoad = currentRoad == null ? null : CarText.create(currentRoad);
+ return this;
+ }
+
+ /**
+ * Sets whether the {@link Trip} is in a loading state.
+ *
+ * <p>If set to {@code true}, the UI may show a loading indicator, and adding any steps
+ * or step travel estimates will throw an {@link IllegalArgumentException}.
+ */
+ // TODO(rampara): Consider renaming to setLoading()
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setIsLoading(boolean isLoading) {
+ this.mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link Trip} defined by this builder.
+ */
+ @NonNull
+ public Trip build() {
+ if (mDestinations.size() != mDestinationTravelEstimates.size()) {
+ throw new IllegalArgumentException(
+ "Destinations and destination travel estimates sizes must match");
+ }
+ if (mSteps.size() != mStepTravelEstimates.size()) {
+ throw new IllegalArgumentException(
+ "Steps and step travel estimates sizes must match");
+ }
+ if (mIsLoading && !mSteps.isEmpty()) {
+ throw new IllegalArgumentException("Step information may not be set while loading");
+ }
+ return new Trip(this);
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java b/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java
index 26c5a04..7f325a0 100644
--- a/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java
+++ b/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java
@@ -808,8 +808,9 @@
}
}
- /** A decorator on a {@link BundlerException} that tacks the frame information on its message
- * . */
+ /**
+ * A decorator on a {@link BundlerException} that tacks the frame information on its message.
+ */
static class TracedBundlerException extends BundlerException {
TracedBundlerException(String msg, Trace trace) {
super(msg + ", frames: " + trace.toFlatString());
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java b/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
index 0a8cd1f..a402482 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
@@ -16,17 +16,11 @@
package androidx.car.app.utils;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
/**
* Logger interface to allow the host to log while using the client library.
- *
- * @hide
*/
-@RestrictTo(LIBRARY)
// TODO: Allow setting logging severity and including throwables
public interface Logger {
void log(@NonNull String message);
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
index 85d408d..1b2ed7e 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
@@ -155,7 +155,8 @@
// TODO(rampara): Change method signature to change parameter order.
@SuppressLint("LambdaLast")
public static void dispatchHostCall(
- @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback, @NonNull String callName) {
+ @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback,
+ @NonNull String callName) {
ThreadUtils.runOnMain(
() -> {
try {
@@ -188,17 +189,15 @@
public static void sendFailureResponse(@NonNull IOnDoneCallback callback,
@NonNull String callName,
@NonNull Throwable e) {
- call(
- () -> {
- try {
- callback.onFailure(Bundleable.create(new FailureResponse(e)));
- } catch (BundlerException bundlerException) {
- // Not possible, but catching since BundlerException is not runtime.
- throw new IllegalStateException(
- "Serialization failure in " + callName, bundlerException);
- }
- return null;
- },
- callName + " onFailure");
+ call(() -> {
+ try {
+ callback.onFailure(Bundleable.create(new FailureResponse(e)));
+ } catch (BundlerException bundlerException) {
+ // Not possible, but catching since BundlerException is not runtime.
+ throw new IllegalStateException(
+ "Serialization failure in " + callName, bundlerException);
+ }
+ return null;
+ }, callName + " onFailure");
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
index 1522107..3e4a283 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
@@ -19,15 +19,15 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
import java.util.List;
-
-// TODO(rampara): Uncomment on addition of model module
-//import androidx.car.app.model.CarText;
-//import androidx.car.app.model.GridItem;
-//import androidx.car.app.model.Row;
-//import androidx.car.app.model.Toggle;
+import java.util.Objects;
/**
* Shared util methods for handling different distraction validation logic.
@@ -80,22 +80,21 @@
return false;
}
- // TODO(rampara): Uncomment on addition of model module
-// if (itemObj1 instanceof Row) {
-// if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i, logger)) {
-// return false;
-// }
-// } else if (itemObj1 instanceof GridItem) {
-// if (!gridItemsHaveSameContent(
-// (GridItem) itemObj1,
-// itemList1SelectedIndex == i,
-// (GridItem) itemObj2,
-// itemList2SelectedIndex == i,
-// i,
-// logger)) {
-// return false;
-// }
-// }
+ if (itemObj1 instanceof Row) {
+ if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i, logger)) {
+ return false;
+ }
+ } else if (itemObj1 instanceof GridItem) {
+ if (!gridItemsHaveSameContent(
+ (GridItem) itemObj1,
+ itemList1SelectedIndex == i,
+ (GridItem) itemObj2,
+ itemList2SelectedIndex == i,
+ i,
+ logger)) {
+ return false;
+ }
+ }
}
return true;
@@ -105,119 +104,116 @@
* Returns {@code true} if the string contents of the two rows are equal, {@code false}
* otherwise.
*/
- // TODO(rampara): Uncomment on addition of model module
-// private static boolean rowsHaveSameContent(Row row1, Row row2, int index, Logger logger) {
-// // Special case for rows with toggles - if the toggle state has changed, then text updates
-// // are allowed.
-// if (rowToggleStateHasChanged(row1, row2)) {
-// return true;
-// }
-//
-// if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
-// logger.log(
-// "Different row titles at index "
-// + index
-// + ". Old: "
-// + row1.getTitle()
-// + ". New: "
-// + row2.getTitle());
-// return false;
-// }
-//
-// List<CarText> row1Texts = row1.getText();
-// List<CarText> row2Texts = row2.getText();
-// if (row1Texts.size() != row2Texts.size()) {
-// logger.log(
-// "Different text list size at row index "
-// + index
-// + ". Old: "
-// + row1Texts.size()
-// + ". New: "
-// + row2Texts.size());
-// return false;
-// }
-//
-// for (int j = 0; j < row1Texts.size(); j++) {
-// if (!carTextsHasSameString(row1Texts.get(j), row2Texts.get(j))) {
-// logger.log(
-// "Different texts at row index "
-// + index
-// + ". Old row: "
-// + row1Texts.get(j)
-// + ". New row: "
-// + row2Texts.get(j));
-// return false;
-// }
-// }
-//
-// return true;
-// }
+ private static boolean rowsHaveSameContent(Row row1, Row row2, int index, Logger logger) {
+ // Special case for rows with toggles - if the toggle state has changed, then text updates
+ // are allowed.
+ if (rowToggleStateHasChanged(row1, row2)) {
+ return true;
+ }
+
+ if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
+ logger.log(
+ "Different row titles at index "
+ + index
+ + ". Old: "
+ + row1.getTitle()
+ + ". New: "
+ + row2.getTitle());
+ return false;
+ }
+
+ List<CarText> row1Texts = row1.getTexts();
+ List<CarText> row2Texts = row2.getTexts();
+ if (row1Texts.size() != row2Texts.size()) {
+ logger.log(
+ "Different text list size at row index "
+ + index
+ + ". Old: "
+ + row1Texts.size()
+ + ". New: "
+ + row2Texts.size());
+ return false;
+ }
+
+ for (int j = 0; j < row1Texts.size(); j++) {
+ if (!carTextsHasSameString(row1Texts.get(j), row2Texts.get(j))) {
+ logger.log(
+ "Different texts at row index "
+ + index
+ + ". Old row: "
+ + row1Texts.get(j)
+ + ". New row: "
+ + row2Texts.get(j));
+ return false;
+ }
+ }
+
+ return true;
+ }
/**
* Returns {@code true} if string contents and images of the two grid items are equal, {@code
* false} otherwise.
*/
- // TODO(rampara): Uncomment on addition of model module
-// private static boolean gridItemsHaveSameContent(
-// GridItem gridItem1,
-// boolean isGridItem1Selected,
-// GridItem gridItem2,
-// boolean isGridItem2Selected,
-// int index,
-// Logger logger) {
-// // Special case for grid items with toggles - if the toggle state has changed, then text
-// // and image updates are allowed.
-// if (gridItemToggleStateHasChanged(gridItem1, gridItem2)) {
-// return true;
-// }
-//
-// // Special case for grid items that are selectable - if the selected state has changed,
-// then
-// // text and image updates are allowed.
-// if (isGridItem1Selected != isGridItem2Selected) {
-// return true;
-// }
-//
-// if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
-// logger.log(
-// "Different grid item titles at index "
-// + index
-// + ". Old: "
-// + gridItem1.getTitle()
-// + ". New: "
-// + gridItem2.getTitle());
-// return false;
-// }
-//
-// if (!carTextsHasSameString(gridItem1.getText(), gridItem2.getText())) {
-// logger.log(
-// "Different grid item texts at index "
-// + index
-// + ". Old: "
-// + gridItem1.getText()
-// + ". New: "
-// + gridItem2.getText());
-// return false;
-// }
-//
-// if (!Objects.equals(gridItem1.getImage(), gridItem2.getImage())) {
-// logger.log("Different grid item images at index " + index);
-// return false;
-// }
-//
-// if (gridItem1.getImageType() != gridItem2.getImageType()) {
-// logger.log(
-// "Different grid item image types at index "
-// + index
-// + ". Old: "
-// + gridItem1.getImageType()
-// + ". New: "
-// + gridItem2.getImageType());
-// return false;
-// }
-//
-// return true;
-// }
+ private static boolean gridItemsHaveSameContent(
+ GridItem gridItem1,
+ boolean isGridItem1Selected,
+ GridItem gridItem2,
+ boolean isGridItem2Selected,
+ int index,
+ Logger logger) {
+ // Special case for grid items with toggles - if the toggle state has changed, then text
+ // and image updates are allowed.
+ if (gridItemToggleStateHasChanged(gridItem1, gridItem2)) {
+ return true;
+ }
+
+ // Special case for grid items that are selectable - if the selected state has changed,
+ // then text and image updates are allowed.
+ if (isGridItem1Selected != isGridItem2Selected) {
+ return true;
+ }
+
+ if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
+ logger.log(
+ "Different grid item titles at index "
+ + index
+ + ". Old: "
+ + gridItem1.getTitle()
+ + ". New: "
+ + gridItem2.getTitle());
+ return false;
+ }
+
+ if (!carTextsHasSameString(gridItem1.getText(), gridItem2.getText())) {
+ logger.log(
+ "Different grid item texts at index "
+ + index
+ + ". Old: "
+ + gridItem1.getText()
+ + ". New: "
+ + gridItem2.getText());
+ return false;
+ }
+
+ if (!Objects.equals(gridItem1.getImage(), gridItem2.getImage())) {
+ logger.log("Different grid item images at index " + index);
+ return false;
+ }
+
+ if (gridItem1.getImageType() != gridItem2.getImageType()) {
+ logger.log(
+ "Different grid item image types at index "
+ + index
+ + ". Old: "
+ + gridItem1.getImageType()
+ + ". New: "
+ + gridItem2.getImageType());
+ return false;
+ }
+
+ return true;
+ }
/**
* Returns {@code true} if the strings of the two {@link CarText}s are the same, {@code false}
@@ -225,32 +221,31 @@
*
* <p>Spans that are attached to the strings are ignored from the comparison.
*/
- // TODO(rampara): Uncomment on addition of model module
-// private static boolean carTextsHasSameString(
-// @Nullable CarText carText1, @Nullable CarText carText2) {
-// // If both carText1 and carText2 are null, return true. If only one of them is null,
-// return
-// // false.
-// if (carText1 == null || carText2 == null) {
-// return carText1 == null && carText2 == null;
-// }
-//
-// return Objects.equals(carText1.getText(), carText2.getText());
-// }
-//
-// private static boolean rowToggleStateHasChanged(Row row1, Row row2) {
-// Toggle toggle1 = row1.getToggle();
-// Toggle toggle2 = row2.getToggle();
-//
-// return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
-// }
-//
-// private static boolean gridItemToggleStateHasChanged(GridItem gridItem1, GridItem gridItem2) {
-// Toggle toggle1 = gridItem1.getToggle();
-// Toggle toggle2 = gridItem2.getToggle();
-//
-// return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
-// }
+ private static boolean carTextsHasSameString(
+ @Nullable CarText carText1, @Nullable CarText carText2) {
+ // If both carText1 and carText2 are null, return true. If only one of them is null,
+ // return false.
+ if (carText1 == null || carText2 == null) {
+ return carText1 == null && carText2 == null;
+ }
+
+ return Objects.equals(carText1.getText(), carText2.getText());
+ }
+
+ private static boolean rowToggleStateHasChanged(Row row1, Row row2) {
+ Toggle toggle1 = row1.getToggle();
+ Toggle toggle2 = row2.getToggle();
+
+ return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
+ }
+
+ private static boolean gridItemToggleStateHasChanged(GridItem gridItem1, GridItem gridItem2) {
+ Toggle toggle1 = gridItem1.getToggle();
+ Toggle toggle2 = gridItem2.getToggle();
+
+ return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
+ }
+
private ValidationUtils() {
}
}
diff --git a/car/app/app/src/main/res/values/attrs.xml b/car/app/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..a481b69
--- /dev/null
+++ b/car/app/app/src/main/res/values/attrs.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ 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.
+-->
+
+<resources>
+ <attr name="carColorPrimary" format="color" />
+ <attr name="carColorPrimaryDark" format="color" />
+ <attr name="carColorSecondary" format="color" />
+ <attr name="carColorSecondaryDark" format="color" />
+</resources>
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 0f3208c..e8377ac8 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -38,6 +38,7 @@
api project(":compose:ui:ui")
api "androidx.navigation:navigation-runtime-ktx:2.3.1"
+ androidTestImplementation project(":compose:material:material")
androidTestImplementation 'androidx.navigation:navigation-testing:2.3.1'
androidTestImplementation project(path: ':internal-testutils-navigation'), {
exclude group: 'androidx.navigation', module: 'navigation-common-ktx'
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
new file mode 100644
index 0000000..fd00b02
--- /dev/null
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.navigation.compose.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonConstants
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigate
+import androidx.navigation.compose.popUpTo
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun NavPopUpToDemo() {
+ val navController = rememberNavController()
+ NavHost(navController, startDestination = "1") {
+ composable("1") { NumberedScreen(navController, 1) }
+ composable("2") { NumberedScreen(navController, 2) }
+ composable("3") { NumberedScreen(navController, 3) }
+ composable("4") { NumberedScreen(navController, 4) }
+ composable("5") { NumberedScreen(navController, 5) }
+ }
+}
+
+@Composable
+fun NumberedScreen(navController: NavController, number: Int) {
+ Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
+ val next = number + 1
+ if (number < 5) {
+ Button(
+ navController.navigate("$next") },
+ colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = "Navigate to Screen $next")
+ }
+ }
+ Text("This is screen $number", Modifier.weight(1f))
+ if (navController.previousBackStackEntry != null) {
+ Button(
+ navController.navigate("1") { popUpTo("1") { inclusive = true } } },
+ colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = "PopUpTo Screen 1")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
new file mode 100644
index 0000000..05f2abc
--- /dev/null
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.navigation.compose.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.savedinstancestate.savedInstanceState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigate
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.compose.samples.NavigateButton
+
+@Composable
+fun NavSingleTopDemo() {
+ val navController = rememberNavController()
+ val query = savedInstanceState(saver = TextFieldValue.Saver) { TextFieldValue() }
+ Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
+ TextField(
+ value = query.value,
+ query.value = it },
+ placeholder = { Text("Search") }
+ )
+ NavigateButton("Search") {
+ navController.navigate("search/" + query.value.text) {
+ launchSingleTop = true
+ }
+ }
+ NavHost(navController, startDestination = "start") {
+ composable("start") { StartScreen() }
+ composable("search/{query}") { backStackEntry ->
+ SearchResultScreen(
+ backStackEntry.arguments!!.getString("query", "no query entered")
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun StartScreen() {
+ Divider(color = Color.Black)
+ Text(text = "Start a search above")
+}
+
+@Composable
+fun SearchResultScreen(query: String) {
+ Text("You searched for $query")
+}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
index e1d2725..cca6694 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
@@ -26,6 +26,8 @@
ComposableDemo("Nested Nav Demo") { NestNavDemo() },
ComposableDemo("Bottom Bar Nav Demo") { BottomBarNavDemo() },
ComposableDemo("Navigation with Args") { NavWithArgsDemo() },
- ComposableDemo("Navigation by DeepLink") { NavByDeepLinkDemo() }
+ ComposableDemo("Navigation by DeepLink") { NavByDeepLinkDemo() },
+ ComposableDemo("Navigation PopUpTo") { NavPopUpToDemo() },
+ ComposableDemo("Navigation SingleTop") { NavSingleTopDemo() }
)
)
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index 9a71f30..a1b852e 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -17,12 +17,21 @@
package androidx.navigation.compose
import android.net.Uri
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Button
+import androidx.compose.material.Text
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
import androidx.core.net.toUri
import androidx.navigation.NavGraph
import androidx.navigation.NavHostController
@@ -30,6 +39,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
+import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Rule
import org.junit.Test
@@ -84,6 +94,58 @@
}
@Test
+ fun testNavigateOutsideStateChange() {
+ lateinit var navController: NavHostController
+ val text = "myButton"
+ var counter = 0
+ composeTestRule.setContent {
+ navController = rememberNavController()
+ var state by remember { mutableStateOf(0) }
+ Column(Modifier.fillMaxSize()) {
+ NavHost(navController, startDestination = "first") {
+ composable("first") { }
+ composable("second") { }
+ }
+ Button(
+ >
+ state++
+ counter = state
+ }
+ ) {
+ Text(text)
+ }
+ }
+ }
+
+ assertWithMessage("Destination should be added to the graph")
+ .that("first" in navController.graph)
+ .isTrue()
+
+ composeTestRule.runOnIdle {
+ navController.navigate("second")
+ }
+
+ composeTestRule.runOnIdle {
+ assertWithMessage("second destination should be current")
+ .that(
+ navController.currentDestination?.hasDeepLink(Uri.parse(createRoute("second")))
+ ).isTrue()
+ }
+
+ composeTestRule.onNodeWithText(text)
+ .performClick()
+
+ composeTestRule.runOnIdle {
+ // ensure our click listener was fired
+ assertThat(counter).isEqualTo(1)
+ assertWithMessage("second destination should be current")
+ .that(
+ navController.currentDestination?.hasDeepLink(Uri.parse(createRoute("second")))
+ ).isTrue()
+ }
+ }
+
+ @Test
fun testPop() {
lateinit var navController: TestNavHostController
composeTestRule.setContent {
@@ -113,8 +175,8 @@
lateinit var state: MutableState<String>
composeTestRule.setContent {
state = remember { mutableStateOf("first") }
-
- navController = TestNavHostController(ContextAmbient.current)
+ val context = ContextAmbient.current
+ navController = remember { TestNavHostController(context) }
NavHost(navController, startDestination = state.value) {
test("first")
@@ -127,9 +189,9 @@
}
composeTestRule.runOnIdle {
- assertWithMessage("Second destination should be current")
+ assertWithMessage("First destination should be current")
.that(
- navController.currentDestination?.hasDeepLink(createRoute("second").toUri())
+ navController.currentDestination?.hasDeepLink(createRoute("first").toUri())
).isTrue()
}
}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index e86802b..ed00cd4 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -42,6 +42,9 @@
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
* the provided [navController].
*
+ * The builder passed into this method is [remember]ed. This means that for this NavHost, the
+ * contents of the builder cannot be changed.
+ *
* @sample androidx.navigation.compose.samples.BasicNav
*
* @param navController the navController for this host
@@ -70,6 +73,9 @@
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
* the provided [navController].
*
+ * The graph passed into this method is [remember]ed. This means that for this NavHost, the graph
+ * cannot be changed.
+ *
* @param navController the navController for this host
* @param graph the graph for this host
*/
@@ -79,6 +85,7 @@
var context = ContextAmbient.current
val lifecycleOwner = LifecycleOwnerAmbient.current
val viewModelStore = ViewModelStoreOwnerAmbient.current.viewModelStore
+ val rememberedGraph = remember { graph }
// on successful recompose we setup the navController with proper inputs
// after the first time, this will only happen again if one of the inputs changes
@@ -98,8 +105,8 @@
}
}
- onCommit(graph) {
- navController.graph = graph
+ onCommit(rememberedGraph) {
+ navController.graph = rememberedGraph
}
val restorableStateHolder = rememberRestorableStateHolder<UUID>()
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
index eebd3a7..9e5dc62 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
@@ -473,6 +473,120 @@
}
/**
+ * Test that popping the last destination in a graph while navigating to a new
+ * destination in that graph keeps the graph around
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleReplaceLastDestination() {
+ val navController = createNavController()
+ val navGraph = navController.navigatorProvider.navigation(
+ id = 1,
+ startDestination = R.id.nested
+ ) {
+ navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+ test(R.id.nested_test)
+ }
+ test(R.id.second_test)
+ }
+ navController.graph = navGraph
+
+ val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+ assertWithMessage("The nested graph should be resumed when its child is resumed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The nested start destination should be resumed")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate(
+ R.id.nested_test,
+ null,
+ navOptions {
+ popUpTo(R.id.nested_test) {
+ inclusive = true
+ }
+ }
+ )
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The nested graph should be resumed when its child is resumed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The nested start destination should be destroyed after being popped")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The new destination should be resumed")
+ .that(secondBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ /**
+ * Test that popping the last destination in a graph while navigating correctly
+ * cleans up the previous navigation graph
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleOrphanedGraph() {
+ val navController = createNavController()
+ val navGraph = navController.navigatorProvider.navigation(
+ id = 1,
+ startDestination = R.id.nested
+ ) {
+ navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+ test(R.id.nested_test)
+ }
+ test(R.id.second_test)
+ }
+ navController.graph = navGraph
+
+ val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+ assertWithMessage("The nested graph should be resumed when its child is resumed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The nested start destination should be resumed")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate(
+ R.id.second_test,
+ null,
+ navOptions {
+ popUpTo(R.id.nested_test) {
+ inclusive = true
+ }
+ }
+ )
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The nested graph should be destroyed when its children are destroyed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ assertWithMessage("The nested start destination should be destroyed after being popped")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
+ assertWithMessage("The new destination should be resumed")
+ .that(secondBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ /**
* Test that navigating to a new instance of a graph leaves the previous instance in its
* current state.
*/
@@ -546,6 +660,132 @@
}
/**
+ * Test that navigating to a new instance of a graph back to back with its previous
+ * instance creates a brand new graph instance
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleNestedRepeatedBackToBack() {
+ val navController = createNavController()
+ val navGraph = navController.navigatorProvider.navigation(
+ id = 1,
+ startDestination = R.id.nested
+ ) {
+ navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+ test(R.id.nested_test)
+ }
+ test(R.id.second_test)
+ }
+ navController.graph = navGraph
+
+ val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+ assertWithMessage("The nested graph should be resumed when its child is resumed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The nested start destination should be resumed")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ // Navigate to a new instance of the graph, creating another copy
+ navController.navigate(navGraph.id)
+
+ assertWithMessage("The original parent graph should move to created")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertWithMessage("The original nested graph should move to created")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertWithMessage("The original nested start destination should move to created")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ val newGraphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+ assertWithMessage("The new parent graph should be resumed when its child is resumed")
+ .that(newGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val newNestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+ assertWithMessage("The new nested graph should be resumed when its child is resumed")
+ .that(newNestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val newNestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The new nested start destination should be resumed")
+ .that(newNestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ /**
+ * Test that navigating to a new instance of a graph back to back with popping the
+ * last destination from the previous instance of the graph correctly cleans up
+ * the orphaned graph and creates a new graph instance.
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleNestedRepeatedBackToBackWithOrphanedGraph() {
+ val navController = createNavController()
+ val navGraph = navController.navigatorProvider.navigation(
+ id = 1,
+ startDestination = R.id.nested
+ ) {
+ navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+ test(R.id.nested_test)
+ }
+ test(R.id.second_test)
+ }
+ navController.graph = navGraph
+
+ val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+ assertWithMessage("The nested graph should be resumed when its child is resumed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The nested start destination should be resumed")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ // Navigate to a new instance of the graph, creating another copy
+ // while popping the last destination from the previous graph
+ navController.navigate(
+ navGraph.id,
+ null,
+ navOptions {
+ popUpTo(R.id.nested_test) {
+ inclusive = true
+ }
+ }
+ )
+
+ assertWithMessage("The parent graph should be destroyed when its children are destroyed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ assertWithMessage("The nested graph should be destroyed when its children are destroyed")
+ .that(nestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ assertWithMessage("The nested start destination should be destroyed after being popped")
+ .that(nestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ val newGraphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+ assertWithMessage("The new parent graph should be resumed when its child is resumed")
+ .that(newGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val newNestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+ assertWithMessage("The new nested graph should be resumed when its child is resumed")
+ .that(newNestedGraphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val newNestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+ assertWithMessage("The new nested start destination should be resumed")
+ .that(newNestedBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ /**
* Test that navigating to a new instance of a graph via a deep link to a FloatingWindow
* destination leaves the previous instance in its current state.
*/
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
index 9031ac4..d3a3b7d 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
@@ -1085,16 +1085,33 @@
// Keep popping
}
}
- // The mGraph should always be on the back stack after you navigate()
- if (mBackStack.isEmpty()) {
- NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
- mLifecycleOwner, mViewModel);
- mBackStack.add(entry);
- }
- // Now ensure all intermediate NavGraphs are put on the back stack
- // to ensure that global actions work.
+
+ // When you navigate() to a NavGraph, we need to ensure that a new instance
+ // is always created vs reusing an existing copy of that destination
ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
NavDestination destination = newDest;
+ if (node instanceof NavGraph) {
+ do {
+ NavGraph parent = destination.getParent();
+ if (parent != null) {
+ NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
+ finalArgs, mLifecycleOwner, mViewModel);
+ hierarchy.addFirst(entry);
+ // Pop any orphaned copy of that navigation graph off the back stack
+ if (!mBackStack.isEmpty()
+ && mBackStack.getLast().getDestination() == parent) {
+ popBackStackInternal(parent.getId(), true);
+ }
+ }
+ destination = parent;
+ } while (destination != null && destination != node);
+ }
+
+ // Now collect the set of all intermediate NavGraphs that need to be put onto
+ // the back stack
+ destination = hierarchy.isEmpty()
+ ? newDest
+ : hierarchy.getFirst().getDestination();
while (destination != null && findDestination(destination.getId()) == null) {
NavGraph parent = destination.getParent();
if (parent != null) {
@@ -1104,7 +1121,25 @@
}
destination = parent;
}
+ NavDestination overlappingDestination = hierarchy.isEmpty()
+ ? newDest
+ : hierarchy.getLast().getDestination();
+ // Pop any orphaned navigation graphs that don't connect to the new destinations
+ //noinspection StatementWithEmptyBody
+ while (!mBackStack.isEmpty()
+ && mBackStack.getLast().getDestination() instanceof NavGraph
+ && ((NavGraph) mBackStack.getLast().getDestination()).findNode(
+ overlappingDestination.getId(), false) == null
+ && popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
+ // Keep popping
+ }
mBackStack.addAll(hierarchy);
+ // The mGraph should always be on the back stack after you navigate()
+ if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
+ NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
+ mLifecycleOwner, mViewModel);
+ mBackStack.addFirst(entry);
+ }
// And finally, add the new destination with its default args
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
diff --git a/ui/ui-tooling/api/current.txt b/ui/ui-tooling/api/current.txt
index 3a99d46..e04894b 100644
--- a/ui/ui-tooling/api/current.txt
+++ b/ui/ui-tooling/api/current.txt
@@ -113,7 +113,7 @@
method public String getName();
method public int getOffset();
method public int getPackageHash();
- method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> getParameters();
+ method public java.util.List<androidx.ui.tooling.inspector.RawParameter> getParameters();
method public int getTop();
method public int getWidth();
property public final java.util.List<androidx.ui.tooling.inspector.InspectorNode> children;
@@ -126,7 +126,7 @@
property public final String name;
property public final int offset;
property public final int packageHash;
- property public final java.util.List<androidx.ui.tooling.inspector.NodeParameter> parameters;
+ property public final java.util.List<androidx.ui.tooling.inspector.RawParameter> parameters;
property public final int top;
property public final int width;
}
@@ -134,6 +134,8 @@
public final class LayoutInspectorTree {
ctor public LayoutInspectorTree();
method public java.util.List<androidx.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
+ method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> convertParameters(androidx.ui.tooling.inspector.InspectorNode node);
+ method public void resetGeneratedId();
}
public final class LayoutInspectorTreeKt {
@@ -167,6 +169,14 @@
enum_constant public static final androidx.ui.tooling.inspector.ParameterType String;
}
+ public final class RawParameter {
+ ctor public RawParameter(String name, Object? value);
+ method public String getName();
+ method public Object? getValue();
+ property public final String name;
+ property public final Object? value;
+ }
+
}
package androidx.ui.tooling.preview {
diff --git a/ui/ui-tooling/api/public_plus_experimental_current.txt b/ui/ui-tooling/api/public_plus_experimental_current.txt
index 3a99d46..e04894b 100644
--- a/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -113,7 +113,7 @@
method public String getName();
method public int getOffset();
method public int getPackageHash();
- method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> getParameters();
+ method public java.util.List<androidx.ui.tooling.inspector.RawParameter> getParameters();
method public int getTop();
method public int getWidth();
property public final java.util.List<androidx.ui.tooling.inspector.InspectorNode> children;
@@ -126,7 +126,7 @@
property public final String name;
property public final int offset;
property public final int packageHash;
- property public final java.util.List<androidx.ui.tooling.inspector.NodeParameter> parameters;
+ property public final java.util.List<androidx.ui.tooling.inspector.RawParameter> parameters;
property public final int top;
property public final int width;
}
@@ -134,6 +134,8 @@
public final class LayoutInspectorTree {
ctor public LayoutInspectorTree();
method public java.util.List<androidx.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
+ method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> convertParameters(androidx.ui.tooling.inspector.InspectorNode node);
+ method public void resetGeneratedId();
}
public final class LayoutInspectorTreeKt {
@@ -167,6 +169,14 @@
enum_constant public static final androidx.ui.tooling.inspector.ParameterType String;
}
+ public final class RawParameter {
+ ctor public RawParameter(String name, Object? value);
+ method public String getName();
+ method public Object? getValue();
+ property public final String name;
+ property public final Object? value;
+ }
+
}
package androidx.ui.tooling.preview {
diff --git a/ui/ui-tooling/api/restricted_current.txt b/ui/ui-tooling/api/restricted_current.txt
index 3a99d46..e04894b 100644
--- a/ui/ui-tooling/api/restricted_current.txt
+++ b/ui/ui-tooling/api/restricted_current.txt
@@ -113,7 +113,7 @@
method public String getName();
method public int getOffset();
method public int getPackageHash();
- method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> getParameters();
+ method public java.util.List<androidx.ui.tooling.inspector.RawParameter> getParameters();
method public int getTop();
method public int getWidth();
property public final java.util.List<androidx.ui.tooling.inspector.InspectorNode> children;
@@ -126,7 +126,7 @@
property public final String name;
property public final int offset;
property public final int packageHash;
- property public final java.util.List<androidx.ui.tooling.inspector.NodeParameter> parameters;
+ property public final java.util.List<androidx.ui.tooling.inspector.RawParameter> parameters;
property public final int top;
property public final int width;
}
@@ -134,6 +134,8 @@
public final class LayoutInspectorTree {
ctor public LayoutInspectorTree();
method public java.util.List<androidx.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
+ method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> convertParameters(androidx.ui.tooling.inspector.InspectorNode node);
+ method public void resetGeneratedId();
}
public final class LayoutInspectorTreeKt {
@@ -167,6 +169,14 @@
enum_constant public static final androidx.ui.tooling.inspector.ParameterType String;
}
+ public final class RawParameter {
+ ctor public RawParameter(String name, Object? value);
+ method public String getName();
+ method public Object? getValue();
+ property public final String name;
+ property public final Object? value;
+ }
+
}
package androidx.ui.tooling.preview {
diff --git a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 48ad3aa..4fbfa86 100644
--- a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -93,9 +93,9 @@
dumpSlotTableSet(slotTableRecord)
val builder = LayoutInspectorTree()
val nodes = builder.convert(view)
- dumpNodes(nodes)
+ dumpNodes(nodes, builder)
- validate(nodes, checkParameters = true) {
+ validate(nodes, builder, checkParameters = true) {
node(
name = "Box",
fileName = "Box.kt",
@@ -404,10 +404,10 @@
dumpSlotTableSet(slotTableRecord)
val builder = LayoutInspectorTree()
val nodes = builder.convert(view)
- dumpNodes(nodes)
+ dumpNodes(nodes, builder)
if (DEBUG) {
- validate(nodes, checkParameters = false) {
+ validate(nodes, builder, checkParameters = false) {
node("Box", children = listOf("ModalDrawerLayout"))
node("ModalDrawerLayout", children = listOf("WithConstraints"))
node("WithConstraints", children = listOf("SubcomposeLayout"))
@@ -484,18 +484,20 @@
private fun validate(
result: List<InspectorNode>,
+ builder: LayoutInspectorTree,
checkParameters: Boolean,
block: TreeValidationReceiver.() -> Unit = {}
) {
val nodes = result.flatMap { flatten(it) }.iterator()
- val tree = TreeValidationReceiver(nodes, density, checkParameters)
+ val tree = TreeValidationReceiver(nodes, density, checkParameters, builder)
tree.block()
}
private class TreeValidationReceiver(
val nodeIterator: Iterator<InspectorNode>,
val density: Density,
- val checkParameters: Boolean
+ val checkParameters: Boolean,
+ val builder: LayoutInspectorTree
) {
fun node(
name: String,
@@ -534,11 +536,12 @@
}
if (checkParameters) {
- val params = ParameterValidationReceiver(node.parameters.listIterator())
- params.block()
- if (params.parameterIterator.hasNext()) {
+ val params = builder.convertParameters(node)
+ val receiver = ParameterValidationReceiver(params.listIterator())
+ receiver.block()
+ if (receiver.parameterIterator.hasNext()) {
val elementNames = mutableListOf<String>()
- params.parameterIterator.forEachRemaining { elementNames.add(it.name) }
+ receiver.parameterIterator.forEachRemaining { elementNames.add(it.name) }
error("$name: has more parameters like: ${elementNames.joinToString()}")
}
}
@@ -549,7 +552,7 @@
listOf(node).plus(node.children.flatMap { flatten(it) })
// region DEBUG print methods
- private fun dumpNodes(nodes: List<InspectorNode>) {
+ private fun dumpNodes(nodes: List<InspectorNode>, builder: LayoutInspectorTree) {
@Suppress("ConstantConditionIf")
if (!DEBUG) {
return
@@ -559,7 +562,7 @@
nodes.forEach { dumpNode(it, indent = 0) }
println()
println("=================== validate statements ==========================")
- nodes.forEach { generateValidate(it) }
+ nodes.forEach { generateValidate(it, builder) }
}
private fun dumpNode(node: InspectorNode, indent: Int) {
@@ -571,7 +574,7 @@
node.children.forEach { dumpNode(it, indent + 1) }
}
- private fun generateValidate(node: InspectorNode) {
+ private fun generateValidate(node: InspectorNode, builder: LayoutInspectorTree) {
with(density) {
val left = round(node.left.toDp())
val top = round(node.top.toDp())
@@ -599,10 +602,10 @@
println()
print(")")
if (node.parameters.isNotEmpty()) {
- generateParameters(node.parameters, 0)
+ generateParameters(builder.convertParameters(node), 0)
}
println()
- node.children.forEach { generateValidate(it) }
+ node.children.forEach { generateValidate(it, builder) }
}
private fun generateParameters(parameters: List<NodeParameter>, indent: Int) {
diff --git a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt
index 02ce967..200a1ad 100644
--- a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt
+++ b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt
@@ -75,7 +75,6 @@
import com.google.common.truth.Truth.assertWithMessage
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -83,15 +82,16 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
class ParameterFactoryTest {
- private val node = MutableInspectorNode()
private val factory = ParameterFactory(InlineClassConverter())
private val api = android.os.Build.VERSION.SDK_INT
+ private val node = MutableInspectorNode().apply {
+ width = 1000
+ height = 500
+ }.build()
@Before
fun before() {
factory.density = Density(2.0f)
- node.width = 1000
- node.height = 500
isDebugInspectorInfoEnabled = true
}
@@ -101,7 +101,6 @@
}
@Test
- @Ignore("b/172466485")
fun testAbsoluteAlignment() {
assertThat(lookup(AbsoluteAlignment.TopLeft))
.isEqualTo(ParameterType.String to "TopLeft")
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt
index f933d53..d495e31 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt
@@ -89,7 +89,7 @@
/**
* The parameters of this Composable.
*/
- val parameters: List<NodeParameter>,
+ val parameters: List<RawParameter>,
/**
* The children nodes of this Composable.
@@ -98,6 +98,11 @@
)
/**
+ * Parameter definition with a raw value reference.
+ */
+class RawParameter(val name: String, val value: Any?)
+
+/**
* Mutable version of [InspectorNode].
*/
@ExperimentalLayoutNodeApi
@@ -114,7 +119,7 @@
var top = 0
var width = 0
var height = 0
- val parameters = mutableListOf<NodeParameter>()
+ val parameters = mutableListOf<RawParameter>()
val children = mutableListOf<InspectorNode>()
fun reset() {
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt
index 792b150..a6dc0eb 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt
@@ -89,10 +89,23 @@
return result
}
+ /**
+ * Converts the [RawParameter]s of the [node] into displayable parameters.
+ */
+ fun convertParameters(node: InspectorNode): List<NodeParameter> {
+ return node.parameters.mapNotNull { parameterFactory.create(node, it.name, it.value) }
+ }
+
+ /**
+ * Reset the generated id. Nodes are assigned an id if there isn't a layout node id present.
+ */
+ fun resetGeneratedId() {
+ generatedId = -1L
+ }
+
private fun clear() {
cache.clear()
inlineClassConverter.clear()
- generatedId = -1L
claimedNodes.clear()
treeMap.clear()
ownerMap.clear()
@@ -298,8 +311,8 @@
parameters.forEach { addParameter(it, node) }
private fun addParameter(parameter: ParameterInformation, node: MutableInspectorNode) {
- val castedValue = castValue(parameter) ?: return
- parameterFactory.create(node, parameter.name, castedValue)?.let { node.parameters.add(it) }
+ val castedValue = castValue(parameter)
+ node.parameters.add(RawParameter(parameter.name, castedValue))
}
private fun castValue(parameter: ParameterInformation): Any? {
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt
index 2d0a0347..cc12a73 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt
@@ -17,7 +17,9 @@
package androidx.ui.tooling.inspector
import android.util.Log
+import android.view.View
import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
@@ -97,6 +99,10 @@
valueLookup[Color.Unspecified] = "Unspecified"
valuesLoaded.add(Enum::class.java)
valuesLoaded.add(Any::class.java)
+
+ // AbsoluteAlignment is not found from an instance of BiasAbsoluteAlignment,
+ // because Alignment has no file level class.
+ loadConstantsFromEnclosedClasses(AbsoluteAlignment::class.java)
}
/**
@@ -105,7 +111,7 @@
* Attempt to convert the value to a user readable value.
* For now: return null when a conversion is not possible/found.
*/
- fun create(node: MutableInspectorNode, name: String, value: Any?): NodeParameter? {
+ fun create(node: InspectorNode, name: String, value: Any?): NodeParameter? {
val creator = creatorCache ?: ParameterCreator()
try {
return creator.create(node, name, value)
@@ -233,10 +239,10 @@
* Convenience class for building [NodeParameter]s.
*/
private inner class ParameterCreator {
- private var node: MutableInspectorNode? = null
+ private var node: InspectorNode? = null
private var recursions = 0
- fun create(node: MutableInspectorNode, name: String, value: Any?): NodeParameter? = try {
+ fun create(node: InspectorNode, name: String, value: Any?): NodeParameter? = try {
this.node = node
recursions = 0
create(name, value)
@@ -277,6 +283,7 @@
is String -> NodeParameter(name, ParameterType.String, value)
is TextUnit -> createFromTextUnit(name, value)
is VectorAsset -> createFromVectorAssert(name, value)
+ is View -> NodeParameter(name, ParameterType.String, value.javaClass.simpleName)
else -> createFromKotlinReflection(name, value)
}
} finally {