DEV Community

zhonghua
zhonghua

Posted on • Edited on

HarmonyOS Sports Development: How to Monitor User Step Data

HarmonyOS Sports Development: How to Monitor User Step Data

Preface

When developing sports applications, accurately monitoring and recording user steps is a key feature. HarmonyOS provides a powerful sensor framework that allows developers to easily obtain device motion data. This article will delve into how to implement step monitoring in HarmonyOS applications and share some experiences and tips from the development process to help you better understand and implement this feature.

  1. Understanding the HarmonyOS Sensor Framework

HarmonyOS offers a variety of sensors, among which the PEDOMETER (step counter) sensor is the core sensor for obtaining user step data. This sensor returns the cumulative steps since the device was turned on, rather than the incremental steps. This means that we need to handle the initial step value and incremental calculation in the application logic.

  1. Core Code Implementation

2.1 Initializing the Sensor

Before we begin, we need to request the necessary permissions and initialize the sensor. Here is the code for initializing the sensor:

import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import { common } from '@kit.AbilityKit';

export class StepCounterService {
  private static instance: StepCounterService;
  private stepCount: number = 0; // Current cumulative steps
  private initialStepCount: number = 0; // Initial steps (cumulative steps since device startup)
  private isMonitoring: boolean = false; // Whether monitoring is ongoing
  private isPaused: boolean = false; // Whether paused
  private listeners: Array<(steps: number) => void> = [];
  private context: common.UIAbilityContext;
  private pausedIntervals: Array<PauseCounter> = [];
  private deviceTotalSteps: number = 0;

  private constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  public static getInstance(context: common.UIAbilityContext): StepCounterService {
    if (!StepCounterService.instance) {
      StepCounterService.instance = new StepCounterService(context);
    }
    return StepCounterService.instance;
  }

