THIS SDK ISDEPRECATED.

We rolled out a new and better SDK for you.

Time to leave

Purpose

This article shows how to use the TomTom Maps SDK for Android to create an application that calculates the time to leave previous to reaching the destination point at the desired time.

Getting Started

A user can plan their route to reach the destination by using the following:

  • Departure and destination locations.
  • Travel mode.
  • The time they want to reach the destination.
  • Optionally preparation time before the departure.

Next:

  1. The route is calculated and displayed on a map.
  2. The time to leave timer starts to count down.
  3. The route is recalculated each minute to check for any traffic delays.
  4. In case of any changes in traffic, the countdown timer gets updated and the user is notified.

This article does not cover the whole of the application's code description, only the code sections where the TomTom SDK is used.

You can find and clone the working application on our GitHub. Have fun with modifying it and testing our APIs! If you want to follow a step-by-step application creation from scratch, check out the “Search Along the Route” tutorial.

The following sections describe the usage of:

  • The TomTom LocationSource API to get the user's current location
  • The TomTom Search SDK module to search for addresses and geocode map positions
  • The TomTom Map SDK module to display a map
  • The TomTom Routing SDK module to plan routes

Prerequisites:

  1. Clone a GitHub repository, with the source code of the TimeToLeave application:
    git clone https://github.com/tomtom-international/tomtom-use-case-time-to-leave-android.git
  2. If you don't have an API key visit a How to get a TomTom API key site and create one.
  3. Create a build config fields with the API keys, which will be used later in the application:
    1android {
    2 compileSdkVersion 29
    3 defaultConfig {
    4 buildConfigField("String", "MAPS_API_KEY", "\YOUR_KEY\")
    5 buildConfigField("String", "ROUTING_API_KEY", "\YOUR_KEY\")
    6 buildConfigField("String", "SEARCH_API_KEY", "\YOUR_KEY\")
    7 (...)

Current location

To get the users device location, a LocationSource class is used. This class uses LocationUpdateListener interface to notify about new location updates.

1import com.tomtom.online.sdk.location.FusedLocationSource
2import com.tomtom.online.sdk.location.LocationSource;
3import com.tomtom.online.sdk.location.LocationUpdateListener;
4//(...)
5public class MainActivity extends AppCompatActivity implements LocationUpdateListener {
6 private LocationSource locationSource;
7 //(...)
8}
1import com.tomtom.online.sdk.location.FusedLocationSource
2import com.tomtom.online.sdk.location.LocationSource
3import com.tomtom.online.sdk.location.LocationUpdateListener
4//(...)
5class MainActivity : AppCompatActivity(), LocationUpdateListener {
6 private lateinit var locationSource: LocationSource
7 //(...)
8}

The application needs to have proper permissions granted which is handled by onRequestPermissionsResult callback, so that location callbacks can be retrieved successfully. A PermissionChecker class is used to check whether the application has already assigned proper permissions. If not, then a requestPermissions function is called so that the user sees a permission request dialog. If location permissions have been granted inside an onRequestPermissionsResult function, a locationSource.activate() method is invoked. As a result, the application receives a GPS location in an onLocationChanged callback function.

1private void initLocationSource() {
2 PermissionChecker permissionChecker = AndroidPermissionChecker.createLocationChecker(this);
3 if(permissionChecker.ifNotAllPermissionGranted()) {
4 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION,
5 Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_LOCATION);
6 }
7 locationSource = new FusedLocationSource(this, LocationRequest.create());
8 locationSource.addLocationUpdateListener(this);
9}
10
11@Override
12public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
13 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
14 if (requestCode == PERMISSION_REQUEST_LOCATION) {
15 if (grantResults.length >= 2 &&
16 grantResults[0] == PackageManager.PERMISSION_GRANTED &&
17 grantResults[1] == PackageManager.PERMISSION_GRANTED) {
18 locationSource.activate();
19 } else {
20 Toast.makeText(this, R.string.location_permissions_denied, Toast.LENGTH_SHORT).show();
21 }
22 }
23}
24
25@Override
26public void onLocationChanged(Location location) {
27 if (latLngCurrentPosition == null) {
28 latLngCurrentPosition = new LatLng(location);
29 locationSource.deactivate();
30 }
31}
1private fun initLocationSource() {
2 val permissionChecker = AndroidPermissionChecker.createLocationChecker(this)
3 if (permissionChecker.ifNotAllPermissionGranted()) {
4 ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_LOCATION)
5 }
6 locationSource = FusedLocationSource(this, LocationRequest.create())
7 locationSource.addLocationUpdateListener(this)
8}
9
10override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
11 super.onRequestPermissionsResult(requestCode, permissions, grantResults)
12 if (requestCode == PERMISSION_REQUEST_LOCATION) {
13 if (grantResults.size >= 2 &&
14 grantResults[0] == PackageManager.PERMISSION_GRANTED &&
15 grantResults[1] == PackageManager.PERMISSION_GRANTED) {
16 locationSource.activate()
17 } else {
18 Toast.makeText(this, R.string.location_permissions_denied, Toast.LENGTH_SHORT).show()
19 }
20 }
21}
22
23override fun onLocationChanged(location: Location) {
24 if (latLngCurrentPosition == null) {
25 latLngCurrentPosition = LatLng(location)
26 locationSource.deactivate()
27 }
28}

To feed departure and destination autocomplete text fields with the list of suggested positions, Results from the Search API query are used.

  1. First, a searchAPI object is defined and initialized:

    1private SearchApi searchApi;
    2//(...)
    3searchApi = OnlineSearchApi.create(this, BuildConfig.SEARCH_API_KEY);
    1private lateinit var searchApi: SearchApi
    2//(...)
    3searchApi = OnlineSearchApi.create(this, BuildConfig.SEARCH_API_KEY)
  2. Then, a searchAddress function handles points of interest (POI) & address search. It takes two parameters:

    1. A search word which is used inside a query.
    2. An AutoCompleteTextView object which is used to match whether an afterTextChanged event has been called from the departure text field instead of the destination field.
    1private void searchAddress(final String searchWord, final AutoCompleteTextView autoCompleteTextView) {
    2 searchApi.search(new FuzzySearchQueryBuilder(searchWord)
    3 .withLanguage(Locale.getDefault().toLanguageTag())
    4 .withTypeAhead(true)
    5 .withMinFuzzyLevel(SEARCH_FUZZY_LVL_MIN).build())
    6 .subscribeOn(Schedulers.io())
    7 .observeOn(AndroidSchedulers.mainThread())
    8 .subscribe(new DisposableSingleObserver<FuzzySearchResponse>() {
    9 @Override
    10 public void onSuccess(FuzzySearchResponse fuzzySearchResponse) {
    11 if (!fuzzySearchResponse.getResults().isEmpty()) {
    12 searchAutocompleteList.clear();
    13 searchResultsMap.clear();
    14 if (autoCompleteTextView == atvDepartureLocation && latLngCurrentPosition != null) {
    15 String currentLocationTitle = getString(R.string.main_current_position);
    16 searchAutocompleteList.add(currentLocationTitle);
    17 searchResultsMap.put(currentLocationTitle, latLngCurrentPosition);
    18 }
    19 for (FuzzySearchResult result : fuzzySearchResponse.getResults()) {
    20 String addressString = result.getAddress().getFreeformAddress();
    21 searchAutocompleteList.add(addressString);
    22 searchResultsMap.put(addressString, result.getPosition());
    23 }
    24 searchAdapter.clear();
    25 searchAdapter.addAll(searchAutocompleteList);
    26 searchAdapter.getFilter().filter("");
    27 }
    28 }
    29
    30 @Override
    31 public void onError(Throwable e) {
    32 Toast.makeText(MainActivity.this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
    33 }
    34 });
    35}
    1private fun searchAddress(searchWord: String, autoCompleteTextView: AutoCompleteTextView) {
    2 searchApi.search(FuzzySearchQueryBuilder(searchWord)
    3 .withLanguage(Locale.getDefault().toLanguageTag())
    4 .withTypeAhead(true)
    5 .withMinFuzzyLevel(SEARCH_FUZZY_LVL_MIN).build())
    6 .subscribeOn(Schedulers.io())
    7 .observeOn(AndroidSchedulers.mainThread())
    8 .subscribe(object : DisposableSingleObserver<FuzzySearchResponse>() {
    9 override fun onSuccess(fuzzySearchResponse: FuzzySearchResponse) {
    10 if (!fuzzySearchResponse.results.isEmpty()) {
    11 searchAutocompleteList.clear()
    12 searchResultsMap.clear()
    13 if (autoCompleteTextView === atv_main_departure_location && latLngCurrentPosition != null) {
    14 val currentLocationTitle = getString(R.string.main_current_position)
    15 searchAutocompleteList.add(currentLocationTitle)
    16 searchResultsMap[currentLocationTitle] = latLngCurrentPosition!!
    17 }
    18 for (result in fuzzySearchResponse.results) {
    19 val addressString = result.address.freeformAddress
    20 searchAutocompleteList.add(addressString)
    21 searchResultsMap[addressString] = result.position
    22 }
    23 searchAdapter.apply {
    24 this.clear()
    25 this.addAll(searchAutocompleteList)
    26 this.filter.filter("")
    27 }
    28 }
    29 }
    30
    31 override fun onError(e: Throwable) {
    32 Toast.makeText(this@MainActivity, e.localizedMessage, Toast.LENGTH_SHORT).show()
    33 }
    34 })
    35}
  3. Inside the searchAddress function, a searchApi.search(...) method is called.

  4. It takes a FuzzySearchQuery object as a parameter where you can provide necessary information like search results language, category, position etc.

  5. FuzzySearchQueryBuilder is used to build the FuzzySearchQuery object.

  6. The following list of options can set the search query:

    • withLanguage(Locale.getDefault().toLanguageTag()) - to return the results in default language on the mobile device.
    • withTypeAhead(true)) - to treat the searchWord query as a partial input and search service enters a predictive mode.
    • withMinFuzzyLevel(2)) - to set the fuzziness level to use normal n-gram spell checking. Feel free to experiment with other fuzziness levels.
  7. The search method from the searchApi object, returns a FuzzySearchResponse observable object.

    • When search operation is finished, it emits either a successful value or an error.
    • If a successful value is emitted, a method named onSuccess is executed in the subscribing DisposableSingleObserver, otherwise an onError method is executed.
    • The same searchAddress function is used in the destination text field control.
    • If there are any results in the FuzzySearchResponse object and the current location is known, then it is added to the departure autocomplete list of suggestions as a first option.
    • Then this list is filled with addresses from the FuzzySearchResponse object.
  8. When the departure position is set, the user can choose destination position, arrival time, preparation time and a travel mode. All these parameters are gathered and passed to a CountDownActivity.prepareIntent method.

    1Intent intent = CountdownActivity.prepareIntent(
    2 MainActivity.this,
    3 latLngDeparture,
    4 latLngDestination,
    5 travelModeSelected,
    6 arrivalTimeInMillis,
    7 preparationTimeSelected);
    8startActivity(intent);
    1val intent = CountdownActivity.prepareIntent(
    2 this@MainActivity,
    3 latLngDeparture,
    4 latLngDestination,
    5 travelModeSelected,
    6 arrivalTimeInMillis,
    7 preparationTimeSelected)
    8startActivity(intent)

