/* * 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.cluster; import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.NORMAL; import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.REROUTING; import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.SERVICE_STATUS_UNSPECIFIED; import android.annotation.Nullable; import android.car.cluster.navigation.NavigationState.Destination; import android.car.cluster.navigation.NavigationState.Destination.Traffic; import android.car.cluster.navigation.NavigationState.Distance; import android.car.cluster.navigation.NavigationState.ImageReference; import android.car.cluster.navigation.NavigationState.Maneuver; import android.car.cluster.navigation.NavigationState.NavigationStateProto; import android.car.cluster.navigation.NavigationState.Road; import android.car.cluster.navigation.NavigationState.Step; import android.car.cluster.navigation.NavigationState.Timestamp; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import java.time.Instant; /** * View controller for navigation state rendering. */ public class NavStateController { private static final String TAG = "Cluster.NavController"; private Handler mHandler = new Handler(); private View mNavigationState; private LinearLayout mSectionManeuver; private LinearLayout mSectionNavigation; private LinearLayout mSectionServiceStatus; private ImageView mManeuver; private ImageView mProvidedManeuver; private LaneView mLane; private LaneView mProvidedLane; private TextView mDistance; private TextView mSegment; private TextView mEta; private CueView mCue; private Context mContext; private ImageResolver mImageResolver; /** * Creates a controller to coordinate updates to the views displaying navigation state * data. * * @param container {@link View} containing the navigation state views */ public NavStateController(View container) { mNavigationState = container; mSectionManeuver = container.findViewById(R.id.section_maneuver); mSectionNavigation = container.findViewById(R.id.section_navigation); mSectionServiceStatus = container.findViewById(R.id.section_service_status); mManeuver = container.findViewById(R.id.maneuver); mProvidedManeuver = container.findViewById(R.id.provided_maneuver); mLane = container.findViewById(R.id.lane); mProvidedLane = container.findViewById(R.id.provided_lane); mDistance = container.findViewById(R.id.distance); mSegment = container.findViewById(R.id.segment); mEta = container.findViewById(R.id.eta); mCue = container.findViewById(R.id.cue); mContext = container.getContext(); } public void hideNavigationStateInfo() { mNavigationState.setVisibility(View.INVISIBLE); } public void showNavigationStateInfo() { mNavigationState.setVisibility(View.VISIBLE); } public void setImageResolver(@Nullable ImageResolver imageResolver) { mImageResolver = imageResolver; } /** * Updates views to reflect the provided navigation state */ public void update(@Nullable NavigationStateProto state) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Updating nav state: " + state); } if (state == null) { return; } NavigationStateProto.ServiceStatus serviceStatus = state.getServiceStatus(); if (serviceStatus == SERVICE_STATUS_UNSPECIFIED) { mSectionManeuver.setVisibility(View.INVISIBLE); mSectionNavigation.setVisibility(View.INVISIBLE); mSectionServiceStatus.setVisibility(View.INVISIBLE); return; } else if (serviceStatus == REROUTING) { mSectionManeuver.setVisibility(View.INVISIBLE); mSectionNavigation.setVisibility(View.INVISIBLE); mSectionServiceStatus.setVisibility(View.VISIBLE); return; } else { mSectionManeuver.setVisibility(View.VISIBLE); mSectionNavigation.setVisibility(View.VISIBLE); mSectionServiceStatus.setVisibility(View.GONE); } Step step = state.getStepsCount() > 0 ? state.getSteps(0) : null; // Get alpha based on is_imminent float alpha = (step != null && !step.getIsImminent()) ? getAlphaFromResource(R.dimen.non_imminent_alpha) : 1f; Destination destination = state.getDestinationsCount() > 0 ? state.getDestinations(0) : null; Traffic traffic = destination != null ? destination.getTraffic() : null; String eta = destination != null ? destination.getFormattedDurationUntilArrival().isEmpty() ? formatEta(destination.getEstimatedTimeAtArrival()) : destination.getFormattedDurationUntilArrival() : null; mEta.setText(eta); mEta.setTextColor(getTrafficColor(traffic)); mManeuver.setImageDrawable(getManeuverIcon(step != null ? step.getManeuver() : null)); setProvidedManeuverIcon(mProvidedManeuver, step != null ? step.getManeuver().hasIcon() ? step.getManeuver().getIcon() : null : null); mManeuver.setImageAlpha((int) (alpha * 255)); mDistance.setText(formatDistance(step != null ? step.getDistance() : null)); mDistance.setAlpha(alpha); mSegment.setText(getSegmentString(state.getCurrentRoad())); mCue.setCue(step != null ? step.getCue() : null, mImageResolver, alpha); if (step != null && step.getLanesCount() > 0) { if (step.hasLanesImage()) { mProvidedLane.setLanes(step.getLanesImage(), mImageResolver); mProvidedLane.setVisibility(View.VISIBLE); } mLane.setLanes(step.getLanesList(), alpha); mLane.setVisibility(View.VISIBLE); } else { mLane.setVisibility(View.GONE); mProvidedLane.setVisibility(View.GONE); } } /** * Get float value from dimens.xml, it only works for float format */ private float getAlphaFromResource(int alphaId) { TypedValue typedValue = new TypedValue(); mContext.getResources().getValue(alphaId, typedValue, true); return typedValue.getFloat(); } private int getTrafficColor(@Nullable Traffic traffic) { if (traffic == Traffic.LOW) { return mContext.getColor(R.color.low_traffic); } else if (traffic == Traffic.MEDIUM) { return mContext.getColor(R.color.medium_traffic); } else if (traffic == Traffic.HIGH) { return mContext.getColor(R.color.high_traffic); } return mContext.getColor(R.color.unknown_traffic); } private String formatEta(@Nullable Timestamp eta) { long seconds = eta.getSeconds() - Instant.now().getEpochSecond(); // Round up to the nearest minute seconds = (long) Math.ceil(seconds / 60d) * 60; long minutes = (seconds / 60) % 60; long hours = (seconds / 3600) % 24; long days = seconds / (3600 * 24); if (days > 0) { return String.format("%d d %d hr", days, hours); } else if (hours > 0) { return String.format("%d hr %d min", hours, minutes); } else { return String.format("%d min", minutes); } } private String getSegmentString(Road segment) { if (segment != null) { return segment.getName(); } return null; } private void setProvidedManeuverIcon(ImageView view, ImageReference imageReference) { if (mImageResolver == null || imageReference == null) { view.setImageBitmap(null); return; } mImageResolver .getBitmap(imageReference, 0, view.getHeight()) .thenAccept(bitmap -> { mHandler.post(() -> { view.setImageBitmap(bitmap); }); }) .exceptionally(ex -> { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Unable to fetch image for maneuver: " + imageReference); } return null; }); } private Drawable getManeuverIcon(@Nullable Maneuver maneuver) { if (maneuver == null) { return null; } switch (maneuver.getType()) { case UNKNOWN: return null; case DEPART: return mContext.getDrawable(R.drawable.direction_depart); case NAME_CHANGE: return mContext.getDrawable(R.drawable.direction_new_name_straight); case KEEP_LEFT: return mContext.getDrawable(R.drawable.direction_continue_left); case KEEP_RIGHT: return mContext.getDrawable(R.drawable.direction_continue_right); case TURN_SLIGHT_LEFT: return mContext.getDrawable(R.drawable.direction_turn_slight_left); case TURN_SLIGHT_RIGHT: return mContext.getDrawable(R.drawable.direction_turn_slight_right); case TURN_NORMAL_LEFT: return mContext.getDrawable(R.drawable.direction_turn_left); case TURN_NORMAL_RIGHT: return mContext.getDrawable(R.drawable.direction_turn_right); case TURN_SHARP_LEFT: return mContext.getDrawable(R.drawable.direction_turn_sharp_left); case TURN_SHARP_RIGHT: return mContext.getDrawable(R.drawable.direction_turn_sharp_right); case U_TURN_LEFT: return mContext.getDrawable(R.drawable.direction_uturn_left); case U_TURN_RIGHT: return mContext.getDrawable(R.drawable.direction_uturn_right); case ON_RAMP_SLIGHT_LEFT: return mContext.getDrawable(R.drawable.direction_on_ramp_slight_left); case ON_RAMP_SLIGHT_RIGHT: return mContext.getDrawable(R.drawable.direction_on_ramp_slight_right); case ON_RAMP_NORMAL_LEFT: return mContext.getDrawable(R.drawable.direction_on_ramp_left); case ON_RAMP_NORMAL_RIGHT: return mContext.getDrawable(R.drawable.direction_on_ramp_right); case ON_RAMP_SHARP_LEFT: return mContext.getDrawable(R.drawable.direction_on_ramp_sharp_left); case ON_RAMP_SHARP_RIGHT: return mContext.getDrawable(R.drawable.direction_on_ramp_sharp_right); case ON_RAMP_U_TURN_LEFT: return mContext.getDrawable(R.drawable.direction_uturn_left); case ON_RAMP_U_TURN_RIGHT: return mContext.getDrawable(R.drawable.direction_uturn_right); case OFF_RAMP_SLIGHT_LEFT: return mContext.getDrawable(R.drawable.direction_off_ramp_slight_left); case OFF_RAMP_SLIGHT_RIGHT: return mContext.getDrawable(R.drawable.direction_off_ramp_slight_right); case OFF_RAMP_NORMAL_LEFT: return mContext.getDrawable(R.drawable.direction_off_ramp_left); case OFF_RAMP_NORMAL_RIGHT: return mContext.getDrawable(R.drawable.direction_off_ramp_right); case FORK_LEFT: return mContext.getDrawable(R.drawable.direction_fork_left); case FORK_RIGHT: return mContext.getDrawable(R.drawable.direction_fork_right); case MERGE_LEFT: return mContext.getDrawable(R.drawable.direction_merge_left); case MERGE_RIGHT: return mContext.getDrawable(R.drawable.direction_merge_right); case MERGE_SIDE_UNSPECIFIED: return mContext.getDrawable(R.drawable.direction_merge_unspecified); case ROUNDABOUT_ENTER: return mContext.getDrawable(R.drawable.direction_roundabout); case ROUNDABOUT_EXIT: return mContext.getDrawable(R.drawable.direction_roundabout); case ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_RIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_sharp_right); case ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_RIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_right); case ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_RIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_slight_right); case ROUNDABOUT_ENTER_AND_EXIT_CW_STRAIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_straight); case ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_LEFT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_sharp_left); case ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_LEFT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_left); case ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_LEFT: return mContext.getDrawable(R.drawable.direction_roundabout_cw_slight_left); case ROUNDABOUT_ENTER_AND_EXIT_CW_U_TURN: return mContext.getDrawable(R.drawable.direction_uturn_right); case ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_RIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_sharp_right); case ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_RIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_right); case ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_RIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_slight_right); case ROUNDABOUT_ENTER_AND_EXIT_CCW_STRAIGHT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_straight); case ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_LEFT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_sharp_left); case ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_LEFT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_left); case ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_LEFT: return mContext.getDrawable(R.drawable.direction_roundabout_ccw_slight_left); case ROUNDABOUT_ENTER_AND_EXIT_CCW_U_TURN: return mContext.getDrawable(R.drawable.direction_uturn_left); case STRAIGHT: return mContext.getDrawable(R.drawable.direction_continue); case FERRY_BOAT: return mContext.getDrawable(R.drawable.direction_close); case FERRY_TRAIN: return mContext.getDrawable(R.drawable.direction_close); case DESTINATION: return mContext.getDrawable(R.drawable.direction_arrive); case DESTINATION_STRAIGHT: return mContext.getDrawable(R.drawable.direction_arrive_straight); case DESTINATION_LEFT: return mContext.getDrawable(R.drawable.direction_arrive_left); case DESTINATION_RIGHT: return mContext.getDrawable(R.drawable.direction_arrive_right); } return null; } private String formatDistance(@Nullable Distance distance) { if (distance == null || distance.getDisplayUnits() == Distance.Unit.UNKNOWN) { return null; } String unit = ""; switch (distance.getDisplayUnits()) { case METERS: unit = "m"; break; case KILOMETERS: unit = "km"; break; case MILES: unit = "mi"; break; case YARDS: unit = "yd"; break; case FEET: unit = "ft"; break; } return String.format("In %s %s", distance.getDisplayValue(), unit); } }