  private async requestMotionPermission(): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    try {
      const result = await atManager.requestPermissionsFromUser(
        this.context,
        ['ohos.permission.ACTIVITY_MOTION']
      );
      return result.permissions[0] === 'ohos.permission.ACTIVITY_MOTION' &&
        result.authResults[0] === 0;
    } catch (err) {
      console.error('Permission request failed:', err);
      return false;
    }
  }

  public async startStepCounter(): Promise<void> {
    if (this.isMonitoring) return;

    const hasPermission = await this.requestMotionPermission();
    if (!hasPermission) {
      throw new Error('Motion sensor permission not granted');
    }

    try {
      sensor.on(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => {
        this.deviceTotalSteps = data.steps;

        if (this.initialStepCount === 0) {
          this.initialStepCount = data.steps;
        }

        const deltaSteps = this.deviceTotalSteps - this.initialStepCount;

        if (this.isPaused) {
          // When paused, only update the device total steps, do not change the business steps
        } else {
          // Calculate the total steps of all paused intervals
          const totalPausedSteps = this.pausedIntervals.reduce((sum, interval) => {
            return sum + (interval.end - interval.start);
          }, 0);

          this.stepCount = deltaSteps - totalPausedSteps;
          this.notifyListeners();
        }
      });

      this.isMonitoring = true;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`Step counter subscription failed: Code=${e.code}, Message=${e.message}`);
      throw error as Error;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2.2 Pause and Resume Functionality

During the sports activity, users may need to pause and resume the activity. To handle this situation, we need to record the steps when pausing and resuming, and calculate the step increment during the pause period when resuming. Here is the implementation of the pause and resume functionality:

public pauseStepCounter(): void {
  if (!this.isMonitoring || this.isPaused) return;
  this.pausedIntervals.push({
    start: this.deviceTotalSteps,
    end: 0
  });
  this.isPaused = true;
}

public resumeStepCounter(): void {
  if (!this.isMonitoring || !this.isPaused) return;

  const lastInterval = this.pausedIntervals[this.pausedIntervals.length - 1];
  lastInterval.end = this.deviceTotalSteps;

  this.isPaused = false;
}
Enter fullscreen mode Exit fullscreen mode

2.3 Stopping the Monitoring

When the user finishes the sports activity, we need to stop monitoring the steps and clean up the related resources:

public stopStepCounter(): void {
  if (!this.isMonitoring) return;

  this.stepCount = 0;
  this.initialStepCount = 0;
  this.isPaused = false;
  this.pausedIntervals = []; // Clear all pause records
  try {
    sensor.off(sensor.SensorId.PEDOMETER);
    this.isMonitoring = false;
  } catch (error) {
    const e = error as BusinessError;
    console.error(`Step counter unsubscription failed: Code=${e.code}, Message=${e.message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Notifying Listeners

To ensure that the interface can update the steps in real-time, we need to notify the listeners when the steps change. To avoid overly frequent notifications, a debounce mechanism can be used:

private debounceTimer?: number;

private notifyListeners(): void {
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
  this.debounceTimer = setTimeout(() => {
    this.listeners.forEach(listener => listener(this.stepCount));
  }, 500); // Notify every 500ms
}
Enter fullscreen mode Exit fullscreen mode
  1. Resetting Steps

In some cases, users may need to reset the steps. Here is the implementation of resetting steps:

public resetStepCounter(): void {
  this.initialStepCount = this.deviceTotalSteps;
  this.pausedIntervals = [];
  this.stepCount = 0;
}
Enter fullscreen mode Exit fullscreen mode
  1. Usage Example

Here is an example of how to use StepCounterService in a page:


@Component
export struct KeepRunningPage {
  @State isRunning: boolean = false;
  @State stepCount: number = 0;

  private stepCounterService?: StepCounterService;

  build() {
    Column() {
      Text('Running Step Counter')
        .fontSize(24)
        .textAlign(TextAlign.Center)
        .margin({ top: 20 })

      Text(`Steps: ${this.stepCount}`)
        .fontSize(18)
        .textAlign(TextAlign.Center)
        .margin({ top: 20 })

      Button('Start Running')
        .onClick(() => this.startRunning())
        .margin({ top: 20 })

      Button('Pause Running')
        .onClick(() => this.pauseRunning())
        .margin({ top: 20 })

      Button('Resume Running')
        .onClick(() => this.resumeRunning())
        .margin({ top: 20 })

      Button('Stop Running')
        .onClick(() => this.stopRunning())
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  aboutToAppear(): void {
    this.stepCounterService = StepCounterService.getInstance(Application.getInstance().uiAbilityContext);
    this.stepCounterService.addStepListener((steps) => {
      this.stepCount = steps;
    });
  }

  aboutToDisappear(): void {
    this.stepCounterService!.removeStepListener((steps) => {
      this.stepCount = steps;
    });
  }

  async startRunning(): Promise<void> {
    if (!this.isRunning) {
      this.isRunning = true;
      await this.stepCounterService!.startStepCounter();
    }
  }

  pauseRunning(): void {
    if (this.isRunning) {
      this.stepCounterService!.pauseStepCounter();
    }
  }

  resumeRunning(): void {
    if (this.isRunning) {
      this.stepCounterService!.resumeStepCounter();
    }
  }

  stopRunning(): void {
    if (this.isRunning) {
      this.isRunning = false;
      this.stepCounterService!.stopStepCounter();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Key Points Summary

6.1 Permission Request

Before using the sensor, the ohos.permission.ACTIVITY_MOTION permission must be requested. This can be achieved through abilityAccessCtrl.createAtManager.

6.2 Initial Step Handling

The PEDOMETER sensor returns the cumulative steps since the device was turned on. Therefore, we need to record the initial steps when we first obtain the data.

6.3 Pause and Resume Mechanism

To handle possible user pauses and resumes, we introduced the pausedIntervals array to record the start and end steps of each pause. In this way, we can accurately calculate the step increment during the pause period when resuming and deduct this part of the steps from the total steps.

6.4 Debounce Mechanism

To avoid performance issues caused by frequent notifications to the listeners, a debounce mechanism is implemented in the notifyListeners method. By setting a timer, we ensure that the listeners are notified only once within a certain time interval (e.g., 500ms), reducing unnecessary updates.

6.5 Reset Functionality

In certain scenarios, users may need to reset the step counter. The resetStepCounter method allows us to set the initial steps to the current total steps of the device and clear all pause records, thereby resetting the step count.

  1. Development Considerations

7.1 Permission Management

In practical applications, permission requests are an essential aspect. If users deny the permission request, the app should kindly prompt the user and provide an option to reapply for the permission.

7.2 Resource Management

Sensor monitoring is an ongoing process, and improper management can lead to resource leaks. Therefore, when monitoring is no longer needed, it is crucial to call the sensor.off method to unsubscribe.

7.3 Data Accuracy

Since the PEDOMETER sensor returns cumulative steps, it is crucial to ensure data accuracy when handling pauses and resumes. Accurate recording and calculation of step increments during pauses and resumes are necessary to avoid data deviations.

  1. Conclusion

We have thoroughly explored how to implement step monitoring in HarmonyOS applications. From initializing the sensor and requesting permissions to implementing pause, resume, and stop monitoring functionalities. We hope that this content will help you develop sports applications more effectively.

Top comments (0)