TomTom Map display module

A CountDown activity uses map and routing modules from the TomTom Maps SDK. It implements an OnMapReadyCallback interface, so that an onMapReady method is called after the TomTom map is ready to be used.

1public class CountdownActivity extends AppCompatActivity implements OnMapReadyCallback {
2 private TomtomMap tomtomMap;
3 private RoutingApi routingApi;
4 //(...)
5
6 @Override
7 public void onMapReady(@NonNull TomtomMap tomtomMap) {
8 this.tomtomMap = tomtomMap;
9 this.tomtomMap.setMyLocationEnabled(true);
10 this.tomtomMap.clear();
11 showDialog(dialogInProgress);
12 requestRoute(departure, destination, travelMode, arriveAt);
13 }
1class CountdownActivity : AppCompatActivity(), OnMapReadyCallback {
2 private lateinit var tomtomMap: TomtomMap
3 private lateinit var routingApi: RoutingApi
4 //(...)
5
6 override fun onMapReady(tomtomMap: TomtomMap) {
7 this.tomtomMap = tomtomMap
8 tomtomMap.apply {
9 this.isMyLocationEnabled = true
10 this.clear()
11 }
12 showDialog(dialogInProgress)
13 requestRoute(departure, destination, travelMode, arriveAt)
14 }
1<fragment
2android:id="@+id/mapFragment"
3android:name="com.tomtom.online.sdk.map.MapFragment"
4android:layout_width="@dimen/size_none"
5android:layout_height="@dimen/size_none"
6android:layout_marginTop="@dimen/size_none"
7app:layout_constraintBottom_toBottomOf="parent"
8app:layout_constraintEnd_toEndOf="parent"
9app:layout_constraintStart_toStartOf="parent"
10app:layout_constraintTop_toBottomOf="@id/layout_countdown_timer"/>

After the startActivity method is called inside a CountDownActivity.onCreate(...) method, TomTom services and all other settings gathered from previous activity are initialized.

1@Override
2protected void onCreate(Bundle savedInstanceState) {
3 super.onCreate(savedInstanceState);
4 setContentView(R.layout.activity_countdown);
5
6 initTomTomServices();
7 initToolbarSettings();
8 Bundle settings = getIntent().getBundleExtra(BUNDLE_SETTINGS);
9 initActivitySettings(settings);
10}
11
12private void initTomTomServices() {
13 routingApi = OnlineRoutingApi.create(this, BuildConfig.ROUTING_API_KEY);
14 Map<ApiKeyType, String> mapKeys = new HashMap<>();
15 mapKeys.put(ApiKeyType.MAPS_API_KEY, BuildConfig.MAPS_API_KEY);
16
17 MapProperties mapProperties = new MapProperties.Builder()
18 .keys(mapKeys)
19 .build();
20 MapFragment mapFragment = MapFragment.newInstance(mapProperties);
21 getSupportFragmentManager()
22 .beginTransaction()
23 .replace(R.id.mapFragment, mapFragment)
24 .commit();
25 mapFragment.getAsyncMap(this);
26}
1override fun onCreate(savedInstanceState: Bundle?) {
2 super.onCreate(savedInstanceState)
3 setContentView(R.layout.activity_countdown)
4
5 initTomTomServices()
6 initToolbarSettings()
7 initActivitySettings(intent.getBundleExtra(BUNDLE_SETTINGS))
8}
9
10private fun initTomTomServices() {
11 routingApi = OnlineRoutingApi.create(applicationContext, BuildConfig.ROUTING_API_KEY)
12 val mapKeys = mapOf(ApiKeyType.MAPS_API_KEY to BuildConfig.MAPS_API_KEY)
13 val mapProperties = MapProperties.Builder().keys(mapKeys).build()
14 val mapFragment = MapFragment.newInstance(mapProperties)
15 supportFragmentManager
16 .beginTransaction()
17 .replace(R.id.mapFragment, mapFragment)
18 .commit()
19 mapFragment.getAsyncMap(this)
20}

When the map is ready to be used, TomTom SDK automatically invokes an onMapReady callback method where:

  • The tomtomMap private field is initialized.
  • An 'in progress' dialog is displayed.
  • A requestRoute method is called.

TomTom Routing module

The CountDown activity uses TomTom Routing APIs to perform routing requests and operations like drawing a route on the map or displaying start and end route icons.

  1. First, a private RoutingApi class member is declared and initialized inside an initTomTomServices function which is shown in several previous paragraphs. The requestRoute method takes four parameters:

    • LatLng departure position,
    • LatLng destination position,
    • TravelMode which describes which type of transport user choosed,
    • A date which determines expected arrival time.
    1private static final int ONE_MINUTE_IN_MILLIS = 60000;
    2private static final int ROUTE_RECALCULATION_DELAY = ONE_MINUTE_IN_MILLIS;
    3
    4private void requestRoute(final LatLng departure, final LatLng destination, TravelMode byWhat, Date arriveAt) {
    5 if (!isInPauseMode) {
    6 //(...)
    7 }
    8 else {
    9 timerHandler.removeCallbacks(requestRouteRunnable);
    10 timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY);
    11 }
    12}
    1private const val ONE_MINUTE_IN_MILLIS = 60000
    2private const val ROUTE_RECALCULATION_DELAY = ONE_MINUTE_IN_MILLIS
    3
    4private fun requestRoute(departure: LatLng?, destination: LatLng?, byWhat: TravelMode?, arriveAt: Date?) {
    5 if (!isInPauseMode) {
    6 //(...)
    7 } else {
    8 timerHandler.removeCallbacks(requestRouteRunnable)
    9 timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY.toLong())
    10 }
    11}
    • If the application is in the pause mode, a requestRouteRunnable object is posted in a timerHandler with a one-minute delay.

    • The RequestRouteRunnable and TimerHandler objects are defined as a private members of the CountDownActivity class:

      1private final Handler timerHandler = new Handler();
      2
      3private final Runnable requestRouteRunnable = new Runnable() {
      4 @Override
      5 public void run() {
      6 requestRoute(departure, destination, travelMode, arriveAt);
      7 }
      8};
      1private val timerHandler = Handler()
      2
      3private val requestRouteRunnable = Runnable { requestRoute(departure, destination, travelMode, arriveAt) }
  • If the application is not in the pause mode, the RouteSpecification object needs to be prepared in order to plan a route.
