Add UX restrictions configuration.
New class will contain the configuration for UX restrictions. It also:
- de/serializes as Json for UXR service internal storage.
- implements parcelable to be passed through binder.
- validates configuration before building.
Functional difference from current implementation:
- allow setting UXR for unknown driving state (previously always fully restricted).
- when not specified, speed range is null (previously INVALID_SPEED)
Test: atest AndroidCarApiTest:android.car.apitest.CarUxRestrictionsConfigurationTest
Bug: 120029082
Change-Id: I6d477f099b85737e162ec9d3ffc2365ffb673254
(cherry picked from commit 90dceff8cd577c9a892a10e2d6a2af0180b05b9b)
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
new file mode 100644
index 0000000..659f1cd
--- /dev/null
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
@@ -0,0 +1,730 @@
+/*
+ * Copyright (C) 2018 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 android.car.drivingstate;
+
+import android.annotation.FloatRange;
+import android.annotation.Nullable;
+import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Configuration for Car UX Restrictions service.
+ *
+ * @hide
+ */
+public final class CarUxRestrictionsConfiguration implements Parcelable {
+ private static final String TAG = "CarUxRConfig";
+
+ // Constants used by json de/serialization.
+ private static final String JSON_NAME_MAX_CONTENT_DEPTH = "max_content_depth";
+ private static final String JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS =
+ "max_cumulative_content_items";
+ private static final String JSON_NAME_MAX_STRING_LENGTH = "max_string_length";
+ private static final String JSON_NAME_MOVING_RESTRICTIONS = "moving_restrictions";
+ private static final String JSON_NAME_IDLING_RESTRICTIONS = "idling_restrictions";
+ private static final String JSON_NAME_PARKED_RESTRICTIONS = "parked_restrictions";
+ private static final String JSON_NAME_UNKNOWN_RESTRICTIONS = "unknown_restrictions";
+ private static final String JSON_NAME_REQ_OPT = "req_opt";
+ private static final String JSON_NAME_RESTRICTIONS = "restrictions";
+ private static final String JSON_NAME_SPEED_RANGE = "speed_range";
+ private static final String JSON_NAME_MIN_SPEED = "min_speed";
+ private static final String JSON_NAME_MAX_SPEED = "max_speed";
+
+ private final int mMaxContentDepth;
+ private final int mMaxCumulativeContentItems;
+ private final int mMaxStringLength;
+ private final Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions = new HashMap<>();
+
+ private CarUxRestrictionsConfiguration(CarUxRestrictionsConfiguration.Builder builder) {
+ mMaxContentDepth = builder.mMaxContentDepth;
+ mMaxCumulativeContentItems = builder.mMaxCumulativeContentItems;
+ mMaxStringLength = builder.mMaxStringLength;
+
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> list = new ArrayList<>();
+ for (RestrictionsPerSpeedRange r : builder.mUxRestrictions.get(drivingState)) {
+ list.add(r);
+ }
+ mUxRestrictions.put(drivingState, list);
+ }
+ }
+
+ /**
+ * Returns the restrictions based on current driving state and speed.
+ */
+ public CarUxRestrictions getUxRestrictions(@CarDrivingState int drivingState,
+ float currentSpeed) {
+ List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+ if (restrictions.isEmpty()) {
+ if (Build.IS_ENG || Build.IS_USERDEBUG) {
+ throw new IllegalStateException("No restrictions for driving state "
+ + getDrivingStateName(drivingState));
+ }
+ return createDefaultUxRestrictionsEvent();
+ }
+
+ RestrictionsPerSpeedRange restriction = null;
+ if (restrictions.size() == 1) {
+ restriction = restrictions.get(0);
+ } else {
+ for (RestrictionsPerSpeedRange r : restrictions) {
+ if (r.mSpeedRange != null && r.mSpeedRange.includes(currentSpeed)) {
+ restriction = r;
+ break;
+ }
+ }
+ }
+
+ if (restriction == null) {
+ if (Build.IS_ENG || Build.IS_USERDEBUG) {
+ throw new IllegalStateException(
+ "No restrictions found for driving state " + drivingState
+ + " at speed " + currentSpeed);
+ }
+ return createDefaultUxRestrictionsEvent();
+ }
+ return createUxRestrictionsEvent(restriction.mReqOpt, restriction.mRestrictions);
+ }
+
+ private CarUxRestrictions createDefaultUxRestrictionsEvent() {
+ return createUxRestrictionsEvent(true,
+ CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
+ }
+
+ /**
+ * Creates CarUxRestrictions with restrictions parameters from current configuration.
+ */
+ private CarUxRestrictions createUxRestrictionsEvent(boolean requiresOpt,
+ @CarUxRestrictions.CarUxRestrictionsInfo int uxr) {
+ // In case the UXR is not baseline, set requiresDistractionOptimization to true since it
+ // doesn't make sense to have an active non baseline restrictions without
+ // requiresDistractionOptimization set to true.
+ if (uxr != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) {
+ requiresOpt = true;
+ }
+ CarUxRestrictions.Builder builder = new CarUxRestrictions.Builder(requiresOpt, uxr,
+ SystemClock.elapsedRealtimeNanos());
+ if (mMaxStringLength != Builder.UX_RESTRICTIONS_UNKNOWN) {
+ builder.setMaxStringLength(mMaxStringLength);
+ }
+ if (mMaxCumulativeContentItems != Builder.UX_RESTRICTIONS_UNKNOWN) {
+ builder.setMaxCumulativeContentItems(mMaxCumulativeContentItems);
+ }
+ if (mMaxContentDepth != Builder.UX_RESTRICTIONS_UNKNOWN) {
+ builder.setMaxContentDepth(mMaxContentDepth);
+ }
+ return builder.build();
+ }
+
+ // Json de/serialization methods.
+
+ /**
+ * Writes current configuration as Json.
+ */
+ public void writeJson(JsonWriter writer) throws IOException {
+ // We need to be lenient to accept infinity number (as max speed).
+ writer.setLenient(true);
+
+ writer.beginObject();
+
+ writer.name(JSON_NAME_MAX_CONTENT_DEPTH).value(mMaxContentDepth);
+ writer.name(JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS).value(
+ mMaxCumulativeContentItems);
+ writer.name(JSON_NAME_MAX_STRING_LENGTH).value(mMaxStringLength);
+
+ writer.name(JSON_NAME_PARKED_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_PARKED));
+
+ writer.name(JSON_NAME_IDLING_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_IDLING));
+
+ writer.name(JSON_NAME_MOVING_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_MOVING));
+
+ writer.name(JSON_NAME_UNKNOWN_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN));
+
+ writer.endObject();
+ }
+
+ private void writeRestrictionsList(JsonWriter writer, List<RestrictionsPerSpeedRange> messages)
+ throws IOException {
+ writer.beginArray();
+ for (RestrictionsPerSpeedRange restrictions : messages) {
+ writeRestrictions(writer, restrictions);
+ }
+ writer.endArray();
+ }
+
+ private void writeRestrictions(JsonWriter writer, RestrictionsPerSpeedRange restrictions)
+ throws IOException {
+ writer.beginObject();
+ writer.name(JSON_NAME_REQ_OPT).value(restrictions.mReqOpt);
+ writer.name(JSON_NAME_RESTRICTIONS).value(restrictions.mRestrictions);
+ if (restrictions.mSpeedRange != null) {
+ writer.name(JSON_NAME_SPEED_RANGE);
+ writer.beginObject();
+ writer.name(JSON_NAME_MIN_SPEED).value(restrictions.mSpeedRange.mMinSpeed);
+ writer.name(JSON_NAME_MAX_SPEED).value(restrictions.mSpeedRange.mMaxSpeed);
+ writer.endObject();
+ }
+ writer.endObject();
+ }
+
+ /**
+ * Reads Json as UX restriction configuration.
+ */
+ public static CarUxRestrictionsConfiguration readJson(JsonReader reader) throws IOException {
+ // We need to be lenient to accept infinity number (as max speed).
+ reader.setLenient(true);
+
+ Builder builder = new Builder();
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals(JSON_NAME_MAX_CONTENT_DEPTH)) {
+ builder.setMaxContentDepth(reader.nextInt());
+ } else if (name.equals(JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS)) {
+ builder.setMaxCumulativeContentItems(reader.nextInt());
+ } else if (name.equals(JSON_NAME_MAX_STRING_LENGTH)) {
+ builder.setMaxStringLength(reader.nextInt());
+ } else if (name.equals(JSON_NAME_PARKED_RESTRICTIONS)) {
+ readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_PARKED, builder);
+ } else if (name.equals(JSON_NAME_IDLING_RESTRICTIONS)) {
+ readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_IDLING, builder);
+ } else if (name.equals(JSON_NAME_MOVING_RESTRICTIONS)) {
+ readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_MOVING, builder);
+ } else if (name.equals(JSON_NAME_UNKNOWN_RESTRICTIONS)) {
+ readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_UNKNOWN, builder);
+ } else {
+ Log.e(TAG, "Unknown name parsing json config: " + name);
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ return builder.build();
+ }
+
+ private static void readRestrictionsList(JsonReader reader, @CarDrivingState int drivingState,
+ Builder builder) throws IOException {
+ reader.beginArray();
+ while (reader.hasNext()) {
+ readRestrictions(reader, drivingState, builder);
+ }
+ reader.endArray();
+ }
+
+ private static void readRestrictions(JsonReader reader, @CarDrivingState int drivingState,
+ Builder builder) throws IOException {
+ reader.beginObject();
+ boolean reqOpt = false;
+ int restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+ Builder.SpeedRange speedRange = null;
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals(JSON_NAME_REQ_OPT)) {
+ reqOpt = reader.nextBoolean();
+ } else if (name.equals(JSON_NAME_RESTRICTIONS)) {
+ restrictions = reader.nextInt();
+ } else if (name.equals(JSON_NAME_SPEED_RANGE)) {
+ reader.beginObject();
+ // Okay to set min initial value as MAX_SPEED because SpeedRange() won't allow it.
+ float minSpeed = Builder.SpeedRange.MAX_SPEED;
+ float maxSpeed = Builder.SpeedRange.MAX_SPEED;
+
+ while (reader.hasNext()) {
+ String n = reader.nextName();
+ if (n.equals(JSON_NAME_MIN_SPEED)) {
+ minSpeed = Double.valueOf(reader.nextDouble()).floatValue();
+ } else if (n.equals(JSON_NAME_MAX_SPEED)) {
+ maxSpeed = Double.valueOf(reader.nextDouble()).floatValue();
+ } else {
+ Log.e(TAG, "Unknown name parsing json config: " + n);
+ reader.skipValue();
+ }
+ }
+ speedRange = new Builder.SpeedRange(minSpeed, maxSpeed);
+ reader.endObject();
+ }
+ }
+ reader.endObject();
+ builder.setUxRestrictions(drivingState, speedRange, reqOpt, restrictions);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof CarUxRestrictionsConfiguration)) {
+ return false;
+ }
+
+ CarUxRestrictionsConfiguration other = (CarUxRestrictionsConfiguration) obj;
+
+ // Compare UXR parameters.
+ if (mMaxContentDepth != other.mMaxContentDepth
+ || mMaxCumulativeContentItems != other.mMaxCumulativeContentItems
+ || mMaxStringLength != other.mMaxStringLength) {
+ return false;
+ }
+
+ // Compare UXR by driving state.
+ if (!mUxRestrictions.keySet().equals(other.mUxRestrictions.keySet())) {
+ return false;
+ }
+ for (int drivingState : mUxRestrictions.keySet()) {
+ List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(
+ drivingState);
+ List<RestrictionsPerSpeedRange> otherRestrictions = other.mUxRestrictions.get(
+ drivingState);
+ if (restrictions.size() != otherRestrictions.size()) {
+ return false;
+ }
+ // Assuming the restrictions are sorted.
+ for (int i = 0; i < restrictions.size(); i++) {
+ if (!restrictions.get(i).equals(otherRestrictions.get(i))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Dump the driving state to UX restrictions mapping.
+ */
+ public void dump(PrintWriter writer) {
+ for (Integer state : mUxRestrictions.keySet()) {
+ List<RestrictionsPerSpeedRange> list = mUxRestrictions.get(state);
+ writer.println("===========================================");
+ writer.println("Driving State to UXR");
+ if (list != null) {
+ writer.println("State:" + getDrivingStateName(state) + " num restrictions:"
+ + list.size());
+ for (RestrictionsPerSpeedRange r : list) {
+ writer.println("Requires DO? " + r.mReqOpt
+ + "\nRestrictions: 0x" + Integer.toHexString(r.mRestrictions)
+ + "\nSpeed Range: " + r.mSpeedRange == null
+ ? "None"
+ : r.mSpeedRange.mMinSpeed + " - " + r.mSpeedRange.mMaxSpeed);
+ writer.println("===========================================");
+ }
+ }
+ }
+ writer.println("Max String length: " + mMaxStringLength);
+ writer.println("Max Cumulative Content Items: " + mMaxCumulativeContentItems);
+ writer.println("Max Content depth: " + mMaxContentDepth);
+ }
+
+ private static String getDrivingStateName(@CarDrivingState int state) {
+ switch (state) {
+ case 0:
+ return "parked";
+ case 1:
+ return "idling";
+ case 2:
+ return "moving";
+ default:
+ return "unknown";
+ }
+ }
+
+ // Parcelable methods/fields.
+
+ // Used by Parcel methods to ensure de/serialization order.
+ private static final int[] DRIVING_STATES = new int[]{
+ CarDrivingStateEvent.DRIVING_STATE_UNKNOWN,
+ CarDrivingStateEvent.DRIVING_STATE_PARKED,
+ CarDrivingStateEvent.DRIVING_STATE_IDLING,
+ CarDrivingStateEvent.DRIVING_STATE_MOVING
+ };
+
+ public static final Parcelable.Creator<CarUxRestrictionsConfiguration> CREATOR =
+ new Parcelable.Creator<CarUxRestrictionsConfiguration>() {
+
+ @Override
+ public CarUxRestrictionsConfiguration createFromParcel(Parcel source) {
+ return new CarUxRestrictionsConfiguration(source);
+ }
+
+ @Override
+ public CarUxRestrictionsConfiguration[] newArray(int size) {
+ return new CarUxRestrictionsConfiguration[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private CarUxRestrictionsConfiguration(Parcel in) {
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> restrictions = new ArrayList<>();
+ in.readTypedList(restrictions, RestrictionsPerSpeedRange.CREATOR);
+ mUxRestrictions.put(drivingState, restrictions);
+ }
+ mMaxContentDepth = in.readInt();
+ mMaxCumulativeContentItems = in.readInt();
+ mMaxStringLength = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ for (int drivingState : DRIVING_STATES) {
+ dest.writeTypedList(mUxRestrictions.get(drivingState), 0);
+ }
+ dest.writeInt(mMaxContentDepth);
+ dest.writeInt(mMaxCumulativeContentItems);
+ dest.writeInt(mMaxStringLength);
+ }
+
+ /**
+ * @hide
+ */
+ public static final class Builder {
+
+ private static final int UX_RESTRICTIONS_UNKNOWN = -1;
+
+ private int mMaxContentDepth = UX_RESTRICTIONS_UNKNOWN;
+ private int mMaxCumulativeContentItems = UX_RESTRICTIONS_UNKNOWN;
+ private int mMaxStringLength = UX_RESTRICTIONS_UNKNOWN;
+
+ private Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions = new HashMap<>();
+
+ public Builder() {
+ for (int drivingState : DRIVING_STATES) {
+ mUxRestrictions.put(drivingState, new ArrayList<>());
+ }
+ }
+
+ /**
+ * Sets ux restrictions for driving state.
+ */
+ public Builder setUxRestrictions(@CarDrivingState int drivingState,
+ boolean requiresOptimization,
+ @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
+ return this.setUxRestrictions(drivingState, null, requiresOptimization, restrictions);
+ }
+
+ /**
+ * Sets ux restrictions with speed range.
+ *
+ * @param drivingState Restrictions will be set for this Driving state.
+ * See constants in {@link CarDrivingStateEvent}.
+ * @param speedRange If set, restrictions will only apply when current speed is within
+ * the range. Only {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}
+ * supports speed range. {@code null} implies the full speed range,
+ * i.e. zero to {@link SpeedRange#MAX_SPEED}.
+ * @param requiresOptimization Whether distraction optimization (DO) is required for this
+ * driving state.
+ * @param restrictions See constants in {@link CarUxRestrictions}.
+ */
+ public Builder setUxRestrictions(@CarDrivingState int drivingState,
+ SpeedRange speedRange, boolean requiresOptimization,
+ @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
+ if (drivingState != CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+ if (speedRange != null) {
+ throw new IllegalArgumentException(
+ "Non-moving driving state cannot specify speed range.");
+ }
+ if (mUxRestrictions.get(drivingState).size() > 0) {
+ throw new IllegalArgumentException("Non-moving driving state cannot have "
+ + "more than one set of restrictions.");
+ }
+ }
+
+ mUxRestrictions.get(drivingState).add(
+ new RestrictionsPerSpeedRange(requiresOptimization, restrictions, speedRange));
+ return this;
+ }
+
+ /**
+ * Sets max string length.
+ */
+ public Builder setMaxStringLength(int maxStringLength) {
+ mMaxStringLength = maxStringLength;
+ return this;
+ }
+
+ /**
+ * Sets max cumulative content items.
+ */
+ public Builder setMaxCumulativeContentItems(int maxCumulativeContentItems) {
+ mMaxCumulativeContentItems = maxCumulativeContentItems;
+ return this;
+ }
+
+ /**
+ * Sets max content depth.
+ */
+ public Builder setMaxContentDepth(int maxContentDepth) {
+ mMaxContentDepth = maxContentDepth;
+ return this;
+ }
+
+ /**
+ * @return CarUxRestrictionsConfiguration based on builder configuration.
+ */
+ public CarUxRestrictionsConfiguration build() {
+ // Create default restriction for unspecified driving state.
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+ if (restrictions.size() == 0) {
+ Log.i(TAG, "Using default restrictions for driving state: "
+ + getDrivingStateName(drivingState));
+ restrictions.add(new RestrictionsPerSpeedRange(
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED));
+ }
+ }
+
+ // Configuration validation.
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+
+ if (drivingState == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+ // Sort restrictions based on speed range.
+ Collections.sort(restrictions,
+ (r1, r2) -> r1.mSpeedRange.compareTo(r2.mSpeedRange));
+
+ if (!isAllSpeedRangeCovered(restrictions)) {
+ throw new IllegalStateException(
+ "Moving state should cover full speed range.");
+ }
+ } else {
+ if (restrictions.size() != 1) {
+ throw new IllegalStateException("Non-moving driving state should contain "
+ + "one set of restriction rules.");
+ }
+ }
+ }
+ return new CarUxRestrictionsConfiguration(this);
+ }
+
+ /**
+ * restrictions should be sorted based on speed range.
+ */
+ private boolean isAllSpeedRangeCovered(List<RestrictionsPerSpeedRange> restrictions) {
+ if (restrictions.size() == 1) {
+ if (restrictions.get(0).mSpeedRange == null) {
+ // Single restriction with null speed range implies that
+ // it applies to the entire driving state.
+ return true;
+ }
+ return restrictions.get(0).mSpeedRange.mMinSpeed == 0
+ && Float.compare(restrictions.get(0).mSpeedRange.mMaxSpeed,
+ SpeedRange.MAX_SPEED) == 0;
+ }
+
+ if (restrictions.get(0).mSpeedRange.mMinSpeed != 0) {
+ Log.e(TAG, "Speed range min speed should start at 0.");
+ return false;
+ }
+ for (int i = 1; i < restrictions.size(); i++) {
+ RestrictionsPerSpeedRange prev = restrictions.get(i - 1);
+ RestrictionsPerSpeedRange curr = restrictions.get(i);
+ // If current min != prev.max, there's either an overlap or a gap in speed range.
+ if (Float.compare(curr.mSpeedRange.mMinSpeed, prev.mSpeedRange.mMaxSpeed) != 0) {
+ Log.e(TAG, "Mis-configured speed range. Possibly speed range overlap or gap.");
+ return false;
+ }
+ }
+ // The last speed range should have max speed.
+ float lastMaxSpeed = restrictions.get(restrictions.size() - 1).mSpeedRange.mMaxSpeed;
+ return lastMaxSpeed == SpeedRange.MAX_SPEED;
+ }
+
+ /**
+ * Speed range is defined by min and max speed. When there is no upper bound for max speed,
+ * set it to {@link SpeedRange#MAX_SPEED}.
+ */
+ public static final class SpeedRange implements Comparable<SpeedRange> {
+ public static final float MAX_SPEED = Float.POSITIVE_INFINITY;
+
+ private float mMinSpeed;
+ private float mMaxSpeed;
+
+ /**
+ * Defaults max speed to {@link SpeedRange#MAX_SPEED}.
+ */
+ public SpeedRange(@FloatRange(from = 0.0) float minSpeed) {
+ this(minSpeed, MAX_SPEED);
+ }
+
+ public SpeedRange(@FloatRange(from = 0.0) float minSpeed,
+ @FloatRange(from = 0.0)float maxSpeed) {
+ if (minSpeed == MAX_SPEED) {
+ throw new IllegalArgumentException("Min speed cannot be MAX_SPEED.");
+ }
+ if (maxSpeed < 0) {
+ throw new IllegalArgumentException("Max speed cannot be negative.");
+ }
+ if (minSpeed > maxSpeed) {
+ throw new IllegalArgumentException("Min speed " + minSpeed
+ + " should not be greater than max speed " + maxSpeed);
+ }
+ mMinSpeed = minSpeed;
+ mMaxSpeed = maxSpeed;
+ }
+
+ /**
+ * Return if the given speed is in the range of [minSpeed, maxSpeed).
+ *
+ * @param speed Speed to check
+ * @return {@code true} if in range; {@code false} otherwise.
+ */
+ public boolean includes(float speed) {
+ if (speed < mMinSpeed) {
+ return false;
+ }
+ if (mMaxSpeed == MAX_SPEED) {
+ return true;
+ }
+ return speed < mMaxSpeed;
+ }
+
+ @Override
+ public int compareTo(SpeedRange other) {
+ // First compare min speed; then max speed.
+ int minSpeedComparison = Float.compare(this.mMinSpeed, other.mMinSpeed);
+ if (minSpeedComparison != 0) {
+ return minSpeedComparison;
+ }
+
+ return Float.compare(this.mMaxSpeed, other.mMaxSpeed);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof SpeedRange)) {
+ return false;
+ }
+ SpeedRange other = (SpeedRange) obj;
+
+ return this.compareTo(other) == 0;
+ }
+ }
+ }
+
+ /**
+ * Container for UX restrictions for a speed range.
+ * Speed range is valid only for the {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}.
+ * @hide
+ */
+ public static final class RestrictionsPerSpeedRange implements Parcelable {
+ final boolean mReqOpt;
+ final int mRestrictions;
+ @Nullable
+ final Builder.SpeedRange mSpeedRange;
+
+ public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions) {
+ this(reqOpt, restrictions, null);
+ }
+
+ public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions,
+ @Nullable Builder.SpeedRange speedRange) {
+ if (!reqOpt && restrictions != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) {
+ throw new IllegalArgumentException(
+ "Driving optimization is not required but UX restrictions is required.");
+ }
+ mReqOpt = reqOpt;
+ mRestrictions = restrictions;
+ mSpeedRange = speedRange;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof RestrictionsPerSpeedRange)) {
+ return false;
+ }
+ RestrictionsPerSpeedRange other = (RestrictionsPerSpeedRange) obj;
+ return mReqOpt == other.mReqOpt
+ && mRestrictions == other.mRestrictions
+ && ((mSpeedRange == null && other.mSpeedRange == null) || mSpeedRange.equals(
+ other.mSpeedRange));
+ }
+
+ // Parcelable methods/fields.
+
+ public static final Creator<RestrictionsPerSpeedRange> CREATOR =
+ new Creator<RestrictionsPerSpeedRange>() {
+ @Override
+ public RestrictionsPerSpeedRange createFromParcel(Parcel in) {
+ return new RestrictionsPerSpeedRange(in);
+ }
+
+ @Override
+ public RestrictionsPerSpeedRange[] newArray(int size) {
+ return new RestrictionsPerSpeedRange[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ protected RestrictionsPerSpeedRange(Parcel in) {
+ mReqOpt = in.readBoolean();
+ mRestrictions = in.readInt();
+ // Whether speed range is specified.
+ Builder.SpeedRange speedRange = null;
+ if (in.readBoolean()) {
+ float minSpeed = in.readFloat();
+ float maxSpeed = in.readFloat();
+ speedRange = new Builder.SpeedRange(minSpeed, maxSpeed);
+ }
+ mSpeedRange = speedRange;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeBoolean(mReqOpt);
+ dest.writeInt(mRestrictions);
+ // Whether speed range is specified.
+ dest.writeBoolean(mSpeedRange != null);
+ if (mSpeedRange != null) {
+ dest.writeFloat(mSpeedRange.mMinSpeed);
+ dest.writeFloat(mSpeedRange.mMaxSpeed);
+ }
+ }
+ }
+}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
new file mode 100644
index 0000000..c52f10f
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2018 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 android.car.apitest;
+
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED;
+import static android.car.drivingstate.CarUxRestrictionsConfiguration.Builder.SpeedRange.MAX_SPEED;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.support.test.filters.SmallTest;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+/**
+ * Unit test for UXR config and its subclasses.
+ */
+@SmallTest
+public class CarUxRestrictionsConfigurationTest extends TestCase {
+
+ // This test verifies the expected way to build config would succeed.
+ public void testConstruction() {
+ new CarUxRestrictionsConfiguration.Builder().build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setMaxStringLength(1)
+ .build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+ .build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+ }
+
+ public void testUnspecifiedDrivingStateUsesDefaultRestriction() {
+ CarUxRestrictionsConfiguration config =
+ new CarUxRestrictionsConfiguration.Builder().build();
+
+ CarUxRestrictions parkedRestrictions = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+ assertTrue(parkedRestrictions.isRequiresDistractionOptimization());
+ assertEquals(parkedRestrictions.getActiveRestrictions(), UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ CarUxRestrictions movingRestrictions = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+ assertTrue(movingRestrictions.isRequiresDistractionOptimization());
+ assertEquals(movingRestrictions.getActiveRestrictions(), UX_RESTRICTIONS_FULLY_RESTRICTED);
+ }
+
+ public void testBuilderValidation_MultipleSpeedRange_NonZeroStart() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, 2),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(2, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_SpeedRange_NonZeroStart() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_SpeedRange_Overlap() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(4), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_SpeedRange_Gap() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(8), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_NonMovingStateCannotUseSpeedRange() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ try {
+ builder.setUxRestrictions(DRIVING_STATE_PARKED,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+ } catch (IllegalArgumentException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testSpeedRange_Construction() {
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ }
+
+ public void testSpeedRange_NegativeMax() {
+ try {
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f, -1f);
+ } catch (IllegalArgumentException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testSpeedRange_MinGreaterThanMax() {
+ try {
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, 2f);
+ } catch (IllegalArgumentException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testSpeedRangeComparison_DifferentMin() {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f);
+ assertTrue(s1.compareTo(s2) < 0);
+ assertTrue(s2.compareTo(s1) > 0);
+ }
+
+ public void testSpeedRangeComparison_SameMin() {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ assertTrue(s1.compareTo(s2) == 0);
+ }
+
+ public void testSpeedRangeComparison_SameMinDifferentMax() {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 2f);
+ assertTrue(s1.compareTo(s2) < 0);
+ assertTrue(s2.compareTo(s1) > 0);
+ }
+
+ public void testSpeedRangeComparison_MaxSpeed() {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ assertTrue(s1.compareTo(s2) < 0);
+ assertTrue(s2.compareTo(s1) > 0);
+ }
+
+ public void testSpeedRangeEquals() {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s1, s2;
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ assertTrue(s1.equals(s1));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ assertTrue(s1.compareTo(s2) == 0);
+ assertTrue(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ assertTrue(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ assertTrue(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ assertFalse(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 2f);
+ assertFalse(s1.equals(s2));
+ }
+
+ // TODO: add more tests that each verifies setting one filed in builder can be
+ // successfully retrieved out of saved json.
+ public void testJsonSerialization_DefaultConstructor() {
+ CarUxRestrictionsConfiguration config =
+ new CarUxRestrictionsConfiguration.Builder().build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_RestrictionParameters() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setMaxStringLength(1)
+ .setMaxCumulativeContentItems(1)
+ .setMaxContentDepth(1)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_NonMovingStateRestrictions() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_MovingStateNoSpeedRange() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_MovingStateWithSpeedRange() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 5f),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ private void verifyConfigThroughJsonSerialization(CarUxRestrictionsConfiguration config) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out))) {
+ config.writeJson(writer);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+ try (JsonReader reader = new JsonReader(new InputStreamReader(in))) {
+ CarUxRestrictionsConfiguration deserialized = CarUxRestrictionsConfiguration.readJson(
+ reader);
+ assertTrue(config.equals(deserialized));
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+}