npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-android-pedometer

v2.0.0

Published

Get steps count on Android devices

Readme

expo-android-pedometer

An Expo module that counts steps on Android with the built-in step counter sensor. It keeps counting in the background through a foreground service, stores per-minute history on the device, and writes steps to Health Connect when the app already holds the permission.

Features

  • Background step counting with a persistent notification
  • Per-minute step history for the last 30 days
  • Hourly totals grouped by local time, including half-hour time zones and DST days
  • Steps walked across midnight, after a reboot, or while the service was stopped are still counted
  • Automatic Health Connect writes when WRITE_STEPS is already granted
  • Self-restoring tracking after reboots, app updates and OEM task killers

Installation

npx expo install expo-android-pedometer

Add the config plugin to app.json:

{
  "expo": {
    "plugins": ["expo-android-pedometer"]
  }
}

The plugin adds the required permissions and raises minSdkVersion to 26. Pass { "isBatteryExemptionEnabled": false } to leave out REQUEST_IGNORE_BATTERY_OPTIMIZATIONS.

Quick start

import * as Pedometer from 'expo-android-pedometer';

async function startTracking() {
  if (!(await Pedometer.isSensorAvailable())) {
    return;
  }

  const activity = await Pedometer.requestActivityPermissions();
  if (!activity.granted) {
    return;
  }
  await Pedometer.requestNotificationPermissions();

  await Pedometer.setupBackgroundUpdates({
    title: 'Step Counter',
    contentTemplate: "You've taken %d steps today",
  });

  const subscription = Pedometer.subscribeToChanges((event) => {
    console.log(`Steps today: ${event.steps}`);
  });

  const today = await Pedometer.getStepsCountAsync();
  console.log(`Today's steps: ${today}`);

  return subscription;
}

API

All functions are exported from expo-android-pedometer. On iOS they resolve to empty or false values.

Sensor and permissions

isSensorAvailable(): Promise<boolean>

Resolves to true when the device has a step counter sensor.

requestActivityPermissions(): Promise<PermissionResponse>

Requests ACTIVITY_RECOGNITION on Android 10 and above, and FOREGROUND_SERVICE_HEALTH on Android 14 and above.

getActivityPermissionStatus(): Promise<PermissionResponse>

Returns the current ACTIVITY_RECOGNITION status.

requestNotificationPermissions(): Promise<PermissionResponse>

Requests POST_NOTIFICATIONS on Android 13 and above. Tracking works without it, but the notification and the restore prompt stay hidden.

getNotificationPermissionStatus(): Promise<PermissionResponse>

Returns the current POST_NOTIFICATIONS status.

PermissionResponse comes from expo-modules-core:

type PermissionResponse = {
  status: 'granted' | 'denied' | 'undetermined';
  granted: boolean;
  expires: 'never' | number;
  canAskAgain: boolean;
};

Background tracking

setupBackgroundUpdates(config: NotificationConfig): Promise<boolean>

Turns on background tracking and starts the foreground service. Tracking stays on across app restarts, reboots and app updates until you call stopBackgroundUpdates(). Calling it again while tracking updates the notification text.

Resolves to true when the service started. Rejects with MISSING_PERMISSIONS when the activity permission is missing, and with INVALID_CONFIG when contentTemplate has no %d.

type NotificationConfig = {
  title?: string;           // Notification title. Default: "Step Counter"
  contentTemplate: string;  // Notification text; %d is replaced with today's steps
  pausedTitle?: string;     // Title of the restore prompt. Default: "Step tracking paused"
  pausedText?: string;      // Text of the restore prompt. Default: "Tap to resume counting steps"
};

stopBackgroundUpdates(): Promise<boolean>

Turns off background tracking. It stops the service, removes the notification, and cancels the watchdog and restart alarms. Stored history is kept. Steps walked while tracking is off are not counted.

getTrackingStatus(): Promise<TrackingStatus>

type TrackingStatus = {
  isEnabled: boolean;                    // setupBackgroundUpdates was called and not stopped
  isRunning: boolean;                    // the foreground service is running now
  isBatteryOptimizationExempt: boolean;
  lastSensorReadingAt: number | null;    // epoch millis of the last accepted sensor reading
};

subscribeToChanges(listener: (event: StepCountChangedEvent) => void): EventSubscription

Calls listener with today's total whenever it changes while the service runs. Call subscription.remove() to stop listening.

type StepCountChangedEvent = {
  steps: number;      // steps today
  timestamp: number;  // epoch millis of the update
};

Step history

History is stored per minute and kept for 30 days.

getStepsCountAsync(dateIsoString?: string): Promise<number>

Returns the total for the local day that contains dateIsoString, or for today when omitted.