1if (!isInPauseMode) {
2 RouteDescriptor routeDescriptor = new RouteDescriptor.Builder()
3 .routeType(RouteType.FASTEST)
4 .considerTraffic(true)
5 .travelMode(byWhat)
6 .build();
7
8 RouteCalculationDescriptor routeCalculationDescriptor = new RouteCalculationDescriptor.Builder()
9 .routeDescription(routeDescriptor)
10 .arriveAt(arriveAt)
11 .build();
12
13 RouteSpecification routeSpecification = new RouteSpecification.Builder(departure, destination)
14 .routeCalculationDescriptor(routeCalculationDescriptor)
15 .build();
16 //(...)
1if (!isInPauseMode) {
2 val routeDescriptor = RouteDescriptor.Builder()
3 .routeType(com.tomtom.online.sdk.routing.route.description.RouteType.FASTEST)
4 .considerTraffic(true)
5 .travelMode(byWhat!!)
6 .build()
7
8 val routeCalculationDescriptor = RouteCalculationDescriptor.Builder()
9 .routeDescription(routeDescriptor)
10 .arriveAt(arriveAt!!)
11 .build()
12
13 val routeSpecification = RouteSpecification.Builder(departure!!, destination!!)
14 .routeCalculationDescriptor(routeCalculationDescriptor)
15 .build()
16 //(...)
  1. The RouteDescriptor and RouteCalculationDescriptor are used to construct the RouteSpecification object:

    • routeType - to allow the users to select one type of route from: fastest, shortest, thrilling or eco.
    • considerTraffic - when enabled, current traffic information is used during the route planning, when disabled - only historical traffic data.
    • travelMode – you can choose one of the following: car, pedestrian, bicycle, truck, other params are listed here.
    • arriveAt - a date and time of arrival at the destination point. It’s an important method inside the application because it changes a departure date which later is used to calculate time left before the departure.
  2. When the RouteSpecification object is created, it is passed as a parameter to a planRoute function from the routingApi object. This function takes as an argument a RouteCallback object. In case of an error, an onError method is called, inside this method the current activity is finished. As a result, the application returns to the previous screen.

    1@Override
    2public void onError(Throwable e) {
    3 hideDialog(dialogInProgress);
    4 Toast.makeText(CountdownActivity.this, getString(R.string.toast_error_message_cannot_find_route), Toast.LENGTH_LONG).show();
    5 CountdownActivity.this.finish();
    6}
    1override fun onError(e: Throwable) {
    2 hideDialog(dialogInProgress)
    3 Toast.makeText(this@CountdownActivity, getString(R.string.toast_error_message_cannot_find_route), Toast.LENGTH_LONG).show()
    4 this@CountdownActivity.finish()
    5}
  3. When the planRoute function is finished, an onSuccess(@NotNull RoutePlan routePlan) function is called and proper actions can be executed.

    1routingApi.planRoute(routeSpecification, new RouteCallback() {
    2 @Override
    3 public void onSuccess(@NotNull RoutePlan routePlan) {
    4 hideDialog(dialogInProgress);
    5
    6 if (!routePlan.getRoutes().isEmpty()) {
    7 FullRoute fullRoute = routePlan.getRoutes().get(0);
    8 int currentTravelTime = fullRoute.getSummary().getTravelTimeInSeconds();
    9 if (previousTravelTime != currentTravelTime) {
    10 int travelDifference = previousTravelTime - currentTravelTime;
    11 if (previousTravelTime != 0) {
    12 showWarningSnackbar(prepareWarningMessage(travelDifference));
    13 }
    14 previousTravelTime = currentTravelTime;
    15 displayRouteOnMap(fullRoute.getCoordinates());
    16 String departureTimeString = fullRoute.getSummary().getDepartureTime();
    17 setupCountDownTimer(new DateFormatter().formatWithTimeZone(departureTimeString).toDate());
    18 } else {
    19 infoSnackbar.show();
    20 }
    21 }
    22 timerHandler.removeCallbacks(requestRouteRunnable);
    23 timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY);
    24 }
    1routingApi.planRoute(routeSpecification, object : RouteCallback {
    2 override fun onSuccess(routePlan: RoutePlan) {
    3 hideDialog(dialogInProgress)
    4 if (routePlan.routes.isNotEmpty()) {
    5 val fullRoute = routePlan.routes.first()
    6 val currentTravelTime = fullRoute.summary.travelTimeInSeconds
    7 if (previousTravelTime != currentTravelTime) {
    8 val travelDifference = previousTravelTime - currentTravelTime
    9 if (previousTravelTime != 0) {
    10 showWarningSnackbar(prepareWarningMessage(travelDifference))
    11 }
    12 previousTravelTime = currentTravelTime
    13 displayRouteOnMap(fullRoute.getCoordinates())
    14 val departureTimeString = fullRoute.summary.departureTime
    15 setupCountDownTimer(DateFormatter().formatWithTimeZone(departureTimeString).toDate())
    16 } else {
    17 infoSnackbar.show()
    18
    19 }
    20 }
    21 timerHandler.removeCallbacks(requestRouteRunnable)
    22 timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY.toLong())
    23 }
  4. The most important task in previous code example is to get a current travel time and compare it to the previous one.

    • If travel times are the same, a Snackbar notification is shown to the user informing him, that there are no changes in the route time since the last check.
    • Otherwise a travel time difference is calculated and stored in a travelDifference variable.
    • Then, a snackbar notification is shown to the user with the calculated travel time difference.
  5. Next, the current travel time is stored in a previousTravelTime variable.

    • The route is displayed on the map.
    • A setupCountDownTimer function is called
    1private void setupCountDownTimer(Date departure) {
    2 if (isCountdownTimerSet()) {
    3 countDownTimer.cancel();
    4 }
    5 Date now = Calendar.getInstance().getTime();
    6 final int preparationTimeMillis = preparationTime * ONE_MINUTE_IN_MILLIS;
    7 long timeToLeave = departure.getTime() - now.getTime();
    8 countDownTimer = new CountDownTimer(timeToLeave, ONE_SECOND_IN_MILLIS) {
    9 public void onTick(long millisUntilFinished) {
    10 updateCountdownTimerTextViews(millisUntilFinished);
    11 if (!isPreparationMode && millisUntilFinished <= preparationTimeMillis) {
    12 isPreparationMode = true;
    13 setCountdownTimerColor(COUNTDOWN_MODE_PREPARATION);
    14 if (!isInPauseMode) {
    15 showPreparationInfoDialog();
    16 }
    17 }
    18 }
    19
    20 public void onFinish() {
    21 timerHandler.removeCallbacks(requestRouteRunnable);
    22 setCountdownTimerColor(COUNTDOWN_MODE_FINISHED);
    23 if (!isInPauseMode) {
    24 createDialogWithCustomButtons();
    25 }
    26 }
    27 }.start();
    28 textViewTravelTime.setText(getString(R.string.travel_time_text, formatTimeFromSecondsDisplayWithoutSeconds(previousTravelTime)));
    29}
    1private fun setupCountDownTimer(departure: Date) {
    2 countDownTimer?.cancel()
    3
    4 val now = Calendar.getInstance().time
    5 val preparationTimeMillis = preparationTime * ONE_MINUTE_IN_MILLIS
    6 val timeToLeave = departure.time - now.time
    7 countDownTimer = object : CountDownTimer(timeToLeave, ONE_SECOND_IN_MILLIS.toLong()) {
    8 override fun onTick(millisUntilFinished: Long) {
    9 updateCountdownTimerTextViews(millisUntilFinished)
    10 if (!isPreparationMode && millisUntilFinished <= preparationTimeMillis) {
    11 isPreparationMode = true
    12 setCountdownTimerColor(COUNTDOWN_MODE_PREPARATION)
    13 if (!isInPauseMode) {
    14 showPreparationInfoDialog()
    15 }
    16 }
    17 }
    18
    19 override fun onFinish() {
    20 timerHandler.removeCallbacks(requestRouteRunnable)
    21 setCountdownTimerColor(COUNTDOWN_MODE_FINISHED)
    22 if (!isInPauseMode) {
    23 createDialogWithCustomButtons()
    24 }
    25 }
    26 }.start()
    27 text_countdown_travel_time.text = getString(R.string.travel_time_text, formatTimeFromSecondsDisplayWithoutSeconds(previousTravelTime.toLong()))
    28}
  6. The setupCountDownTimer function takes the departure date as a parameter. It checks if there is already a countdown timer service already running in the background, and if there is, it cancels it. Next, a current date is stored in a currentDate variable and the preparation time taken from the first activity is stored in a preparationTimeMillis variable. A time difference between the departure time and the current time is stored in a timeToLeave variable. It is later passed to a CountDownTimer constructor.

  7. The CountDownTimer class implements two callbacks:

    • The first callback is called onTick(long millisUntilFinished).
    • It updates time info and checks whether the application is in a preparation mode.
    • If this is the case, the color of the timer text views changes, and a preparation dialog is displayed to the user.
    • onFinish() is called by the timer when counting down is completed.
      • In this function, requestRouteRunnable callback is removed from timerHandler’s queue by calling removeCallbacks method.
      • The color of a timer text views is updated again, and final alert dialog window is displayed to the user with nicely designed message that there is a time to leave!

Happy coding!

Summary

This tutorial describes the TomTom SDK and APIs used to build a Time To Leave application. Source code can be downloaded from Github.