function TaskTracker(log) {
  var self = this;
  var taskCounts = {};
  var taskCallbacks = [];

  var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$';
  var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$';

  /**
   * Execute the specified function and decrement the appropriate `taskCounts` counter.
   * If the counter reaches 0, all corresponding `taskCallbacks` are executed.
   *
   * @param {Function} fn - The function to execute.
   * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed.
   */
  self.completeTask = completeTask;

  /**
   * Increase the task count for the specified task type (or the default task type if non is
   * specified).
   *
   * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased.
   */
  self.incTaskCount = incTaskCount;

  /**
   * Execute the specified callback when all pending tasks have been completed.
   *
   * If there are no pending tasks, the callback is executed immediately. You can optionally limit
   * the tasks that will be waited for to a specific type, by passing a `taskType`.
   *
   * @param {function} callback - The function to call when there are no pending tasks.
   * @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for.
   */
  self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks;

  function completeTask(fn, taskType) {
    taskType = taskType || DEFAULT_TASK_TYPE;

    try {
      fn();
    } finally {
      decTaskCount(taskType);

      var countForType = taskCounts[taskType];
      var countForAll = taskCounts[ALL_TASKS_TYPE];

      // If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks.
      if (!countForAll || !countForType) {
        var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType;
        var nextCb;

        while ((nextCb = getNextCallback(taskType))) {
          try {
            nextCb();
          } catch (e) {
            log.error(e);
          }
        }
      }
    }
  }

  function decTaskCount(taskType) {
    taskType = taskType || DEFAULT_TASK_TYPE;
    if (taskCounts[taskType]) {
      taskCounts[taskType]--;
      taskCounts[ALL_TASKS_TYPE]--;
    }
  }

  function getLastCallback() {
    var cbInfo = taskCallbacks.pop();
    return cbInfo && cbInfo.cb;
  }

  function getLastCallbackForType(taskType) {
    for (var i = taskCallbacks.length - 1; i >= 0; --i) {
      var cbInfo = taskCallbacks[i];
      if (cbInfo.type === taskType) {
        taskCallbacks.splice(i, 1);
        return cbInfo.cb;
      }
    }
  }

  function incTaskCount(taskType) {
    taskType = taskType || DEFAULT_TASK_TYPE;
    taskCounts[taskType] = (taskCounts[taskType] || 0) + 1;
    taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1;
  }

  function notifyWhenNoPendingTasks(callback, taskType) {
    taskType = taskType || ALL_TASKS_TYPE;
    if (!taskCounts[taskType]) {
      callback();
    } else {
      taskCallbacks.push({type: taskType, cb: callback});
    }
  }
}
@ngdoc input
@name input[url]

@description
Text input with URL validation. Sets the `url` validation error key if the content is not a
valid URL.

<div class="alert alert-warning">
**Note:** `input[url]` uses a regex to validate urls that is derived from the regex
used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify
the built-in validators (see the {@link guide/forms Forms guide})
</div>

@param {string} ngModel Assignable AngularJS expression to data-bind to.
@param {string=} name Property name of the form under which the control is published.
@param {string=} required Sets `required` validation error key if the value is not entered.
@param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
   the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
   `required` when you want to data-bind to the `required` attribute.
@param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
   minlength.
@param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
   maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
   any length.
@param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
   that contains the regular expression body that will be converted to a regular expression
   as in the ngPattern directive.
@param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
   does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
   If the expression evaluates to a RegExp object, then this is used directly.
   If the expression evaluates to a string, then it will be converted to a RegExp
   after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
   `new RegExp('^abc$')`.<br />
   **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
   start at the index of the last search's match, thus not taking the whole input value into
   account.
@param {string=} ngChange AngularJS expression to be executed when input changes due to user
   interaction with the input element.

@example
      <example name="url-input-directive" module="urlExample">
        <file name="index.html">
         <script>
           angular.module('urlExample', [])
             .controller('ExampleController', ['$scope', function($scope) {
               $scope.url = {
                 text: 'http://google.com'
               };
             }]);
         </script>
         <form name="myForm" ng-controller="ExampleController">
           <label>URL:
             <input type="url" name="input" ng-model="url.text" required>
           <label>
           <div role="alert">
             <span class="error" ng-show="myForm.input.$error.required">
               Required!</span>
             <span class="error" ng-show="myForm.input.$error.url">
               Not valid url!</span>
           </div>
           <tt>text = {{url.text}}</tt><br/>
           <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
           <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
           <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
           <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
           <tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/>
          </form>
        </file>
        <file name="protractor.js" type="protractor">
          var text = element(by.binding('url.text'));
          var valid = element(by.binding('myForm.input.$valid'));
          var input = element(by.model('url.text'));

          it('should initialize to model', function() {
            expect(text.getText()).toContain('http://google.com');
            expect(valid.getText()).toContain('true');
          });

          it('should be invalid if empty', function() {
            input.clear();
            input.sendKeys('');

            expect(text.getText()).toEqual('text =');
            expect(valid.getText()).toContain('false');
          });

          it('should be invalid if not url', function() {
            input.clear();
            input.sendKeys('box');

            expect(valid.getText()).toContain('false');
          });
        </file>
      </example>