/**
* This file is part of Threema Web.
*
* Threema Web is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
* General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Threema Web. If not, see .
*/
import {StateService as UiStateService} from '@uirouter/angularjs';
import {ControllerService} from '../services/controller';
import {StateService} from '../services/state';
import {WebClientService} from '../services/webclient';
import GlobalConnectionState = threema.GlobalConnectionState;
/**
* This controller handles state changes globally.
*
* It also controls auto-reconnecting and the connection status indicator bar.
*
* Status updates should be done through the status service.
*/
export class StatusController {
private logTag: string = '[StatusController]';
// State variable
private state = GlobalConnectionState.Error;
// Expanded status bar
public expandStatusBar = false;
private expandStatusBarTimer: ng.IPromise | null = null;
private expandStatusBarTimeout = 3000;
// Reconnect
private reconnectTimeout: ng.IPromise;
// Angular services
private $timeout: ng.ITimeoutService;
private $log: ng.ILogService;
private $state: UiStateService;
// Custom services
private stateService: StateService;
private webClientService: WebClientService;
private controllerService: ControllerService;
public static $inject = ['$scope', '$timeout', '$log', '$state', 'StateService',
'WebClientService', 'ControllerService'];
constructor($scope, $timeout: ng.ITimeoutService, $log: ng.ILogService, $state: UiStateService,
stateService: StateService, webClientService: WebClientService,
controllerService: ControllerService) {
// Angular services
this.$timeout = $timeout;
this.$log = $log;
this.$state = $state;
// Custom services
this.stateService = stateService;
this.webClientService = webClientService;
this.controllerService = controllerService;
// Register event handlers
this.stateService.evtGlobalConnectionStateChange.attach(
(stateChange: threema.GlobalConnectionStateChange) => {
this.onStateChange(stateChange.state, stateChange.prevState);
},
);
}
/**
* Return the prefixed status.
*/
public get statusClass(): string {
return 'status-task-' + this.webClientService.chosenTask + ' status-' + this.state;
}
/**
* Handle state changes.
*/
private onStateChange(newValue: threema.GlobalConnectionState,
oldValue: threema.GlobalConnectionState): void {
this.$log.debug(this.logTag, 'State change:', oldValue, '->', newValue);
if (newValue === oldValue) {
return;
}
this.state = newValue;
const isWebrtc = this.webClientService.chosenTask === threema.ChosenTask.WebRTC;
const isRelayedData = this.webClientService.chosenTask === threema.ChosenTask.RelayedData;
switch (newValue) {
case 'ok':
this.collapseStatusBar();
break;
case 'warning':
if (oldValue === 'ok' && isWebrtc) {
this.scheduleStatusBar();
}
if (this.stateService.wasConnected) {
this.webClientService.clearIsTypingFlags();
}
if (this.stateService.wasConnected && isRelayedData) {
this.reconnectIos();
}
break;
case 'error':
if (this.stateService.wasConnected && isWebrtc) {
if (oldValue === 'ok') {
this.scheduleStatusBar();
}
this.reconnectAndroid();
}
break;
default:
this.$log.error(this.logTag, 'Invalid state change: From', oldValue, 'to', newValue);
}
}
/**
* Show full status bar with a certain delay.
*/
private scheduleStatusBar(): void {
this.expandStatusBarTimer = this.$timeout(() => {
this.expandStatusBar = true;
}, this.expandStatusBarTimeout);
}
/**
* Collapse the status bar if expanded.
*/
private collapseStatusBar(): void {
this.expandStatusBar = false;
if (this.expandStatusBarTimer !== null) {
this.$timeout.cancel(this.expandStatusBarTimer);
}
}
/**
* Attempt to reconnect an Android device after a connection loss.
*/
private reconnectAndroid(): void {
this.$log.warn(this.logTag, 'Connection lost (Android). Attempting to reconnect...');
// Get original keys
const originalKeyStore = this.webClientService.salty.keyStore;
const originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
// Timeout durations
const TIMEOUT1 = 20 * 1000; // Duration per step for first reconnect
const TIMEOUT2 = 20 * 1000; // Duration per step for second reconnect
// Reconnect state
let reconnectTry: 1 | 2 = 1;
// Handler for failed reconnection attempts
const reconnectionFailed = () => {
// Collapse status bar
this.collapseStatusBar();
// Reset state
this.stateService.reset();
// Redirect to welcome page
this.$state.go('welcome', {
initParams: {
keyStore: originalKeyStore,
peerTrustedKey: originalPeerPermanentKeyBytes,
},
});
};
// Handlers for reconnecting timeout
const reconnect2Timeout = () => {
// Give up
this.$log.error(this.logTag, 'Reconnect timeout 2. Going back to initial loading screen...');
reconnectionFailed();
};
const reconnect1Timeout = () => {
// Could not connect so far.
this.$log.error(this.logTag, 'Reconnect timeout 1. Retrying...');
reconnectTry = 2;
this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
doSoftReconnect();
};
// Function to soft-reconnect. Does not reset the loaded data.
const doSoftReconnect = () => {
const deleteStoredData = false;
const resetPush = false;
const redirect = false;
this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
this.webClientService.init(originalKeyStore, originalPeerPermanentKeyBytes, false);
this.webClientService.start().then(
() => {
// Cancel timeout
this.$timeout.cancel(this.reconnectTimeout);
// Hide expanded status bar
this.collapseStatusBar();
},
(error) => {
this.$log.error(this.logTag, 'Error state:', error);
this.$timeout.cancel(this.reconnectTimeout);
reconnectionFailed();
},
(progress: threema.ConnectionBuildupStateChange) => {
if (progress.state === 'peer_handshake' || progress.state === 'loading') {
this.$log.debug(this.logTag, 'Connection buildup advanced, resetting timeout');
// Restart timeout
this.$timeout.cancel(this.reconnectTimeout);
if (reconnectTry === 1) {
this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
} else if (reconnectTry === 2) {
this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
} else {
throw new Error('Invalid reconnectTry value: ' + reconnectTry);
}
}
},
);
};
// Start timeout
this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
// Start reconnecting process
doSoftReconnect();
// TODO: Handle server closing state
}
/**
* Attempt to reconnect an iOS device after a connection loss.
*/
private reconnectIos(): void {
this.$log.warn(this.logTag, 'Connection lost (iOS). Attempting to reconnect...');
// Get original keys
const originalKeyStore = this.webClientService.salty.keyStore;
const originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
// Handler for failed reconnection attempts
const reconnectionFailed = () => {
// Reset state
this.stateService.reset();
// Redirect to welcome page
this.$state.go('welcome', {
initParams: {
keyStore: originalKeyStore,
peerTrustedKey: originalPeerPermanentKeyBytes,
},
});
};
const deleteStoredData = false;
const resetPush = false;
const skipPush = true;
const redirect = false;
const startTimeout = 500; // Delay connecting a bit to wait for old websocket to close
this.$log.debug(this.logTag, 'Stopping old connection');
this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
this.$timeout(() => {
this.$log.debug(this.logTag, 'Starting new connection');
this.webClientService.init(originalKeyStore, originalPeerPermanentKeyBytes, false);
this.webClientService.start(skipPush).then(
() => { /* ok */ },
(error) => {
this.$log.error(this.logTag, 'Error state:', error);
reconnectionFailed();
},
// Progress
(progress: threema.ConnectionBuildupStateChange) => {
this.$log.debug(this.logTag, 'Connection buildup advanced:', progress);
},
);
}, startTimeout);
}
public wide(): boolean {
return this.controllerService.getControllerName() !== undefined
&& this.controllerService.getControllerName() === 'messenger';
}
}