Utilize the Event Timeline API
Accessing some of the API classes that are mentioned on this page requires additional Sentiance SDK dependencies. See this page for more information.
On this page, you can find examples of how to query the Sentiance SDK for historic timeline events, and how to register to receive real time timeline updates in your app, as new events are detected or existing ones are updated.
Query for Historic Events
let from = Date.distantPast
let to = Date.distantFuture
Sentiance.shared.getTimelineEvents(from: from, to: to).forEach { event in
print("Event ID: \(event.eventId)")
print("Started on: \(event.startDate)")
print("Ended on: \(event.endDate)")
if event.type == .inTransport {
let transport = event as! SENTTransportEvent
print("Type: transport")
print("Mode: \(transport.transportMode)")
if let distance = transport.distanceInMeters {
print("Distance: \(distance)")
}
print("Waypoints: \(transport.waypoints)")
}
else if event.type == .stationary {
let stationary = event as! SENTStationaryEvent
print("Type: stationary")
print("Location: \(stationary.location)")
print("Venue: \(stationary.venue)")
}
else if event.type == .offTheGrid {
print("Type: off-the-grid")
}
else {
print("Type: unknown")
}
}
val from = Date(0)
val to = Date(Long.MAX_VALUE)
EventTimelineApi.getInstance(context).getTimelineEvents(from, to).forEach { event ->
print("Event ID: ${event.id}")
print("Started on: ${event.startTime}")
print("Ended on: ${event.endTime}")
when (event.eventType) {
EventType.IN_TRANSPORT -> {
val transport = event as TransportEvent!
print("Type: transport")
print("Mode: ${transport.transportMode}")
if (transport.distanceInMeters != null) {
print("Distance: ${transport.distanceInMeters}")
}
print("Waypoints: ${transport.waypoints}")
}
EventType.STATIONARY -> {
val stationary = event as StationaryEvent!
print("Type: stationary")
print("Location: ${stationary.location}")
print("Venue: ${stationary.venue}")
}
EventType.OFF_THE_GRID -> {
print("Type: off-the-grid")
}
else -> {
print("Type: unknown")
}
}
}
import EventTimelineApi from '@sentiance-react-native/event-timeline';
const from = new Date(0); // 01/01/1970
const to = new Date(2 ** 31 * 1000);
const events = await EventTimelineApi.getTimelineEvents(from.getTime(), to.getTime());
for (const event of events) {
console.log(`Event ID: ${event.id}`);
console.log(`Started on: ${event.startTime}`);
console.log(`Ended on: ${event.endTime}`);
switch (event.type) {
case 'IN_TRANSPORT':
const transport = event;
console.log('Type: transport');
console.log(`Mode: ${transport.transportMode}`);
if (transport.distance) {
console.log(`Distance: ${transport.distance}`);
}
console.log(`Waypoints: ${JSON.stringify(transport.waypoints)}`);
break;
case 'STATIONARY':
const stationary = event;
console.log('Type: stationary');
console.log(`Location: ${JSON.stringify(stationary.location)}`);
console.log(`Venue: ${JSON.stringify(stationary.venue)}`);
break;
case 'OFF_THE_GRID':
console.log('Type: off-the-grid');
break;
default:
console.log('Type: unknown');
}
}
app.dart
import 'package:sentiance_event_timeline/sentiance_event_timeline.dart';
final eventTimeline = SentianceEventTimeline();
/// A function that processes all events that occurred since the dawn of times.
void processTimelineEvents() async {
const startEpochTimeMs = 0;
final endEpochTimeMs = DateTime.now().millisecondsSinceEpoch;
final timelineEvents = await eventTimeline.getTimelineEvents(startEpochTimeMs, endEpochTimeMs);
for (final event in timelineEvents) {
if (event is StationaryEvent) {
print("Type: stationary");
print("Location: ${event.location}");
print("Venue: ${event.venue}");
} else if (event is TransportEvent) {
print("Type: transport");
print("Mode: ${event.transportMode}");
print("Distance: ${event.distance}");
for (final waypoint in event.waypoints) {
print(waypoint.toString());
}
} else if (event is OffTheGridEvent) {
print("Type: off-the-grid");
} else if (event is UnknownEvent) {
print("Type: unknown");
}
}
}
Subscribe for Event Timeline Updates
public class EventTimelineUpdateReceiver: EventTimelineDelegate {
private let LAST_UPDATE_KEY = "SentianceTimelineEventLastUpdateTime"
private let timelineStore: TimelineStore
private let queue: OperationQueue
init() {
timelineStore = TimelineStore() // your own store implementation
queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
}
public func listenForUpdates() {
// Save a local copy of the most recent event update time, before
// setting the delegate below. This way, we make sure that this
// value does not change by the delegate method, by the time we query
// for updates.
let mostRecentEventUpdateTime = self.mostRecentEventUpdateTime
// Set the delegate to start listening for timeline updates.
Sentiance.shared.eventTimelineDelegate = self
// Finally, let's make sure we haven't missed any events since the
// last time our app ran (e.g. due to unexpected app termination).
let afterDate = Date(timeIntervalSince1970: mostRecentEventUpdateTime)
Sentiance.shared.getTimelineUpdates(after: afterDate)
.forEach { [weak self] timelineEvent in
self?.updateOrInsert(event: timelineEvent,
updateMostRecentEventUpdateTime: false)
}
}
public func onEventTimelineUpdate(event: SENTTimelineEvent) {
// Note: this delegate method invocation happens on the
// main/UI thread.
updateOrInsert(event: event, updateMostRecentEventUpdateTime: true)
}
private func updateOrInsert(event: SENTTimelineEvent,
updateMostRecentEventUpdateTime: Bool) {
// The non-concurrent queue makes sure we don't process incoming
// updates via the delegate method and the result of calling
// getTimelineUpdates() at the same time. Plus, it makes sure
// we do the processing off of the main thread.
queue.addOperation { [weak self] in
guard let self = self else { return }
if let existingEvent = timelineStore.get(id: event.eventId) {
// Make sure the event we're about to update isn't older
// than the one we already have.
if (existingEvent.lastUpdateDate.timeIntervalSince1970 <
event.lastUpdateDate.timeIntervalSince1970) {
timelineStore.update(event: event)
}
} else {
timelineStore.insert(event: event)
}
if updateMostRecentEventUpdateTime {
// Note: events arriving via the onEventTimelineUpdate
// delegate method have a monotonically increasing
// lastUpdateDate.
self.mostRecentEventUpdateTime
= event.lastUpdateDate.timeIntervalSince1970
}
}
}
// We keep track of the most recent lastUpdateDate of the received
// events, so that on the next run, we can use it to process missed
// events (see listenForUpdates()).
private var mostRecentEventUpdateTime : TimeInterval {
get {
// We use UserDefaults. But if your app uses the iOS
// DataProtection capability, this won't work for you
// when the device is locked. Use something that will
// be accessible in this case, such as the keychain or
// a file with a specific protection option.
let defaults = UserDefaults.standard
var updateTime = defaults.double(forKey: LAST_UPDATE_KEY)
if updateTime == 0 {
updateTime = Date().timeIntervalSince1970
self.mostRecentEventUpdateTime = updateTime
}
return updateTime
}
set {
let defaults = UserDefaults.standard
defaults.set(newValue, forKey: LAST_UPDATE_KEY)
}
}
}
public class EventTimelineUpdateReceiver(val context: Context) {
private val timelineStore: TimelineStore
private val executor: ExecutorService
init {
timelineStore = TimelineStore() // your own store implementation
executor = Executors.newSingleThreadExecutor()
}
fun listenForUpdates() {
val api = EventTimelineApi.getInstance(context)
// Save a local copy of the most recent event time, before setting
// the listener below. This way, we make sure that this value does not
// change by the listener callback method, by the time we query for
// updates.
val mostRecentEventUpdateTime = getMostRecentEventUpdateTime()
// Set the listener to start listening for timeline updates.
api.setTimelineUpdateListener { event ->
// Note: this invocation happens on the main/UI thread.
updateOrInsert(event, updateMostRecentEventUpdateTime = true)
}
// Finally, let's make sure we haven't missed any events since the
// last time our app ran (e.g. due to unexpected app termination).
api.getTimelineUpdates(Date(mostRecentEventUpdateTime)).forEach { event ->
updateOrInsert(event, updateMostRecentEventUpdateTime = false)
}
}
private fun updateOrInsert(event: Event, updateMostRecentEventUpdateTime: Boolean) {
// The single thread executor makes sure we don't process incoming
// updates via the listener callback method and the result of calling
// getTimelineUpdates() at the same time. Plus, it makes sure we do
// the processing off of the main thread.
executor.submit {
val existingEvent = timelineStore.get(event.id)
if (existingEvent != null) {
// Make sure the event we're about to update isn't older
// than the one we already have.
if (existingEvent.lastUpdateTime < event.lastUpdateTime) {
timelineStore.update(event)
}
} else {
timelineStore.insert(event)
}
if (updateMostRecentEventUpdateTime) {
// Note: events arriving listener callback method have
// monotonically increasing lastUpdateTime.
setMostRecentEventUpdateTime(event.lastUpdateTime.epochTime)
}
}
}
// We keep track of the most recent lastUpdateTime of the received
// events, so that on the next run, we can use it to process missed
// events (see listenForUpdates()).
private fun getMostRecentEventUpdateTime(): Long {
val prefs = context.getSharedPreferences(null, Context.MODE_PRIVATE)
var updateTime = prefs.getLong(EventTimelineUpdateReceiver.PREFS_KEY_LAST_UPDATE_TIME, -1)
if (updateTime < 0) {
// Store the current time, so that in the future, we start from here.
updateTime = System.currentTimeMillis()
setMostRecentEventUpdateTime(updateTime)
}
return updateTime
}
private fun setMostRecentEventUpdateTime(timestamp: Long) {
val prefs = context.getSharedPreferences(null, Context.MODE_PRIVATE)
prefs.edit().putLong(EventTimelineUpdateReceiver.PREFS_KEY_LAST_UPDATE_TIME, timestamp).apply()
}
companion object {
const val PREFS_KEY_LAST_UPDATE_TIME = "SentianceTimelineEventLastUpdateTime"
}
}
// We're using the DefaultPreference library to store key/value pairs (using
// SharedPreferences on Android, and UserDefaults on iOS). Feel free to use
// a library of your own choosing.
// Note however that if your app uses the iOS DataProtection capability,
// accessing UserDefaults won't work when the device is locked. In this
// case, use something that will be accessible (such as the keychain or
// a file with a specific protection option).
import DefaultPreference from 'react-native-default-preference';
import EventTimelineApi from '@sentiance-react-native/event-timeline';
const KEY_LAST_UPDATE_TIME = 'SentianceTimelineEventLastUpdateTime';
const timelineStore = getTimelineStore(); // your own store implementation
listenForUpdates(); // Start listening to event updates
async function listenForUpdates() {
// Save a local copy of the most recent event update time, before setting
// the listener below. This way, we make sure that this value does not
// change by the listener callback method, by the time we query for updates.
const mostRecentEventUpdateTime = await getMostRecentEventUpdateTime();
// Set the listener to start listening for timeline updates.
const subscription = await EventTimelineApi.addTimelineUpdateListener(
async event => await updateOrInsert(event, true));
// Finally, let's make sure we haven't missed any events since the
// last time our app ran (e.g. due to unexpected app termination).
const events = await EventTimelineApi.getTimelineUpdates(mostRecentEventUpdateTime);
for (let event in events) {
await updateOrInsert(event, false);
}
}
async function updateOrInsert(event, updateMostRecentEventUpdateTime) {
// Make sure to handle concurrent access appropriately here
const existingEvent = await timelineStore.get(event.id);
if (existingEvent) {
// Make sure the event we're about to update isn't older
// than the one we already have.
if (existingEvent.lastUpdateTimeEpoch < event.lastUpdateTimeEpoch) {
await timelineStore.update(event);
}
} else {
await timelineStore.insert(event);
}
if (updateMostRecentEventUpdateTime) {
// Note: events arriving through the listener callback method have
// a monotonically increasing lastUpdateTimeEpoch.
await setMostRecentEventUpdateTime(event.lastUpdateTimeEpoch)
}
}
async function getMostRecentEventUpdateTime() {
let updateTimeEpoch = await DefaultPreference.get(KEY_LAST_UPDATE_TIME);
if (updateTimeEpoch < 0) {
// Store the current time, so that in the future, we start from here.
updateTimeEpoch = Date.now();
await setMostRecentEventUpdateTime(updateTimeEpoch);
}
return updateTimeEpoch;
}
async function setMostRecentEventUpdateTime(timestamp) {
await DefaultPreference.set(KEY_LAST_UPDATE_TIME, timestamp);
}
Create a background.dart file under your project's lib folder with the following code:
background.dart
// We're using the shared_preferences Flutter package to store key/value pairs
// (using SharedPreferences on Android, and UserDefaults on iOS).
// Feel free to use a library of your own choosing.
// Note however that if your app uses the iOS DataProtection capability,
// accessing UserDefaults won't work when the device is locked. In this
// case, use something that will be accessible (such as the keychain or
// a file with a specific protection option).
import 'package:flutter/material.dart';
import 'package:sentiance_event_timeline/sentiance_event_timeline.dart';
import 'package:shared_preferences/shared_preferences.dart';
const keyLastUpdateTime = 'SentianceTimelineEventLastUpdateTime';
final sentianceEventTimeline = SentianceEventTimeline();
const timelineStore = getTimelineStore(); // your own store implementation
@pragma('vm:entry-point')
void registerEventTimelineListener() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
// Save a local copy of the most recent event update time, before setting
// the listener below. This way, we make sure that this value does not
// change by the listener callback method, by the time we query for updates.
final mostRecentEventUpdateTime = await getMostRecentEventUpdateTime();
// Set the listener to start listening for timeline updates.
SentianceEventTimeline.registerEventTimelineUpdateListener((timelineEvent) {
await updateOrInsert(timelineEvent, true);
});
// Finally, let's make sure we haven't missed any events since the
// last time our app ran (e.g. due to unexpected app termination).
final events = await sentianceEventTimeline.getTimelineUpdates(mostRecentEventUpdateTime);
for (var event in events.nonNulls) {
await updateOrInsert(event, false);
}
}
Future<void> updateOrInsert(TimelineEvent event, bool updateMostRecentEventUpdateTime) async {
final existingEvent = await timelineStore.get(event.id); // your own store implementation
if (existingEvent) {
// Make sure the event we're about to update isn't older
// than the one we already have.
if (existingEvent.lastUpdateTimeMs < event.lastUpdateTimeMs) {
await timelineStore.update(event);
}
} else {
await timelineStore.insert(event);
}
if (updateMostRecentEventUpdateTime) {
// Note: events arriving through the listener callback method have
// a monotonically increasing lastUpdateTimeMs.
await setMostRecentEventUpdateTime(event.lastUpdateTimeMs);
}
}
Future<void> setMostRecentEventUpdateTime(int timestamp) async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await sharedPrefs.setInt(keyLastUpdateTime, timestamp);
}
Future<int> getMostRecentEventUpdateTime() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
var updateTimeEpoch = sharedPrefs.getInt(keyLastUpdateTime);
if (updateTimeEpoch == null) {
// Store the current time, so that in the future, we start from here.
updateTimeEpoch = DateTime.now().millisecondsSinceEpoch;
await setMostRecentEventUpdateTime(updateTimeEpoch);
}
return updateTimeEpoch;
}
Add the following code, depending on your target platform.
For iOS, add the following to your app delegate class:
AppDelegate.swift
import Flutter
import sentiance_event_timeline
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Other code
SentianceEventTimelinePlugin.initializeListener(
withEntryPoint: "registerEventTimelineListener",
libraryURI: "package:your_app_package_name/background.dart"
)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
For Android, add this code to your custom application class:
MainApplication.kt
import android.app.Application
import com.sentiance.event_timeline_plugin.EventTimelinePlugin
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
// Other code
val dartLibrary = "package:your_app_package_name/background.dart"
EventTimelinePlugin.initializeListener(this, dartLibrary, "registerEventTimelineListener")
}
}
Last updated