1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.car.cluster; 17 18 import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.NORMAL; 19 import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.REROUTING; 20 import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.SERVICE_STATUS_UNSPECIFIED; 21 22 import android.annotation.Nullable; 23 import android.car.cluster.navigation.NavigationState.Destination; 24 import android.car.cluster.navigation.NavigationState.Destination.Traffic; 25 import android.car.cluster.navigation.NavigationState.Distance; 26 import android.car.cluster.navigation.NavigationState.ImageReference; 27 import android.car.cluster.navigation.NavigationState.Maneuver; 28 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 29 import android.car.cluster.navigation.NavigationState.Road; 30 import android.car.cluster.navigation.NavigationState.Step; 31 import android.car.cluster.navigation.NavigationState.Timestamp; 32 import android.content.Context; 33 import android.graphics.drawable.Drawable; 34 import android.os.Handler; 35 import android.util.Log; 36 import android.util.TypedValue; 37 import android.view.View; 38 import android.widget.ImageView; 39 import android.widget.LinearLayout; 40 import android.widget.TextView; 41 42 import java.time.Instant; 43 44 /** 45 * View controller for navigation state rendering. 46 */ 47 public class NavStateController { 48 private static final String TAG = "Cluster.NavController"; 49 50 private Handler mHandler = new Handler(); 51 52 private View mNavigationState; 53 private LinearLayout mSectionManeuver; 54 private LinearLayout mSectionNavigation; 55 private LinearLayout mSectionServiceStatus; 56 57 private ImageView mManeuver; 58 private ImageView mProvidedManeuver; 59 private LaneView mLane; 60 private LaneView mProvidedLane; 61 private TextView mDistance; 62 private TextView mSegment; 63 private TextView mEta; 64 private CueView mCue; 65 private Context mContext; 66 private ImageResolver mImageResolver; 67 68 /** 69 * Creates a controller to coordinate updates to the views displaying navigation state 70 * data. 71 * 72 * @param container {@link View} containing the navigation state views 73 */ NavStateController(View container)74 public NavStateController(View container) { 75 mNavigationState = container; 76 mSectionManeuver = container.findViewById(R.id.section_maneuver); 77 mSectionNavigation = container.findViewById(R.id.section_navigation); 78 mSectionServiceStatus = container.findViewById(R.id.section_service_status); 79 80 mManeuver = container.findViewById(R.id.maneuver); 81 mProvidedManeuver = container.findViewById(R.id.provided_maneuver); 82 mLane = container.findViewById(R.id.lane); 83 mProvidedLane = container.findViewById(R.id.provided_lane); 84 mDistance = container.findViewById(R.id.distance); 85 mSegment = container.findViewById(R.id.segment); 86 mEta = container.findViewById(R.id.eta); 87 mCue = container.findViewById(R.id.cue); 88 89 mContext = container.getContext(); 90 } 91 hideNavigationStateInfo()92 public void hideNavigationStateInfo() { 93 mNavigationState.setVisibility(View.INVISIBLE); 94 } 95 showNavigationStateInfo()96 public void showNavigationStateInfo() { 97 mNavigationState.setVisibility(View.VISIBLE); 98 } 99 setImageResolver(@ullable ImageResolver imageResolver)100 public void setImageResolver(@Nullable ImageResolver imageResolver) { 101 mImageResolver = imageResolver; 102 } 103 104 /** 105 * Updates views to reflect the provided navigation state 106 */ update(@ullable NavigationStateProto state)107 public void update(@Nullable NavigationStateProto state) { 108 if (Log.isLoggable(TAG, Log.DEBUG)) { 109 Log.d(TAG, "Updating nav state: " + state); 110 } 111 112 if (state == null) { 113 return; 114 } 115 116 NavigationStateProto.ServiceStatus serviceStatus = state.getServiceStatus(); 117 if (serviceStatus == SERVICE_STATUS_UNSPECIFIED) { 118 mSectionManeuver.setVisibility(View.INVISIBLE); 119 mSectionNavigation.setVisibility(View.INVISIBLE); 120 mSectionServiceStatus.setVisibility(View.INVISIBLE); 121 return; 122 } else if (serviceStatus == REROUTING) { 123 mSectionManeuver.setVisibility(View.INVISIBLE); 124 mSectionNavigation.setVisibility(View.INVISIBLE); 125 mSectionServiceStatus.setVisibility(View.VISIBLE); 126 return; 127 } else { 128 mSectionManeuver.setVisibility(View.VISIBLE); 129 mSectionNavigation.setVisibility(View.VISIBLE); 130 mSectionServiceStatus.setVisibility(View.GONE); 131 } 132 133 Step step = state.getStepsCount() > 0 ? state.getSteps(0) : null; 134 135 // Get alpha based on is_imminent 136 float alpha = (step != null && !step.getIsImminent()) 137 ? getAlphaFromResource(R.dimen.non_imminent_alpha) 138 : 1f; 139 140 Destination destination = state.getDestinationsCount() > 0 141 ? state.getDestinations(0) : null; 142 Traffic traffic = destination != null ? destination.getTraffic() : null; 143 String eta = destination != null 144 ? destination.getFormattedDurationUntilArrival().isEmpty() 145 ? formatEta(destination.getEstimatedTimeAtArrival()) 146 : destination.getFormattedDurationUntilArrival() 147 : null; 148 mEta.setText(eta); 149 mEta.setTextColor(getTrafficColor(traffic)); 150 mManeuver.setImageDrawable(getManeuverIcon(step != null ? step.getManeuver() : null)); 151 setProvidedManeuverIcon(mProvidedManeuver, step != null 152 ? step.getManeuver().hasIcon() ? step.getManeuver().getIcon() : null 153 : null); 154 mManeuver.setImageAlpha((int) (alpha * 255)); 155 mDistance.setText(formatDistance(step != null ? step.getDistance() : null)); 156 mDistance.setAlpha(alpha); 157 mSegment.setText(getSegmentString(state.getCurrentRoad())); 158 mCue.setCue(step != null ? step.getCue() : null, mImageResolver, alpha); 159 160 if (step != null && step.getLanesCount() > 0) { 161 if (step.hasLanesImage()) { 162 mProvidedLane.setLanes(step.getLanesImage(), mImageResolver); 163 mProvidedLane.setVisibility(View.VISIBLE); 164 } 165 166 mLane.setLanes(step.getLanesList(), alpha); 167 mLane.setVisibility(View.VISIBLE); 168 } else { 169 mLane.setVisibility(View.GONE); 170 mProvidedLane.setVisibility(View.GONE); 171 } 172 173 } 174 175 /** 176 * Get float value from dimens.xml, it only works for float format 177 */ getAlphaFromResource(int alphaId)178 private float getAlphaFromResource(int alphaId) { 179 TypedValue typedValue = new TypedValue(); 180 mContext.getResources().getValue(alphaId, typedValue, true); 181 return typedValue.getFloat(); 182 } 183 getTrafficColor(@ullable Traffic traffic)184 private int getTrafficColor(@Nullable Traffic traffic) { 185 if (traffic == Traffic.LOW) { 186 return mContext.getColor(R.color.low_traffic); 187 } else if (traffic == Traffic.MEDIUM) { 188 return mContext.getColor(R.color.medium_traffic); 189 } else if (traffic == Traffic.HIGH) { 190 return mContext.getColor(R.color.high_traffic); 191 } 192 193 return mContext.getColor(R.color.unknown_traffic); 194 } 195 formatEta(@ullable Timestamp eta)196 private String formatEta(@Nullable Timestamp eta) { 197 long seconds = eta.getSeconds() - Instant.now().getEpochSecond(); 198 199 // Round up to the nearest minute 200 seconds = (long) Math.ceil(seconds / 60d) * 60; 201 202 long minutes = (seconds / 60) % 60; 203 long hours = (seconds / 3600) % 24; 204 long days = seconds / (3600 * 24); 205 206 if (days > 0) { 207 return String.format("%d d %d hr", days, hours); 208 } else if (hours > 0) { 209 return String.format("%d hr %d min", hours, minutes); 210 } else { 211 return String.format("%d min", minutes); 212 } 213 } 214 getSegmentString(Road segment)215 private String getSegmentString(Road segment) { 216 if (segment != null) { 217 return segment.getName(); 218 } 219 220 return null; 221 } 222 setProvidedManeuverIcon(ImageView view, ImageReference imageReference)223 private void setProvidedManeuverIcon(ImageView view, ImageReference imageReference) { 224 if (mImageResolver == null || imageReference == null) { 225 view.setImageBitmap(null); 226 return; 227 } 228 229 mImageResolver 230 .getBitmap(imageReference, 0, view.getHeight()) 231 .thenAccept(bitmap -> { 232 mHandler.post(() -> { 233 view.setImageBitmap(bitmap); 234 }); 235 }) 236 .exceptionally(ex -> { 237 if (Log.isLoggable(TAG, Log.DEBUG)) { 238 Log.d(TAG, "Unable to fetch image for maneuver: " + imageReference); 239 } 240 return null; 241 }); 242 } 243 getManeuverIcon(@ullable Maneuver maneuver)244 private Drawable getManeuverIcon(@Nullable Maneuver maneuver) { 245 if (maneuver == null) { 246 return null; 247 } 248 switch (maneuver.getType()) { 249 case UNKNOWN: 250 return null; 251 case DEPART: 252 return mContext.getDrawable(R.drawable.direction_depart); 253 case NAME_CHANGE: 254 return mContext.getDrawable(R.drawable.direction_new_name_straight); 255 case KEEP_LEFT: 256 return mContext.getDrawable(R.drawable.direction_continue_left); 257 case KEEP_RIGHT: 258 return mContext.getDrawable(R.drawable.direction_continue_right); 259 case TURN_SLIGHT_LEFT: 260 return mContext.getDrawable(R.drawable.direction_turn_slight_left); 261 case TURN_SLIGHT_RIGHT: 262 return mContext.getDrawable(R.drawable.direction_turn_slight_right); 263 case TURN_NORMAL_LEFT: 264 return mContext.getDrawable(R.drawable.direction_turn_left); 265 case TURN_NORMAL_RIGHT: 266 return mContext.getDrawable(R.drawable.direction_turn_right); 267 case TURN_SHARP_LEFT: 268 return mContext.getDrawable(R.drawable.direction_turn_sharp_left); 269 case TURN_SHARP_RIGHT: 270 return mContext.getDrawable(R.drawable.direction_turn_sharp_right); 271 case U_TURN_LEFT: 272 return mContext.getDrawable(R.drawable.direction_uturn_left); 273 case U_TURN_RIGHT: 274 return mContext.getDrawable(R.drawable.direction_uturn_right); 275 case ON_RAMP_SLIGHT_LEFT: 276 return mContext.getDrawable(R.drawable.direction_on_ramp_slight_left); 277 case ON_RAMP_SLIGHT_RIGHT: 278 return mContext.getDrawable(R.drawable.direction_on_ramp_slight_right); 279 case ON_RAMP_NORMAL_LEFT: 280 return mContext.getDrawable(R.drawable.direction_on_ramp_left); 281 case ON_RAMP_NORMAL_RIGHT: 282 return mContext.getDrawable(R.drawable.direction_on_ramp_right); 283 case ON_RAMP_SHARP_LEFT: 284 return mContext.getDrawable(R.drawable.direction_on_ramp_sharp_left); 285 case ON_RAMP_SHARP_RIGHT: 286 return mContext.getDrawable(R.drawable.direction_on_ramp_sharp_right); 287 case ON_RAMP_U_TURN_LEFT: 288 return mContext.getDrawable(R.drawable.direction_uturn_left); 289 case ON_RAMP_U_TURN_RIGHT: 290 return mContext.getDrawable(R.drawable.direction_uturn_right); 291 case OFF_RAMP_SLIGHT_LEFT: 292 return mContext.getDrawable(R.drawable.direction_off_ramp_slight_left); 293 case OFF_RAMP_SLIGHT_RIGHT: 294 return mContext.getDrawable(R.drawable.direction_off_ramp_slight_right); 295 case OFF_RAMP_NORMAL_LEFT: 296 return mContext.getDrawable(R.drawable.direction_off_ramp_left); 297 case OFF_RAMP_NORMAL_RIGHT: 298 return mContext.getDrawable(R.drawable.direction_off_ramp_right); 299 case FORK_LEFT: 300 return mContext.getDrawable(R.drawable.direction_fork_left); 301 case FORK_RIGHT: 302 return mContext.getDrawable(R.drawable.direction_fork_right); 303 case MERGE_LEFT: 304 return mContext.getDrawable(R.drawable.direction_merge_left); 305 case MERGE_RIGHT: 306 return mContext.getDrawable(R.drawable.direction_merge_right); 307 case MERGE_SIDE_UNSPECIFIED: 308 return mContext.getDrawable(R.drawable.direction_merge_unspecified); 309 case ROUNDABOUT_ENTER: 310 return mContext.getDrawable(R.drawable.direction_roundabout); 311 case ROUNDABOUT_EXIT: 312 return mContext.getDrawable(R.drawable.direction_roundabout); 313 case ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_RIGHT: 314 return mContext.getDrawable(R.drawable.direction_roundabout_cw_sharp_right); 315 case ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_RIGHT: 316 return mContext.getDrawable(R.drawable.direction_roundabout_cw_right); 317 case ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_RIGHT: 318 return mContext.getDrawable(R.drawable.direction_roundabout_cw_slight_right); 319 case ROUNDABOUT_ENTER_AND_EXIT_CW_STRAIGHT: 320 return mContext.getDrawable(R.drawable.direction_roundabout_cw_straight); 321 case ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_LEFT: 322 return mContext.getDrawable(R.drawable.direction_roundabout_cw_sharp_left); 323 case ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_LEFT: 324 return mContext.getDrawable(R.drawable.direction_roundabout_cw_left); 325 case ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_LEFT: 326 return mContext.getDrawable(R.drawable.direction_roundabout_cw_slight_left); 327 case ROUNDABOUT_ENTER_AND_EXIT_CW_U_TURN: 328 return mContext.getDrawable(R.drawable.direction_uturn_right); 329 case ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_RIGHT: 330 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_sharp_right); 331 case ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_RIGHT: 332 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_right); 333 case ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_RIGHT: 334 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_slight_right); 335 case ROUNDABOUT_ENTER_AND_EXIT_CCW_STRAIGHT: 336 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_straight); 337 case ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_LEFT: 338 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_sharp_left); 339 case ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_LEFT: 340 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_left); 341 case ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_LEFT: 342 return mContext.getDrawable(R.drawable.direction_roundabout_ccw_slight_left); 343 case ROUNDABOUT_ENTER_AND_EXIT_CCW_U_TURN: 344 return mContext.getDrawable(R.drawable.direction_uturn_left); 345 case STRAIGHT: 346 return mContext.getDrawable(R.drawable.direction_continue); 347 case FERRY_BOAT: 348 return mContext.getDrawable(R.drawable.direction_close); 349 case FERRY_TRAIN: 350 return mContext.getDrawable(R.drawable.direction_close); 351 case DESTINATION: 352 return mContext.getDrawable(R.drawable.direction_arrive); 353 case DESTINATION_STRAIGHT: 354 return mContext.getDrawable(R.drawable.direction_arrive_straight); 355 case DESTINATION_LEFT: 356 return mContext.getDrawable(R.drawable.direction_arrive_left); 357 case DESTINATION_RIGHT: 358 return mContext.getDrawable(R.drawable.direction_arrive_right); 359 } 360 return null; 361 } 362 formatDistance(@ullable Distance distance)363 private String formatDistance(@Nullable Distance distance) { 364 if (distance == null || distance.getDisplayUnits() == Distance.Unit.UNKNOWN) { 365 return null; 366 } 367 368 String unit = ""; 369 370 switch (distance.getDisplayUnits()) { 371 case METERS: 372 unit = "m"; 373 break; 374 case KILOMETERS: 375 unit = "km"; 376 break; 377 case MILES: 378 unit = "mi"; 379 break; 380 case YARDS: 381 unit = "yd"; 382 break; 383 case FEET: 384 unit = "ft"; 385 break; 386 } 387 return String.format("In %s %s", distance.getDisplayValue(), unit); 388 } 389 } 390