You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
samples/experimental/pedometer/example/lib/steps_repo.dart

162 lines
5.5 KiB

import 'dart:async';
import 'dart:collection';
import 'dart:ffi' as ffi;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:jni/jni.dart' as jni;
import 'package:pedometer/pedometer_bindings_generated.dart' as pd;
import 'package:pedometer/health_connect.dart' as hc;
/// Class to hold the information needed for the chart
class Steps {
final String startHour;
final int steps;
Steps(this.startHour, this.steps);
}
abstract class StepsRepo {
static const _formatString = "yyyy-MM-dd HH:mm:ss";
static StepsRepo? _instance;
static StepsRepo get instance =>
_instance ??= Platform.isAndroid ? _AndroidStepsRepo() : _IOSStepsRepo();
Future<List<Steps>> getSteps();
}
class _IOSStepsRepo implements StepsRepo {
static const _dylibPath =
'/System/Library/Frameworks/CoreMotion.framework/CoreMotion';
// Bindings for the CMPedometer class.
final lib = pd.PedometerBindings(ffi.DynamicLibrary.open(_dylibPath));
// Bindings for the helper function.
final helpLib = pd.PedometerBindings(ffi.DynamicLibrary.process());
late final pd.CMPedometer client;
late final pd.NSDateFormatter formatter;
late final pd.NSDateFormatter hourFormatter;
_IOSStepsRepo() {
// Contains the Dart API helper functions
final dylib = ffi.DynamicLibrary.open("pedometer.framework/pedometer");
// Initialize the Dart API
final initializeApi = dylib.lookupFunction<
ffi.IntPtr Function(ffi.Pointer<ffi.Void>),
int Function(ffi.Pointer<ffi.Void>)>('Dart_InitializeApiDL');
final initializeResult = initializeApi(ffi.NativeApi.initializeApiDLData);
if (initializeResult != 0) {
throw StateError('failed to init API.');
}
// Create a new CMPedometer instance.
client = pd.CMPedometer.new1(lib);
// Setting the formatter for date strings.
formatter =
pd.NSDateFormatter.castFrom(pd.NSDateFormatter.alloc(lib).init());
formatter.dateFormat = pd.NSString(lib, "${StepsRepo._formatString} zzz");
hourFormatter =
pd.NSDateFormatter.castFrom(pd.NSDateFormatter.alloc(lib).init());
hourFormatter.dateFormat = pd.NSString(lib, "HH");
}
pd.NSDate dateConverter(DateTime dartDate) {
// Format dart date to string.
final formattedDate = DateFormat(StepsRepo._formatString).format(dartDate);
// Get current timezone.
// If eastern african change to AST to follow with NSDate.
final tz = dartDate.timeZoneName == "EAT" ? "AST" : dartDate.timeZoneName;
// Create a new NSString with the formatted date and timezone.
final nString = pd.NSString(lib, "$formattedDate $tz");
// Convert the NSString to NSDate.
return formatter.dateFromString_(nString)!;
}
@override
Future<List<Steps>> getSteps() async {
if (!pd.CMPedometer.isStepCountingAvailable(lib)) {
debugPrint("Step counting is not available.");
return [];
}
final handlers = [];
final futures = <Future<Steps?>>[];
final now = DateTime.now();
for (var h = 0; h <= now.hour; h++) {
final start = dateConverter(DateTime(now.year, now.month, now.day, h));
final end = dateConverter(DateTime(now.year, now.month, now.day, h + 1));
final completer = Completer<Steps?>();
futures.add(completer.future);
final handler = helpLib.wrapCallback(
pd.ObjCBlock_ffiVoid_CMPedometerData_NSError.listener(lib,
(pd.CMPedometerData? result, pd.NSError? error) {
if (result != null) {
final stepCount = result.numberOfSteps.intValue;
final startHour =
hourFormatter.stringFromDate_(result.startDate).toString();
completer.complete(Steps(startHour, stepCount));
} else {
debugPrint("Query error: ${error?.localizedDescription}");
completer.complete(null);
}
}));
handlers.add(handler);
client.queryPedometerDataFromDate_toDate_withHandler_(
start, end, handler);
}
return (await futures.wait).nonNulls.toList();
}
}
class _AndroidStepsRepo implements StepsRepo {
late final hc.Activity activity;
late final hc.Context applicationContext;
late final hc.HealthConnectClient client;
_AndroidStepsRepo() {
activity = hc.Activity.fromReference(jni.Jni.getCurrentActivity());
applicationContext =
hc.Context.fromReference(jni.Jni.getCachedApplicationContext());
client = hc.HealthConnectClient.getOrCreate1(applicationContext);
}
@override
Future<List<Steps>> getSteps() async {
final futures = <Future<hc.AggregationResult>>[];
final now = DateTime.now();
for (var h = 0; h <= now.hour; h++) {
final start =
DateTime(now.year, now.month, now.day, h).millisecondsSinceEpoch;
final end =
DateTime(now.year, now.month, now.day, h + 1).millisecondsSinceEpoch;
final request = hc.AggregateRequest(
{hc.StepsRecord.COUNT_TOTAL}
.toJSet(hc.AggregateMetric.type(jni.JLong.type)),
hc.TimeRangeFilter.between(
hc.Instant.ofEpochMilli(start),
hc.Instant.ofEpochMilli(end),
),
jni.JSet.hash(jni.JObject.type),
);
futures.add(client.aggregate(request));
}
final data = await Future.wait(futures);
return data.asMap().entries.map((entry) {
final stepsLong = entry.value.get0(hc.StepsRecord.COUNT_TOTAL);
final steps = stepsLong.isNull ? 0 : stepsLong.intValue();
return Steps(entry.key.toString().padLeft(2, '0'), steps);
}).toList();
}
}