How To Make an Android Run Tracking App

Learn how to make an Android run tracking app to show a user’s position on a map, along with their path. By Roberto Orgiu.

5 (1) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Asking for Another Permission

Starting with Android Q, recognizing a user’s activity requires a new permission. So, you need to add more logic inside the PermissionManager.

First, change the constructor of the PermissionManager so that it accepts an instance of the StepCounter:

class PermissionsManager(activity: AppCompatActivity,
    private val locationProvider: LocationProvider,
    private val stepCounter: StepCounter)

Then, declare the callback that runs when the user grants the permission on Android Q and newer. This will set up the step counter:

private val activityRecognitionPermissionProvider = 
  activity.registerForActivityResult(
      ActivityResultContracts.RequestPermission()
  ) { granted ->
    if (granted) {
      stepCounter.setupStepCounter()
    }
}

Finally, create a method that asks for the permission on Android Q and newer, or simply setup the step counter on older versions of the operating system:

fun requestActivityRecognition() {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    activityRecognitionPermissionProvider.launch(Manifest.permission.ACTIVITY_RECOGNITION)
  } else {
    stepCounter.setupStepCounter()
  }
}

Adding More Glue

Now, head back to MapPresenter.kt and add a new instance of the StepCounter close to the declaration of the LocationProvider:

private val stepCounter = StepCounter(activity)

You also need to pass this new stepCounter to the newly updated PermissionsManager:

private val permissionsManager = PermissionsManager(activity, locationProvider, stepCounter)

Now, scroll to onViewCreated and add a listener to the LiveData, so that the data from the StepCounter can flow with the others you previously listened to:

stepCounter.liveSteps.observe(activity) { steps ->
  val current = ui.value
  ui.value = current?.copy(formattedPace = "$steps")
}

Next, add the permission request for the number of steps in startTracking. Since the user can remove permissions at any moment, every time they track their activities, you’ll need to ask for the permission again.

Don’t worry. If you have the permission already, the callback will run automatically:

permissionsManager.requestActivityRecognition()

Finally, unload the StepCounter when the user stops recording their activity, in onStopTracking:

stepCounter.unloadStepCounter()

The only thing left to take care of is the UI. Time to do it!

Drawing the UI, At Last!

Open MapsActivity.kt. You don’t need any reference to the PermissionManager nor to the LocationProvider, so, delete them. In their place, add a reference to the MapPresenter:

private val presenter = MapPresenter(this)

Once you do this, in your activity’s onCreate, use the binding to reach out for the start button and add the following logic:

binding.btnStartStop.setOnClickListener {
  if (binding.btnStartStop.text == getString(R.string.start_label)) {
    //1
    startTracking()
    binding.btnStartStop.setText(R.string.stop_label)
  } else {
    //2
    stopTracking()
    binding.btnStartStop.setText(R.string.start_label)
  }
}

In this snippet, first, you check if the button’s label is START. If it is, you start the tracking and change the label to STOP. Otherwise, you assume the label is STOP already, stop the tracking and change the label back to START.

The IDE is complaining, isn’t it? You’ll deal with it in a second. But first, add the presenter callback right below the bindings for the button:

presenter.onViewCreated()

This method tells the presenter that your UI is ready and to attach all the listeners to the LiveDatas.

Next, you’ll change the UI a little so that it better fits the code you’re about to write.

Open activity_maps.xml and replace the include tag with:

<include
      android:id="@+id/container"
      layout="@layout/layout_indicators" />

Here, you add a new container id that you’ll use later to access the fields in the layout. You need one more step.

Switch to layout_indicators.xml and find the TextView with id @+id/textTime. Change it’s type to a Chronometer, so that it looks like this:

<Chronometer
      android:id="@+id/txtTime"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:fontFamily="monospace"
      tools:text="45 minutes"
      app:layout_constraintBottom_toBottomOf="@+id/txtTimeLabel"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_bias="1.0"
      app:layout_constraintStart_toEndOf="@+id/txtTimeLabel"
      app:layout_constraintTop_toTopOf="@+id/txtTimeLabel" />

Here, you change the TextView so that it tracks the elapsed time in a much easier way and displays it in the underlying TextView.

Back to the IDE warnings. To make it happy, you need to add some code. Paste this snippet in MapsActivity as class methods:

private fun startTracking() {
  //1
  binding.container.txtPace.text = ""
  binding.container.txtDistance.text = ""
  //2
  binding.container.txtTime.base = SystemClock.elapsedRealtime()
  binding.container.txtTime.start()
  //3
  map.clear()

  //4
  presenter.startTracking()
}

private fun stopTracking() {
  presenter.stopTracking()
  binding.container.txtTime.stop()
}

A few good things happen here:

  1. First, you reset the pace and distance fields since the user wants to record a new activity.
  2. Second, you set the time label to the current time and restart it. You do this because it only starts counting from the last moment recorded, so if you don’t reset it, you’ll have some weird data.
  3. Third, you clear the map. You don’t want to have old markers because you only care about the new ones coming in any minute.
  4. Last but not least, you ask the presenter to start tracking, and all the data will flow!

When you stop tracking, you stop all the presenter’s functions and also stop the time label. You don’t need to reset anything yet. This way, you also have a nice recap frame for your users.

You’re about to make the IDE unhappy once more! Right below these two methods, paste:

@SuppressLint("MissingPermission")
private fun updateUi(ui: Ui) {
  //1
  if (ui.currentLocation != null && ui.currentLocation != map.cameraPosition.target) {
    map.isMyLocationEnabled = true
    map.animateCamera(CameraUpdateFactory.newLatLngZoom(ui.currentLocation, 14f))
  }
  //2
  binding.container.txtDistance.text = ui.formattedDistance
  binding.container.txtPace.text = ui.formattedPace
  //3
  drawRoute(ui.userPath)
}

Here’s what you did:

  1. You check that the new position you get is not null and that it’s different from the position on the map. If you skip this step, you’ll experience crashes since the location could be null or weird flickers as you move the camera in and out from the same position. If the check passes, you move the camera to the new location, as you did earlier.
  2. Next, you bind the formatted data to the bindings.
  3. Finally, you draw the user’s path!

Scroll a little bit, and paste this code as another function of the MapsActivity:

private fun drawRoute(locations: List<LatLng>) {
  val polylineOptions = PolylineOptions()

  map.clear()

  val points = polylineOptions.points
  points.addAll(locations)

  map.addPolyline(polylineOptions)
}

This function is all you need to draw a line with several points on the UI.

  1. First, you create a PolylineOptions, an object that contains the points to draw on the map.
  2. Then you clear everything on the map itself.
  3. Finally, you add all the new locations and plot the line on the map.

Done!

Well, not quite. There’s one more step to take.

Are you ready? Great!

Scroll to the onMapReady method and change so that it looks like this:

override fun onMapReady(googleMap: GoogleMap) {
  map = googleMap

  //1
  presenter.ui.observe(this) { ui ->
    updateUi(ui)
  }

  //2
  presenter.onMapLoaded()
  map.uiSettings.isZoomControlsEnabled = true
}

First, you observe the data from the presenter so that you change the UI accordingly every time there’s an update. Second, you tell the presenter that the map loaded, so it can ask for the location permission if the user removed it.

Now, you really did it. It’s time to build your app and go out for a walk. You have to test it, don’t you?

Start your app and walk around. It’ll track you and display some data, more or less like this: