touchDevice method
Marks this device as active by updating lastActiveAt.
Updates the device document's lastActiveAt timestamp to indicate
recent activity. Used by backend to determine:
- Which devices are "active" for notification delivery
- Which devices to clean up as stale
Throttling
Default throttle is 60 minutes to avoid excessive writes. Configurable
via Remote Config (dreamic_device_touch_throttle_minutes).
When Called
- On app resume from background (throttled)
- After registerDevice (implicitly updates lastActiveAt)
Server Behavior
Uses upsert semantics: if the device doc doesn't exist, it will be created with minimal fields (deviceId, lastActiveAt, updatedAt).
Returns
Right(unit)on successLeft(RepositoryFailure)on failure
Implementation
@override
Future<Either<RepositoryFailure, Unit>> touchDevice() async {
logd('DeviceService: touchDevice called');
try {
final now = DateTime.now();
final throttleMinutes = AppConfigBase.deviceTouchThrottleMinutes;
// Apply throttle
if (_lastTouchAt != null &&
now.difference(_lastTouchAt!) < Duration(minutes: throttleMinutes)) {
logd('DeviceService: Touch throttled, last touch was ${now.difference(_lastTouchAt!).inMinutes} minutes ago');
// Try to flush any pending payload on this lifecycle event (if authenticated)
if (_isUserAuthenticated()) {
await _flushPendingPayload();
}
return const Right(unit);
}
final deviceId = await getDeviceId();
// Check authentication - if not authenticated, store pending and return
if (!_isUserAuthenticated()) {
logd('DeviceService: User not authenticated, storing pending touch');
await _updatePendingPayload(
deviceId: deviceId,
touch: true,
);
// Return success since we've stored it for later
return const Right(unit);
}
logd('DeviceService: Touching device $deviceId');
final result = await _deviceCallable.call({
'action': 'touch',
'deviceId': deviceId,
});
final data = Map<String, dynamic>.from(result.data as Map);
if (data['success'] != true) {
logw('DeviceService: Touch response indicated failure');
// Store touch in pending payload for retry
await _updatePendingPayload(
deviceId: deviceId,
touch: true,
);
return const Left(RepositoryFailure.unexpected);
}
_lastTouchAt = now;
// Try to flush any other pending data now that we have connectivity
await _flushPendingPayload();
logd('DeviceService: Device touched successfully');
return const Right(unit);
} on FirebaseFunctionsException catch (e) {
loge(e, 'DeviceService: Firebase Functions error during touch');
// Store touch in pending payload for retry on transient errors
if (_shouldStorePendingOnError(e)) {
final deviceId = await getDeviceId();
await _updatePendingPayload(
deviceId: deviceId,
touch: true,
);
}
return _mapFirebaseFunctionsException(e);
} catch (e) {
loge(e, 'DeviceService: Unexpected error during touch');
// Store touch in pending payload for retry
final deviceId = await getDeviceId();
await _updatePendingPayload(
deviceId: deviceId,
touch: true,
);
return const Left(RepositoryFailure.unexpected);
}
}