DEV Community

HarmonyOS
HarmonyOS

Posted on

Continuous Location Tracking with Geofence Alerts

Read the original article:Continuous Location Tracking with Geofence Alerts

Continuous Location Tracking with Geofence Alerts

Requirement Description

Wearable apps often need continuous location tracking for fitness and navigation. Beyond just fetching the user's coordinates once, applications must also montior entry and exit from defined geographic areas (geofences).

This scenario shows how to:

  1. Request user permission for location services.
  2. Start continuous location updates.
  3. Define a geofence (circle area)
  4. Trigger custom logic when entering/exiting the area.
  5. Stop updates when no longer needed.

Background Knowledge

  • Location data is accessed via @ohos.geoLocationManager.
  • Needs runtime permission handling.
  • Updates may consume significant battery. Only subscribe when required.
  • Geofencing is not directly built-in, must be implemented with math.

Implementation Steps

1.Request user authorization using a UIAbility;

   //EntryAbility.ets
   // Use UIExtensionAbility: Replace import { UIAbility } from '@kit.AbilityKit' with import { UIExtensionAbility } from '@kit.AbilityKit'.
   import { abilityAccessCtrl, common, Permissions, UIAbility } from '@kit.AbilityKit';
   import { window } from '@kit.ArkUI';
   import { BusinessError } from '@kit.BasicServicesKit';

   const permissions: Array<Permissions> = ['ohos.permission.LOCATION','ohos.permission.APPROXIMATELY_LOCATION'];
   // Use UIExtensionAbility: Replace common.UIAbilityContext with common.UIExtensionContext.
   function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
     let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
     // The return value of requestPermissionsFromUser determines whether to display a dialog box to request user authorization.
     atManager.requestPermissionsFromUser(context, permissions).then((data) => {
       let grantStatus: Array<number> = data.authResults;
       let length: number = grantStatus.length;
       for (let i = 0; i < length; i++) {
         if (grantStatus[i] === 0) {
           // If the user grants the permission, the application continues the operation.
         } else {
           // If the user denies the permission, a message is displayed, indicating that user authorization is required and instructing the user to set the permission in Settings.
           return;
         }
       }
      // Authorization is successful.
     }).catch((err: BusinessError) => {
       console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
     })
   }
   // Use UIExtensionAbility: Replace UIAbility with UIExtensionAbility.
   export default class EntryAbility extends UIAbility {
     onWindowStageCreate(windowStage: window.WindowStage): void {
       // ...
       windowStage.loadContent('pages/Index', (err, data) => {
         reqPermissionsFromUser(permissions, this.context);
       // ...
       });
     }

     // ...
   }
Enter fullscreen mode Exit fullscreen mode

2.Applying for Permissions: ohos.permission.APPROXIMATELY_LOCATION, ohos.permission.APPROXIMATELY_LOCATION and ohos.permission.LOCATION.

   //module.json5
    "requestPermissions": [
         {
           "name": "ohos.permission.LOCATION",
           "reason": "$string:location_permission_reason",
           "usedScene": {
             "abilities": ["EntryAbility"],
             "when": "inuse"
           }
         },
         {
           "name": "ohos.permission.APPROXIMATELY_LOCATION",
           "reason": "$string:location_permission_reason",
           "usedScene": {
             "abilities": ["EntryAbility"],
             "when": "inuse"
           }
         }
       ],
Enter fullscreen mode Exit fullscreen mode

3.Create a location request with interval.

4.Implement Haversine distance calculation to simulate geofence detection.

5.Display updates in UI and stop listener in onDisappear.

Code Snippet / Configuration

//MainPage.ets
import geoLocationManager from '@ohos.geoLocationManager';
import {BusinessError} from '@ohos.base'

interface GeoFenceType {
  lat: number;
  lon: number;
  radius: number;
}
//Example geofence
const GEOFENCE: GeoFenceType = {lat: 22.87707, lon: 113.88187, radius: 100}

@Entry
@Component
struct MainPage {
  @State latitude: string = "-"
  @State longitude: string = "-"
  @State insideFence: boolean = false

  async aboutToAppear() {
//Use ContinuousLocationRequest as the input parameter.
    let request:geoLocationManager.ContinuousLocationRequest = {'interval': 1, 'locationScenario': geoLocationManager.UserActivityScenario.NAVIGATION};
    let locationCallback = (location:geoLocationManager.Location):void => {
      console.info('locationCallback: data: ' + JSON.stringify(location));
      this.latitude = location.latitude.toFixed(5)
      this.longitude = location.longitude.toFixed(5)
      const inside = this.isInsideGeofence(location.latitude, location.longitude)
      this.insideFence = inside
    }
    try {
      geoLocationManager.on('locationChange', request, locationCallback);
    }catch (err){
      let e: BusinessError = err as BusinessError
      console.error(e.code + e.message)
    }
  }
  aboutToDisappear() {
    let locationChange = (location:geoLocationManager.Location):void => {
      console.info('locationChange: data: ' + JSON.stringify(location));
    };
      geoLocationManager.off('locationChange',locationChange)

  }
  private isInsideGeofence(lat: number, lon: number) : boolean{
    const R = 6371000;//Earth radius in meters
    const dLat = (lat - GEOFENCE.lat) * Math.PI / 180
    const dLon = (lon - GEOFENCE.lon) * Math.PI / 180
    const a = Math.sin(dLat/2)**2 +
      Math.cos(GEOFENCE.lat * Math.PI/180) * Math.cos(lat * Math.PI/180) *
        Math.sin(dLon/2)**2
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
    const distance = R * c
    return distance <= GEOFENCE.radius
  }
  build() {
    Column({space:6}){
      Text(`Latitude: ${this.latitude}`).fontSize(12)
      Text(`Longitude: ${this.longitude}`).fontSize(12)
      Text(`Inside Geofence: ${this.insideFence ? 'YES' : 'NO'}`).fontSize(12)
    }  .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

Enter fullscreen mode Exit fullscreen mode

Test Results

output8.gif

Limitations or Considerations

Not applicable to apps targeting API Level < 11.

Related Documents or Links

Written by Emrecan Karakas

Top comments (0)