Beispiel #1
0
        /// <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);
            }
        }