truemetrics_flutter_sdk 0.0.5
truemetrics_flutter_sdk: ^0.0.5 copied to clipboard
Flutter plugin for Truemetrics SDK
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:truemetrics_flutter_sdk/truemetrics_flutter_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
runApp(const TruemetricsApp());
}
class TruemetricsApp extends StatelessWidget {
const TruemetricsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Truemetrics API Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyApp(),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _truemetricsPlugin = TruemetricsFlutterSdk();
final _apiKeyController = TextEditingController(text: "");
final _metadataKeyController = TextEditingController();
final _metadataValueController = TextEditingController();
final _templateNameController = TextEditingController();
final _tagNameController = TextEditingController();
TruemetricsState _sdkState = TruemetricsState.uninitialized;
String? _deviceId;
UploadStatistics? _uploadStats;
List<SensorStatistics> _sensorStats = [];
@override
void initState() {
super.initState();
_setupListeners();
}
void _setupListeners() {
_truemetricsPlugin.setStatusListener(
onStateChange: (event) {
print('State changed: ${event.state}, deviceId: ${event.deviceId}');
setState(() {
_sdkState = event.state;
if (event.deviceId != null) {
_deviceId = event.deviceId;
}
});
},
onError: (errorCode, message) {
print('Error: $errorCode - $message');
_showError(errorCode, message);
},
onPermissionsRequired: (permissions) async {
print('Need permissions: $permissions');
// Handle all SDK permission requests
if (permissions.any((p) =>
p.contains('LOCATION') ||
p.contains('ACTIVITY_RECOGNITION'))) {
await _handleLocationPermission();
}
},
);
}
Future<void> _initializeSdk() async {
if (_apiKeyController.text.isEmpty) {
_showError('INPUT_ERROR', 'API key cannot be empty');
return;
}
final config = TruemetricsConfig(
config: {
'apiKey': _apiKeyController.text,
'delayAutoStartRecording': TruemetricsConfig.autoStartOnInit,
},
);
final isInit = await _truemetricsPlugin.isInitialized();
if (isInit == false || isInit == null) {
try {
await _truemetricsPlugin.initialize(config);
} catch (e) {
print('Failed to initialize SDK: $e');
_showError('INITIALIZATION_ERROR', e.toString());
}
} else {
setState(() {
_sdkState = TruemetricsState.initialized;
});
}
}
Future<void> _deinitializeSdk() async {
try {
await _truemetricsPlugin.deInitialize();
setState(() {
_sdkState = TruemetricsState.uninitialized;
_deviceId = null;
_uploadStats = null;
_sensorStats = [];
});
} catch (e) {
print('Failed to deinitialize SDK: $e');
_showError('DEINITIALIZATION_ERROR', e.toString());
}
}
Future<void> _logMetadata() async {
if (_metadataKeyController.text.isEmpty ||
_metadataValueController.text.isEmpty) {
_showError('METADATA_ERROR', 'Key and value cannot be empty');
return;
}
try {
await _truemetricsPlugin.logMetadata({
_metadataKeyController.text: _metadataValueController.text,
});
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Metadata logged successfully'),
backgroundColor: Colors.green,
),
);
_metadataKeyController.clear();
_metadataValueController.clear();
} catch (e) {
_showError('METADATA_ERROR', e.toString());
}
}
Future<void> _loadStatistics() async {
final upload = await _truemetricsPlugin.getUploadStatistics();
final sensor = await _truemetricsPlugin.getSensorStatistics();
setState(() {
_uploadStats = upload;
_sensorStats = sensor;
});
}
Future<void> _createTemplate() async {
if (_templateNameController.text.isEmpty ||
_metadataKeyController.text.isEmpty ||
_metadataValueController.text.isEmpty) {
_showError('INPUT_ERROR', 'Template name, key and value are required');
return;
}
try {
await _truemetricsPlugin.createMetadataTemplate(
_templateNameController.text,
{_metadataKeyController.text: _metadataValueController.text},
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Template "${_templateNameController.text}" created'),
backgroundColor: Colors.green,
),
);
} catch (e) {
_showError('TEMPLATE_ERROR', e.toString());
}
}
Future<void> _createFromTemplateAndLog() async {
if (_templateNameController.text.isEmpty || _tagNameController.text.isEmpty) {
_showError('INPUT_ERROR', 'Template name and tag are required');
return;
}
try {
final created = await _truemetricsPlugin.createMetadataFromTemplate(
_tagNameController.text,
_templateNameController.text,
);
if (!created) {
_showError('TEMPLATE_ERROR', 'Template not found');
return;
}
final logged = await _truemetricsPlugin.logMetadataByTag(_tagNameController.text);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(logged ? 'Tagged metadata logged' : 'Failed to log'),
backgroundColor: logged ? Colors.green : Colors.orange,
),
);
} catch (e) {
_showError('TEMPLATE_ERROR', e.toString());
}
}
Future<void> _showTemplateNames() async {
final names = await _truemetricsPlugin.getMetadataTemplateNames();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Templates: ${names.isEmpty ? "(none)" : names.join(", ")}'),
),
);
}
Future<void> _clearAllMetadata() async {
try {
await _truemetricsPlugin.clearAllMetadata();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('All metadata cleared'),
backgroundColor: Colors.green,
),
);
} catch (e) {
_showError('METADATA_ERROR', e.toString());
}
}
void _showError(String code, String? message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error [$code]: ${message ?? 'Unknown error'}'),
backgroundColor: Colors.red,
),
);
}
Future<void> _handleLocationPermission() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.locationWhenInUse,
Permission.activityRecognition,
].request();
if (statuses[Permission.locationWhenInUse]?.isGranted ?? false) {
final backgroundStatus = await Permission.locationAlways.request();
statuses[Permission.locationAlways] = backgroundStatus;
}
final deniedPermissions = statuses.entries
.where((entry) => entry.value.isDenied)
.map((entry) => entry.key)
.toList();
if (deniedPermissions.isNotEmpty) {
_showError('PERMISSION_ERROR',
'Some permissions were denied. SDK functionality may be limited.');
}
final permanentlyDenied = statuses.entries
.where((entry) => entry.value.isPermanentlyDenied)
.map((entry) => entry.key)
.toList();
if (permanentlyDenied.isNotEmpty && mounted) {
showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Permissions Required'),
content: const Text(
'Some permissions are required for the SDK to function properly. '
'Please enable them in the app settings.'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('Open Settings'),
onPressed: () {
Navigator.of(context).pop();
openAppSettings();
},
),
],
),
);
}
}
String _stateDisplayName(TruemetricsState state) {
switch (state) {
case TruemetricsState.recordingInProgress:
return 'Recording';
case TruemetricsState.readingsDatabaseFull:
return 'DB Full';
case TruemetricsState.trafficLimitReached:
return 'Traffic Limit';
default:
return 'Not Recording';
}
}
Color _stateColor(TruemetricsState state) {
switch (state) {
case TruemetricsState.recordingInProgress:
return Colors.green;
case TruemetricsState.readingsDatabaseFull:
return Colors.orange;
case TruemetricsState.trafficLimitReached:
return Colors.orange;
default:
return Colors.red;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Truemetrics API Demo'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_sdkState == TruemetricsState.uninitialized) ...[
TextField(
controller: _apiKeyController,
decoration: const InputDecoration(
labelText: 'API Key',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeSdk,
child: const Text('Initialize SDK'),
),
] else ...[
if (_deviceId != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Device ID:',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Expanded(
child: Text(
_deviceId!,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 16),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Recording Status:',
style: TextStyle(fontSize: 16),
),
Text(
_stateDisplayName(_sdkState),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _stateColor(_sdkState),
),
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
if (_sdkState == TruemetricsState.recordingInProgress) {
await _truemetricsPlugin.stopRecording();
} else {
await _truemetricsPlugin.startRecording();
}
},
style: ElevatedButton.styleFrom(
backgroundColor:
_sdkState == TruemetricsState.recordingInProgress
? Colors.red
: Colors.green,
foregroundColor: Colors.white,
),
child: Text(
_sdkState == TruemetricsState.recordingInProgress
? 'Stop Recording'
: 'Start Recording',
style: const TextStyle(
color: Colors.white,
),
),
),
// --- Statistics Section ---
const SizedBox(height: 32),
const Text(
'Statistics',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _loadStatistics,
child: const Text('Load Statistics'),
),
if (_uploadStats != null) ...[
const SizedBox(height: 8),
Text('Successful uploads: ${_uploadStats!.successfulUploadsCount}'),
Text('Last upload: ${_uploadStats!.lastSuccessfulUploadTimestamp != null ? DateTime.fromMillisecondsSinceEpoch(_uploadStats!.lastSuccessfulUploadTimestamp!).toString() : "N/A"}'),
],
if (_sensorStats.isNotEmpty) ...[
const SizedBox(height: 8),
...(_sensorStats.map((s) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'${s.sensorName}: ${s.actualFrequencyHz.toStringAsFixed(1)}/${s.configuredFrequencyHz.toStringAsFixed(1)} Hz (${s.quality.name})',
style: const TextStyle(fontSize: 13),
),
))),
],
// --- Metadata Section ---
const SizedBox(height: 32),
const Text(
'Log Metadata',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _metadataKeyController,
decoration: const InputDecoration(
labelText: 'Key',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _metadataValueController,
decoration: const InputDecoration(
labelText: 'Value',
border: OutlineInputBorder(),
),
),
),
],
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _logMetadata,
child: const Text('Log Metadata'),
),
// --- Metadata Templates Section ---
const SizedBox(height: 32),
const Text(
'Metadata Templates',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
TextField(
controller: _templateNameController,
decoration: const InputDecoration(
labelText: 'Template Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _tagNameController,
decoration: const InputDecoration(
labelText: 'Tag Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: _createTemplate,
child: const Text('Create Template'),
),
ElevatedButton(
onPressed: _createFromTemplateAndLog,
child: const Text('Create & Log Tag'),
),
ElevatedButton(
onPressed: _showTemplateNames,
child: const Text('List Templates'),
),
ElevatedButton(
onPressed: _clearAllMetadata,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
child: const Text('Clear All'),
),
],
),
// --- Deinitialize ---
const SizedBox(height: 48),
ElevatedButton(
onPressed: _deinitializeSdk,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Deinitialize SDK'),
),
],
],
),
),
),
);
}
@override
void dispose() {
_apiKeyController.dispose();
_metadataKeyController.dispose();
_metadataValueController.dispose();
_templateNameController.dispose();
_tagNameController.dispose();
_truemetricsPlugin.deInitialize();
super.dispose();
}
}