const today = await Pedometer.getStepsCountAsync();
const yesterday = await Pedometer.getStepsCountAsync(subDays(new Date(), 1).toISOString());

getStepsCountInRangeAsync(startIso: string, endIso: string): Promise<StepRecord[]>

Returns one record per minute with steps, for minutes that start between startIso and endIso (both inclusive), sorted by time.

type StepRecord = {
  timestamp: number;  // start of the minute, epoch millis
  steps: number;
};

getHourlyStepsForRange(startIso: string, endIso: string): Promise<HourlyStepData[]>

Returns hourly totals grouped by local hour, sorted by time. Hours without steps are left out.

type HourlyStepData = {
  hourStart: number;  // start of the local hour, epoch millis
  steps: number;
};

Keeping tracking alive

isBatteryOptimizationExcluded(): Promise<boolean>

Resolves to true when the app is exempt from battery optimization, or when the device has no such setting.

requestBatteryOptimizationExemption(): Promise<boolean>

Opens the system prompt to exempt the app from battery optimization. Resolves to false when the prompt is not available.

canOpenAutostartSettings(): Promise<boolean>

Resolves to true on devices with a known autostart or background-activity screen: Xiaomi, Redmi, POCO, Huawei, Honor, Oppo, Realme, OnePlus, Vivo, iQOO, Samsung, Asus, Letv, Meizu and Nokia.

openAutostartSettings(): Promise<boolean>

Opens that screen. Resolves to false when none of the known screens could be opened.

Health Connect

getHealthConnectDeviceDataOrigin(): Promise<string | null>

Returns the package name Health Connect uses as the data origin for this device, or null when unsupported.

The module writes one Health Connect steps record per minute. It never asks for Health Connect permission. Request android.permission.health.WRITE_STEPS in your app, for example with react-native-health-connect. Steps recorded before the permission is granted are written once it is granted. Writes are retried until they succeed, and a retry never creates a duplicate record.

Keeping tracking alive

Android and many vendor builds stop background services to save battery. The module recovers in these ways:

  • Periodic watchdog. A WorkManager job runs about every 15 minutes while tracking is on. It restarts the service if it stopped, and it reads the step counter once so steps walked while the service was down are still counted. The same job writes pending steps to Health Connect and removes history older than 30 days.
  • Restart signals. The service restarts after a reboot (including vendor fast boot), after an app update, when the app opens, and a few seconds after the system removes it.
  • Restore prompt. Android 12 and above block starting a foreground service from the background in most cases. When that happens, the module posts a notification with pausedTitle and pausedText. Tapping it resumes tracking.

The step counter keeps counting while the service is stopped, so the steps are counted when tracking resumes. They are spread evenly over the time the service was down, which makes hourly totals for that period approximate.

For the most reliable tracking, ask users to:

  1. Exempt the app from battery optimization with requestBatteryOptimizationExemption().
  2. Allow autostart or background activity with openAutostartSettings() when canOpenAutostartSettings() is true.
  3. Keep the notification permission on, so the restore prompt can appear.

Testing on an emulator

Emulators have no step counter sensor. In debug builds on such a device, the module uses a fake counter that you drive with adb. Release builds never include it.

Replace com.example.app with your application ID. The example app uses expo.modules.androidpedometer.example.

# Grant the activity permission without the dialog
adb shell pm grant com.example.app android.permission.ACTIVITY_RECOGNITION

# Walk 100 steps
adb shell am broadcast -a expo.modules.androidpedometer.debug.FAKE_STEPS -p com.example.app --ei add 100

# Set the raw counter; a value lower than before simulates a sensor reset
adb shell am broadcast -a expo.modules.androidpedometer.debug.FAKE_STEPS -p com.example.app --el raw 20

# Reboot; the fake counter restarts from 0, like the hardware counter
adb reboot

The fake counter keeps its value across process kills. It resets only when the device boot count changes.

scripts/e2e-emulator.sh runs the reliability scenarios against the example app: process kill, blocked background restart and the resume prompt, restart with a battery exemption, a gap across midnight, the watchdog job, and stopping tracking. It needs a rooted emulator image (Google APIs, not Play Store) on Android 12 or newer, the example debug build installed, and Metro running. It clears the example app's data first. Set E2E_REBOOT=1 to include a reboot.

cd example/android && ./gradlew :app:installDebug
cd .. && bunx expo start   # in another terminal
E2E_REBOOT=1 ./scripts/e2e-emulator.sh

Notes

  • Requires Android 8.0 (API 26) or newer and a hardware step counter sensor.
  • Step data stays on the device, apart from the Health Connect writes.
  • Stopping tracking with stopBackgroundUpdates() does not delete history.

License

MIT