Sfoglia il codice sorgente

Use TimeoutService for most timeouts

Danilo Bargen 6 anni fa
parent
commit
9400684d86

+ 12 - 9
src/controllers/status.ts

@@ -19,6 +19,7 @@ import {StateService as UiStateService} from '@uirouter/angularjs';
 
 import {ControllerService} from '../services/controller';
 import {StateService} from '../services/state';
+import {TimeoutService} from '../services/timeout';
 import {WebClientService} from '../services/webclient';
 
 import GlobalConnectionState = threema.GlobalConnectionState;
@@ -52,17 +53,18 @@ export class StatusController {
     private $state: UiStateService;
 
     // Custom services
+    private controllerService: ControllerService;
     private stateService: StateService;
+    private timeoutService: TimeoutService;
     private webClientService: WebClientService;
-    private controllerService: ControllerService;
 
     public static $inject = [
-        '$scope', '$timeout', '$log', '$state', 'StateService',
-        'WebClientService', 'ControllerService',
+        '$scope', '$timeout', '$log', '$state',
+        'ControllerService', 'StateService', 'TimeoutService', 'WebClientService',
     ];
     constructor($scope, $timeout: ng.ITimeoutService, $log: ng.ILogService, $state: UiStateService,
-                stateService: StateService, webClientService: WebClientService,
-                controllerService: ControllerService) {
+                controllerService: ControllerService, stateService: StateService,
+                timeoutService: TimeoutService, webClientService: WebClientService) {
 
         // Angular services
         this.$timeout = $timeout;
@@ -70,9 +72,10 @@ export class StatusController {
         this.$state = $state;
 
         // Custom services
+        this.controllerService = controllerService;
         this.stateService = stateService;
+        this.timeoutService = timeoutService;
         this.webClientService = webClientService;
-        this.controllerService = controllerService;
 
         // Register event handlers
         this.stateService.evtGlobalConnectionStateChange.attach(
@@ -138,9 +141,9 @@ export class StatusController {
      * Show full status bar with a certain delay.
      */
     private scheduleStatusBar(): void {
-        this.expandStatusBarTimer = this.$timeout(() => {
+        this.expandStatusBarTimer = this.timeoutService.register(() => {
             this.expandStatusBar = true;
-        }, this.expandStatusBarTimeout);
+        }, this.expandStatusBarTimeout, true, 'expandStatusBar');
     }
 
     /**
@@ -149,7 +152,7 @@ export class StatusController {
     private collapseStatusBar(): void {
         this.expandStatusBar = false;
         if (this.expandStatusBarTimer !== null) {
-            this.$timeout.cancel(this.expandStatusBarTimer);
+            this.timeoutService.cancel(this.expandStatusBarTimer);
         }
     }
 

+ 12 - 7
src/directives/avatar.ts

@@ -17,17 +17,18 @@
 
 import {bufferToUrl, logAdapter} from '../helpers';
 import {isEchoContact, isGatewayContact} from '../receiver_helpers';
+import {TimeoutService} from '../services/timeout';
 import {WebClientService} from '../services/webclient';
 import {isContactReceiver} from '../typeguards';
 
 export default [
     '$rootScope',
-    '$timeout',
     '$log',
+    'TimeoutService',
     'WebClientService',
     function($rootScope: ng.IRootScopeService,
-             $timeout: ng.ITimeoutService,
              $log: ng.ILogService,
+             timeoutService: TimeoutService,
              webClientService: WebClientService) {
         return {
             restrict: 'E',
@@ -144,27 +145,31 @@ export default [
                             if (loadingPromise === null) {
                                 // Do not wait on high resolution avatar
                                 const loadingTimeout = this.highResolution ? 0 : 500;
-                                loadingPromise = $timeout(() => {
+                                loadingPromise = timeoutService.register(() => {
                                     // show loading only on high res images!
                                     webClientService.requestAvatar({
                                         type: this.receiver.type,
                                         id: this.receiver.id,
-                                    } as threema.Receiver, this.highResolution).then((avatar) => {
+                                    } as threema.Receiver, this.highResolution)
+                                    .then((avatar) => {
                                         $rootScope.$apply(() => {
                                             this.isLoading = false;
                                         });
-                                    }).catch((error) => {
+                                        loadingPromise = null;
+                                    })
+                                    .catch((error) => {
                                         // TODO: Handle this properly / show an error message
                                         $log.error(this.logTag, `Avatar request has been rejected: ${error}`);
                                         $rootScope.$apply(() => {
                                             this.isLoading = false;
                                         });
+                                        loadingPromise = null;
                                     });
-                                }, loadingTimeout);
+                                }, loadingTimeout, false, 'avatar');
                             }
                         } else if (loadingPromise !== null) {
                             // Cancel pending avatar loading
-                            $timeout.cancel(loadingPromise);
+                            timeoutService.cancel(loadingPromise);
                             loadingPromise = null;
                         }
                     };

+ 2 - 6
src/directives/avatar_editor.ts

@@ -23,12 +23,8 @@ import {bufferToUrl, logAdapter} from '../helpers';
  * Support uploading and resizing avatar
  */
 export default [
-    '$window',
-    '$timeout',
-    '$translate',
     '$log',
-    '$mdDialog',
-    function($window, $timeout: ng.ITimeoutService, $translate, $log: ng.ILogService, $mdDialog) {
+    function($log: ng.ILogService) {
         return {
             restrict: 'EA',
             scope: {
@@ -67,7 +63,7 @@ export default [
                                 clearTimeout(updateTimeout);
                             }
 
-                            updateTimeout = setTimeout(() => {
+                            updateTimeout = self.setTimeout(() => {
                                 croppieInstance.result({
                                     type: 'blob',
                                     // max allowed size on device

+ 6 - 1
src/directives/click_action.ts

@@ -25,7 +25,12 @@ export default [
     '$state',
     'UriService',
     'WebClientService',
-    function($timeout, $state: UiStateService, uriService: UriService, webClientService: WebClientService) {
+    function(
+        $timeout: ng.ITimeoutService,
+        $state: UiStateService,
+        uriService: UriService,
+        webClientService: WebClientService,
+    ) {
 
         const validateThreemaId = (id: string): boolean => {
             return id !== undefined && id !== null && /^[0-9A-Z]{8}/.test(id);

+ 7 - 5
src/directives/compose_area.ts

@@ -18,6 +18,7 @@
 import {isActionTrigger} from '../helpers';
 import {BrowserService} from '../services/browser';
 import {StringService} from '../services/string';
+import {TimeoutService} from '../services/timeout';
 
 /**
  * The compose area where messages are written.
@@ -25,7 +26,7 @@ import {StringService} from '../services/string';
 export default [
     'BrowserService',
     'StringService',
-    '$window',
+    'TimeoutService',
     '$timeout',
     '$translate',
     '$mdDialog',
@@ -34,7 +35,8 @@ export default [
     '$rootScope',
     function(browserService: BrowserService,
              stringService: StringService,
-             $window, $timeout: ng.ITimeoutService,
+             timeoutService: TimeoutService,
+             $timeout: ng.ITimeoutService,
              $translate: ng.translate.ITranslateService,
              $mdDialog: ng.material.IDialogService,
              $filter: ng.IFilterService,
@@ -130,7 +132,7 @@ export default [
                     // that we started typing earlier)
                     if (stopTypingTimer !== null) {
                         // Cancel timer
-                        $timeout.cancel(stopTypingTimer);
+                        timeoutService.cancel(stopTypingTimer);
                         stopTypingTimer = null;
 
                         // Send stop typing message
@@ -144,11 +146,11 @@ export default [
                         scope.startTyping();
                     } else {
                         // Cancel timer, we'll re-create it
-                        $timeout.cancel(stopTypingTimer);
+                        timeoutService.cancel(stopTypingTimer);
                     }
 
                     // Define a timeout to send the stopTyping event
-                    stopTypingTimer = $timeout(stopTyping, 10000);
+                    stopTypingTimer = timeoutService.register(stopTyping, 10000, true, 'stopTyping');
                 }
 
                 // Process a DOM node recursively and extract text from compose area.

+ 1 - 5
src/directives/drag_file.ts

@@ -19,12 +19,8 @@
  * Allow to drag and drop elements, set class to parent object
  */
 export default [
-    '$window',
-    '$timeout',
-    '$translate',
-    '$filter',
     '$log',
-    function($window, $timeout: ng.ITimeoutService, $translate, $filter: ng.IFilterService, $log: ng.ILogService) {
+    function($log: ng.ILogService) {
         return {
             restrict: 'EA',
             scope: {

+ 7 - 4
src/directives/message_media.ts

@@ -20,6 +20,7 @@ import {saveAs} from 'file-saver';
 import {bufferToUrl, hasValue, logAdapter} from '../helpers';
 import {MediaboxService} from '../services/mediabox';
 import {MessageService} from '../services/message';
+import {TimeoutService} from '../services/timeout';
 import {WebClientService} from '../services/webclient';
 
 function showAudioDialog(
@@ -64,6 +65,7 @@ export default [
     'WebClientService',
     'MediaboxService',
     'MessageService',
+    'TimeoutService',
     '$rootScope',
     '$mdDialog',
     '$timeout',
@@ -74,6 +76,7 @@ export default [
     function(webClientService: WebClientService,
              mediaboxService: MediaboxService,
              messageService: MessageService,
+             timeoutService: TimeoutService,
              $rootScope: ng.IRootScopeService,
              $mdDialog: ng.material.IDialogService,
              $timeout: ng.ITimeoutService,
@@ -141,7 +144,7 @@ export default [
                         };
                     }
 
-                    let loadingThumbnailTimeout = null;
+                    let loadingThumbnailTimeout: ng.IPromise<void> = null;
 
                     this.wasInView = false;
                     this.thumbnailInView = (inView: boolean) => {
@@ -153,7 +156,7 @@ export default [
                         this.wasInView = inView;
 
                         if (!inView) {
-                            $timeout.cancel(loadingThumbnailTimeout);
+                            timeoutService.cancel(loadingThumbnailTimeout);
                             this.thumbnailDownloading = false;
                             this.thumbnail = null;
                         } else {
@@ -171,7 +174,7 @@ export default [
                                     return;
                                 } else {
                                     this.thumbnailDownloading = true;
-                                    loadingThumbnailTimeout = $timeout(() => {
+                                    loadingThumbnailTimeout = timeoutService.register(() => {
                                         webClientService
                                             .requestThumbnail(this.receiver, this.message)
                                             .then((img) => $timeout(() => {
@@ -183,7 +186,7 @@ export default [
                                                 const message = `Thumbnail request has been rejected: ${error}`;
                                                 this.$log.error(this.logTag, message);
                                             });
-                                    }, 1000);
+                                    }, 1000, false, 'thumbnail');
                                 }
                             }
                         }

+ 9 - 8
src/partials/messenger.ts

@@ -36,6 +36,7 @@ import {NotificationService} from '../services/notification';
 import {ReceiverService} from '../services/receiver';
 import {SettingsService} from '../services/settings';
 import {StateService} from '../services/state';
+import {TimeoutService} from '../services/timeout';
 import {VersionService} from '../services/version';
 import {WebClientService} from '../services/webclient';
 import {isContactReceiver} from '../typeguards';
@@ -204,7 +205,6 @@ class ConversationController {
 
     // Angular services
     private $stateParams;
-    private $timeout: ng.ITimeoutService;
     private $state: UiStateService;
     private $log: ng.ILogService;
     private $scope: ng.IScope;
@@ -217,6 +217,7 @@ class ConversationController {
     private receiverService: ReceiverService;
     private stateService: StateService;
     private mimeService: MimeService;
+    private timeoutService: TimeoutService;
 
     // Third party services
     private $mdDialog: ng.material.IDialogService;
@@ -269,14 +270,13 @@ class ConversationController {
     };
 
     public static $inject = [
-        '$stateParams', '$timeout', '$log', '$scope', '$rootScope',
+        '$stateParams', '$log', '$scope', '$rootScope',
         '$mdDialog', '$mdToast', '$translate', '$filter',
         '$state', '$transitions',
         'WebClientService', 'StateService', 'ReceiverService', 'MimeService', 'VersionService',
-        'ControllerModelService',
+        'ControllerModelService', 'TimeoutService',
     ];
     constructor($stateParams: ConversationStateParams,
-                $timeout: ng.ITimeoutService,
                 $log: ng.ILogService,
                 $scope: ng.IScope,
                 $rootScope: ng.IRootScopeService,
@@ -291,14 +291,15 @@ class ConversationController {
                 receiverService: ReceiverService,
                 mimeService: MimeService,
                 versionService: VersionService,
-                controllerModelService: ControllerModelService) {
+                controllerModelService: ControllerModelService,
+                timeoutService: TimeoutService) {
         this.$stateParams = $stateParams;
-        this.$timeout = $timeout;
         this.$log = $log;
         this.webClientService = webClientService;
         this.receiverService = receiverService;
         this.stateService = stateService;
         this.mimeService = mimeService;
+        this.timeoutService = timeoutService;
 
         this.$state = $state;
         this.$scope = $scope;
@@ -839,10 +840,10 @@ class ConversationController {
             this.msgReadReportPending = true;
             const receiver = angular.copy(this.receiver);
             receiver.type = this.type;
-            this.$timeout(() => {
+            this.timeoutService.register(() => {
                 this.webClientService.requestRead(receiver, this.lastReadMsg);
                 this.msgReadReportPending = false;
-            }, 300);
+            }, 300, false, 'requestRead');
         }
     }
 

+ 20 - 11
src/partials/welcome.ts

@@ -31,6 +31,7 @@ import {TrustedKeyStoreService} from '../services/keystore';
 import {PushService} from '../services/push';
 import {SettingsService} from '../services/settings';
 import {StateService} from '../services/state';
+import {TimeoutService} from '../services/timeout';
 import {VersionService} from '../services/version';
 import {WebClientService} from '../services/webclient';
 
@@ -61,8 +62,6 @@ class WelcomeController {
 
     // Angular services
     private $scope: ng.IScope;
-    private $timeout: ng.ITimeoutService;
-    private $interval: ng.IIntervalService;
     private $log: ng.ILogService;
     private $window: ng.IWindowService;
     private $state: UiStateService;
@@ -77,6 +76,7 @@ class WelcomeController {
     private pushService: PushService;
     private stateService: StateService;
     private settingsService: SettingsService;
+    private timeoutService: TimeoutService;
     private config: threema.Config;
 
     // Other
@@ -90,13 +90,12 @@ class WelcomeController {
     private browserWarningShown: boolean = false;
 
     public static $inject = [
-        '$scope', '$state', '$timeout', '$interval', '$log', '$window', '$mdDialog', '$translate',
+        '$scope', '$state', '$log', '$window', '$mdDialog', '$translate',
         'WebClientService', 'TrustedKeyStore', 'StateService', 'PushService', 'BrowserService',
-        'VersionService', 'SettingsService', 'ControllerService',
+        'VersionService', 'SettingsService', 'TimeoutService', 'ControllerService',
         'BROWSER_MIN_VERSIONS', 'CONFIG',
     ];
     constructor($scope: ng.IScope, $state: UiStateService,
-                $timeout: ng.ITimeoutService, $interval: ng.IIntervalService,
                 $log: ng.ILogService, $window: ng.IWindowService, $mdDialog: ng.material.IDialogService,
                 $translate: ng.translate.ITranslateService,
                 webClientService: WebClientService, trustedKeyStore: TrustedKeyStoreService,
@@ -104,6 +103,7 @@ class WelcomeController {
                 browserService: BrowserService,
                 versionService: VersionService,
                 settingsService: SettingsService,
+                timeoutService: TimeoutService,
                 controllerService: ControllerService,
                 minVersions: threema.BrowserMinVersions,
                 config: threema.Config) {
@@ -111,8 +111,6 @@ class WelcomeController {
         // Angular services
         this.$scope = $scope;
         this.$state = $state;
-        this.$timeout = $timeout;
-        this.$interval = $interval;
         this.$log = $log;
         this.$window = $window;
         this.$mdDialog = $mdDialog;
@@ -124,6 +122,7 @@ class WelcomeController {
         this.stateService = stateService;
         this.pushService = pushService;
         this.settingsService = settingsService;
+        this.timeoutService = timeoutService;
         this.config = config;
 
         // TODO: Allow to trigger below behaviour by using state parameters
@@ -363,10 +362,10 @@ class WelcomeController {
                     // is already active.
                     if (message.key === publicKeyHex && this.stateService.connectionBuildupState !== 'done') {
                         this.$log.error(this.logTag, 'Session already connected in another tab or window');
-                        this.$timeout(() => {
+                        this.timeoutService.register(() => {
                             this.stateService.updateConnectionBuildupState('already_connected');
                             this.stateService.state = GlobalConnectionState.Error;
-                        }, 500);
+                        }, 500, true, 'alreadyConnected');
                     }
                     break;
                 default:
@@ -572,14 +571,24 @@ class WelcomeController {
                 this.formLocked = false;
 
                 // Redirect to home
-                this.$timeout(() => this.$state.go('messenger.home'), WelcomeController.REDIRECT_DELAY);
+                this.timeoutService.register(
+                    () => this.$state.go('messenger.home'),
+                    WelcomeController.REDIRECT_DELAY,
+                    true,
+                    'redirectToHome',
+                );
             },
 
             // If an error occurs...
             (error) => {
                 this.$log.error(this.logTag, 'Error state:', error);
                 // TODO: should probably show an error message instead
-                this.$timeout(() => this.$state.reload(), WelcomeController.REDIRECT_DELAY);
+                this.timeoutService.register(
+                    () => this.$state.reload(),
+                    WelcomeController.REDIRECT_DELAY,
+                    true,
+                    'reloadStateError',
+                );
             },
 
             // State updates

+ 2 - 0
src/services.ts

@@ -32,6 +32,7 @@ import {ReceiverService} from './services/receiver';
 import {SettingsService} from './services/settings';
 import {StateService} from './services/state';
 import {StringService} from './services/string';
+import {TimeoutService} from './services/timeout';
 import {TitleService} from './services/title';
 import {UriService} from './services/uri';
 import {VersionService} from './services/version';
@@ -51,6 +52,7 @@ angular.module('3ema.services', [])
 .service('QrCodeService', QrCodeService)
 .service('ReceiverService', ReceiverService)
 .service('StateService', StateService)
+.service('TimeoutService', TimeoutService)
 .service('TitleService', TitleService)
 .service('TrustedKeyStore', TrustedKeyStoreService)
 .service('WebClientService', WebClientService)

+ 8 - 7
src/services/message.ts

@@ -17,6 +17,7 @@
 
 import {isContactReceiver} from '../typeguards';
 import {ReceiverService} from './receiver';
+import {TimeoutService} from './timeout';
 
 export class MessageAccess {
     public quote = false;
@@ -31,19 +32,19 @@ export class MessageService {
 
     // Angular services
     private $log: ng.ILogService;
-    private $timeout: ng.ITimeoutService;
 
     // Own services
     private receiverService: ReceiverService;
+    private timeoutService: TimeoutService;
 
     // Other
     private timeoutDelaySeconds = 30;
 
-    public static $inject = ['$log', '$timeout', 'ReceiverService'];
-    constructor($log: ng.ILogService, $timeout: ng.ITimeoutService, receiverService: ReceiverService) {
+    public static $inject = ['$log', 'ReceiverService', 'TimeoutService'];
+    constructor($log: ng.ILogService, receiverService: ReceiverService, timeoutService: TimeoutService) {
         this.$log = $log;
-        this.$timeout = $timeout;
         this.receiverService = receiverService;
+        this.timeoutService = timeoutService;
     }
 
     public getAccess(message: threema.Message, receiver: threema.Receiver): MessageAccess {
@@ -168,8 +169,8 @@ export class MessageService {
         }
 
         // Add delay for timeout checking
-        // TODO: This should be removed. It either works or it doesn't. There's nothing in between.
-        this.$timeout(() => {
+        // TODO: This should be removed once Android has reliable message delivery.
+        this.timeoutService.register(() => {
             // Set the state to timeout if it is still pending.
             // Note: If sending the message worked, by now the message object
             // will have been replaced by a new one and the state change would
@@ -177,7 +178,7 @@ export class MessageService {
             if (message.state === 'pending') {
                 message.state = 'timeout';
             }
-        }, this.timeoutDelaySeconds * 1000);
+        }, this.timeoutDelaySeconds * 1000, true, 'messageTimeout');
 
         return message;
     }

+ 36 - 8
src/services/timeout.ts

@@ -18,6 +18,9 @@
 export class TimeoutService {
     private logTag: string = '[TimeoutService]';
 
+    // Config
+    private config: threema.Config;
+
     // Angular services
     private $log: ng.ILogService;
     private $timeout: ng.ITimeoutService;
@@ -25,20 +28,40 @@ export class TimeoutService {
     // List of registered timeouts
     private timeouts: Set<ng.IPromise<any>> = new Set();
 
-    public static $inject = ['$log', '$timeout'];
-    constructor($log: ng.ILogService, $timeout: ng.ITimeoutService) {
-        // Angular services
+    public static $inject = ['CONFIG', '$log', '$timeout'];
+    constructor(config: threema.Config, $log: ng.ILogService, $timeout: ng.ITimeoutService) {
+        this.config = config;
         this.$log = $log;
         this.$timeout = $timeout;
     }
 
+    /**
+     * Log a message on debug log level, but only if the `DEBUG` flag is enabled.
+     */
+    private logDebug(msg: string): void {
+        if (this.config.DEBUG) {
+            this.$log.debug(this.logTag, msg);
+        }
+    }
+
     /**
      * Register a timeout.
      */
-    public register<T>(fn: (...args: any[]) => T, delay: number, invokeApply: boolean): ng.IPromise<T> {
-        this.$log.debug(this.logTag, 'Registering timeout');
+    public register<T>(fn: (...args: any[]) => T, delay: number, invokeApply: boolean, name?: string): ng.IPromise<T> {
+        this.logDebug('Registering timeout' + (name === undefined ? '' : ` (${name})`));
         const timeout = this.$timeout(fn, delay, invokeApply);
-        timeout.finally(() => this.timeouts.delete(timeout));
+        timeout
+            .then(() => this.timeouts.delete(timeout))
+            .catch((reason) => {
+                if (reason !== 'canceled') { // We can safely ignore cancellation
+                    this.$log.error(this.logTag, 'Registered timeout promise rejected:', reason);
+                }
+            });
+
+        // Stick name onto promise for debugging purposes
+        // tslint:disable-next-line: no-string-literal
+        timeout['_timeout_name'] = name;
+
         this.timeouts.add(timeout);
         return timeout;
     }
@@ -49,8 +72,13 @@ export class TimeoutService {
      * Return true if the task hasn't executed yet and was successfully canceled.
      */
     public cancel<T>(timeout: ng.IPromise<T>): boolean {
-        this.$log.debug(this.logTag, 'Cancelling timeout');
+        // Retrieve name from promise for debugging purposes
+        // tslint:disable-next-line: no-string-literal
+        const name = timeout['_timeout_name'];
+
+        this.logDebug('Cancelling timeout' + (name === undefined ? '' : ` (${name})`));
         const cancelled = this.$timeout.cancel(timeout);
+
         this.timeouts.delete(timeout);
         return cancelled;
     }
@@ -59,7 +87,7 @@ export class TimeoutService {
      * Cancel all pending timeouts.
      */
     public cancelAll() {
-        this.$log.debug(this.logTag, 'Cancelling all timeouts');
+        this.$log.debug(this.logTag, 'Cancelling ' + this.timeouts.size + ' timeouts');
         for (const timeout of this.timeouts) {
             this.$timeout.cancel(timeout);
         }

+ 20 - 7
src/services/webclient.ts

@@ -38,6 +38,7 @@ import {PushService} from './push';
 import {QrCodeService} from './qrcode';
 import {ReceiverService} from './receiver';
 import {StateService} from './state';
+import {TimeoutService} from './timeout';
 import {TitleService} from './title';
 import {VersionService} from './version';
 
@@ -172,6 +173,7 @@ export class WebClientService {
     private pushService: PushService;
     private qrCodeService: QrCodeService;
     private receiverService: ReceiverService;
+    private timeoutService: TimeoutService;
     private titleService: TitleService;
     private versionService: VersionService;
 
@@ -213,6 +215,9 @@ export class WebClientService {
     private pushToken: string = null;
     private pushTokenType: threema.PushTokenType = null;
 
+    // Timeouts
+    private batteryStatusTimeout: ng.IPromise<void> = null;
+
     // Other
     private config: threema.Config;
     private container: threema.Container.Factory;
@@ -222,7 +227,6 @@ export class WebClientService {
     private trustedKeyStore: TrustedKeyStoreService;
     public clientInfo: threema.ClientInfo = null;
     public version = null;
-    private batteryStatusTimeout: ng.IPromise<void> = null;
 
     private blobCache = new Map<string, threema.BlobInfo>();
     private loadingMessages = new Map<string, boolean>();
@@ -245,7 +249,7 @@ export class WebClientService {
         'Container', 'TrustedKeyStore',
         'StateService', 'NotificationService', 'MessageService', 'PushService', 'BrowserService',
         'TitleService', 'QrCodeService', 'MimeService', 'ReceiverService',
-        'VersionService', 'BatteryStatusService',
+        'VersionService', 'BatteryStatusService', 'TimeoutService',
         'CONFIG',
     ];
     constructor($log: ng.ILogService,
@@ -270,6 +274,7 @@ export class WebClientService {
                 receiverService: ReceiverService,
                 versionService: VersionService,
                 batteryStatusService: BatteryStatusService,
+                timeoutService: TimeoutService,
                 CONFIG: threema.Config) {
 
         // Angular services
@@ -292,6 +297,7 @@ export class WebClientService {
         this.pushService = pushService;
         this.qrCodeService = qrCodeService;
         this.receiverService = receiverService;
+        this.timeoutService = timeoutService;
         this.titleService = titleService;
         this.versionService = versionService;
 
@@ -396,11 +402,12 @@ export class WebClientService {
         this.$log.info(`Initializing (keyStore=${keyStore !== undefined ? 'yes' : 'no'}, peerTrustedKey=` +
             `${flags.peerTrustedKey !== undefined ? 'yes' : 'no'}, resume=${resumeSession})`);
 
-        // Reset fields, blob cache & pending requests in case the session
+        // Reset fields, blob cache, pending requests and pending timeouts in case the session
         // should explicitly not be resumed
         if (!resumeSession) {
             this.clearCache();
             this.wireMessageFutures.clear();
+            this.timeoutService.cancelAll();
         }
 
         // Only move the previous connection's instances if the previous
@@ -870,10 +877,11 @@ export class WebClientService {
             this.discardSession({ resetMessageSequenceNumber: false });
             this.$log.debug(this.logTag, 'Session discarded');
 
-            // Reset fields, blob cache & pending requests in case the session
+            // Reset fields, blob cache, pending requests and pending timeouts in case the session
             // cannot be resumed
             this.clearCache();
             this.wireMessageFutures.clear();
+            this.timeoutService.cancelAll();
 
             // Set required initialisation steps
             requiredInitializationSteps.push(
@@ -923,7 +931,7 @@ export class WebClientService {
         // Fetch current version
         // Delay it to prevent the dialog from being closed by the messenger constructor,
         // which closes all open dialogs.
-        this.$timeout(() => this.versionService.checkForUpdate(), 7000);
+        this.timeoutService.register(() => this.versionService.checkForUpdate(), 7000, true, 'checkForUpdate');
 
         // Notify state service about data loading
         this.stateService.updateConnectionBuildupState('loading');
@@ -1147,6 +1155,9 @@ export class WebClientService {
             // Remove all pending promises
             this.wireMessageFutures.clear();
 
+            // Cancel pending timeouts
+            this.timeoutService.cancelAll();
+
             // Reset the push service
             this.pushService.reset();
 
@@ -4021,15 +4032,17 @@ export class WebClientService {
         const isOk = stateChange.state === threema.GlobalConnectionState.Ok;
         const wasOk = stateChange.prevState === threema.GlobalConnectionState.Ok;
         if (!isOk && wasOk && this.batteryStatusService.dataAvailable) {
-            this.batteryStatusTimeout = this.$timeout(
+            this.batteryStatusTimeout = this.timeoutService.register(
                 () => {
                     this.batteryStatusService.clearStatus();
                     this.batteryStatusTimeout = null;
                 },
                 60000,
+                true,
+                'batteryStatusHide',
             );
         } else if (isOk && this.batteryStatusTimeout !== null) {
-            this.$timeout.cancel(this.batteryStatusTimeout);
+            this.timeoutService.cancel(this.batteryStatusTimeout);
             this.batteryStatusTimeout = null;
         }
     }

+ 8 - 1
tests/service/message.js

@@ -7,7 +7,14 @@ describe('MessageService', function() {
 
     beforeEach(function() {
 
-        // load threema services
+        // Inject constants
+        module(($provide) => {
+            $provide.constant('CONFIG', {
+                'DEBUG': true,
+            });
+        });
+
+        // Load threema services
         module('3ema.services');
 
         // Inject the MessageService