/// <summary> /// Raises a <see cref="ScheduledCallback"/>. This involves initiating the <see cref="ScheduledCallback"/>, setting up cancellation timing /// for the callback's action based on the <see cref="ScheduledCallback.Timeout"/>, and scheduling the next invocation of the /// <see cref="ScheduledCallback"/> in the case of repeating <see cref="ScheduledCallback"/>s. See <see cref="CancelRaisedCallback(ScheduledCallback)"/> /// for how to cancel a <see cref="ScheduledCallback"/> after it has been raised. Unlike other methods called via the app's entry points /// (e.g., push notifications, alarms, etc.), this method does not take a <see cref="CancellationToken"/>. The reason for this is that /// raising <see cref="ScheduledCallback"/>s is done from several locations, and cancellation is only needed due to background considerations /// on iOS. So we've centralized background-sensitive cancellation into the iOS override of this method. /// </summary> /// <returns>Async task</returns> /// <param name="callback">Callback to raise.</param> /// <param name="invocationId">Identifier of invocation.</param> public virtual async Task RaiseCallbackAsync(ScheduledCallback callback, string invocationId) { try { if (callback == null) { throw new NullReferenceException("Attemped to raise null callback."); } if (SensusServiceHelper.Get() == null) { throw new NullReferenceException("Attempted to raise callback with null service helper."); } // the same callback must not be run multiple times concurrently, so drop the current callback if it's already running. multiple // callers might compete for the same callback, but only one will win the lock below and it will exclude all others until the // the callback has finished executing. furthermore, the callback must not run multiple times in sequence (e.g., if the callback // is raised by the local scheduling system and then later by a remote push notification). this is handled by tracking invocation // identifiers, which are only runnable once. string initiationError = null; lock (callback) { if (callback.State != ScheduledCallbackState.Scheduled) { initiationError += "Callback " + callback.Id + " is not scheduled. Current state: " + callback.State; } if (invocationId != callback.InvocationId) { initiationError += (initiationError == null ? "" : ". ") + "Invocation ID provided for callback " + callback.Id + " does not match the one on record."; } if (initiationError == null) { callback.State = ScheduledCallbackState.Running; } } if (initiationError == null) { try { if (callback.Canceller.IsCancellationRequested) { SensusServiceHelper.Get().Logger.Log("Callback " + callback.Id + " was cancelled before it was raised.", LoggingLevel.Normal, GetType()); } else { SensusServiceHelper.Get().Logger.Log("Raising callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); #if __ANDROID__ // on android we wouldn't have yet notified the user using the callback's message. on ios, the // message would have already been displayed to the user if the app was in the background. on // ios we do not display callback messages if the app is foregrounded. see the notification // delegate for how this is done. await SensusContext.Current.Notifier.IssueNotificationAsync("Sensus", callback.UserNotificationMessage, callback.Id, true, callback.Protocol, null, callback.NotificationUserResponseAction, callback.NotificationUserResponseMessage); #endif // if the callback specified a timeout, request cancellation at the specified time. if (callback.Timeout.HasValue) { callback.Canceller.CancelAfter(callback.Timeout.Value); } await callback.ActionAsync(callback.Canceller.Token); } } catch (Exception raiseException) { SensusException.Report("Callback " + callback.Id + " threw an exception: " + raiseException.Message, raiseException); } finally { // the cancellation token source for the current callback might have been canceled. if this is a repeating callback then we'll need a new // cancellation token source because they cannot be reset and we're going to use the same scheduled callback again for the next repeat. // if we enter the _idCallback lock before CancelRaisedCallback does, then the next raise will be cancelled. if CancelRaisedCallback enters the // _idCallback lock first, then the cancellation token source will be overwritten here and the cancel will not have any effect on the next // raise. the latter case is a reasonable outcome, since the purpose of CancelRaisedCallback is to terminate a callback that is currently in // progress, and the current callback is no longer in progress. if the desired outcome is complete discontinuation of the repeating callback // then UnscheduleRepeatingCallback should be used -- this method first cancels any raised callbacks and then removes the callback entirely. try { if (callback.RepeatDelay.HasValue) { callback.Canceller = new CancellationTokenSource(); } } catch (Exception ex) { SensusException.Report("Exception while assigning new callback canceller.", ex); } finally { callback.State = ScheduledCallbackState.Completed; // schedule callback again if it is still scheduled with a valid repeat delay if (ContainsCallback(callback) && callback.RepeatDelay.HasValue && callback.RepeatDelay.Value.Ticks > 0) { callback.NextExecution = DateTime.Now + callback.RepeatDelay.Value; callback.InvocationId = Guid.NewGuid().ToString(); // set the new invocation ID before resetting the state so that concurrent callers won't run (their invocation IDs won't match) BatchNextExecutionWithToleratedDelay(callback); // the state needs to be updated after batching is performed, so that other callbacks don't // attempt to batch with it, but before the invocations are requested, so that if the invocation // comes back immediately (e.g., being scheduled in the past) the callback is scheduled and // ready to run. callback.State = ScheduledCallbackState.Scheduled; await RequestLocalInvocationAsync(callback); #if __IOS__ await RequestRemoteInvocationAsync(callback); #endif } else { await UnscheduleCallbackAsync(callback); } } } } else { SensusServiceHelper.Get().Logger.Log("Initiation error for callback " + callback.Id + ": " + initiationError, LoggingLevel.Normal, GetType()); } } catch (Exception ex) { SensusException.Report("Exception raising callback: " + ex.Message, ex); } }