123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- /**
- * 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 <http://www.gnu.org/licenses/>.
- */
- 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;
- import DisconnectReason = threema.DisconnectReason;
- /**
- * This controller handles state changes globally.
- *
- * It also controls auto-reconnecting and the connection status indicator bar.
- *
- * Status updates should be done through the state service.
- */
- export class StatusController {
- private logTag: string = '[StatusController]';
- // State variable
- private state = GlobalConnectionState.Error;
- // Expanded status bar
- public expandStatusBar = false;
- private expandStatusBarTimer: ng.IPromise<void> | null = null;
- private expandStatusBarTimeout = 3000;
- // Reconnect
- private reconnectTimeout: ng.IPromise<void>;
- // Angular services
- private $timeout: ng.ITimeoutService;
- private $log: ng.ILogService;
- private $state: UiStateService;
- // Custom services
- private controllerService: ControllerService;
- private stateService: StateService;
- private timeoutService: TimeoutService;
- private webClientService: WebClientService;
- public static $inject = [
- '$scope', '$timeout', '$log', '$state',
- 'ControllerService', 'StateService', 'TimeoutService', 'WebClientService',
- ];
- constructor($scope, $timeout: ng.ITimeoutService, $log: ng.ILogService, $state: UiStateService,
- controllerService: ControllerService, stateService: StateService,
- timeoutService: TimeoutService, webClientService: WebClientService) {
- // Angular services
- this.$timeout = $timeout;
- this.$log = $log;
- this.$state = $state;
- // Custom services
- this.controllerService = controllerService;
- this.stateService = stateService;
- this.timeoutService = timeoutService;
- this.webClientService = webClientService;
- // 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();
- }
- if (this.stateService.wasConnected && isRelayedData) {
- this.reconnectIos();
- }
- 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.timeoutService.register(() => {
- this.expandStatusBar = true;
- }, this.expandStatusBarTimeout, true, 'expandStatusBar');
- }
- /**
- * Collapse the status bar if expanded.
- */
- private collapseStatusBar(): void {
- this.expandStatusBar = false;
- if (this.expandStatusBarTimer !== null) {
- this.timeoutService.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 connection & state
- this.webClientService.stop({
- reason: DisconnectReason.SessionError,
- send: false,
- // TODO: Use welcome.error once we have it
- close: 'welcome',
- connectionBuildupState: 'reconnect_failed',
- });
- };
- // 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 = () => {
- this.webClientService.stop({
- reason: DisconnectReason.SessionStopped,
- send: true,
- close: false,
- });
- this.webClientService.init({
- keyStore: originalKeyStore,
- peerTrustedKey: originalPeerPermanentKeyBytes,
- resume: true,
- });
- 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.info(this.logTag, 'Connection lost (iOS). Attempting to reconnect...');
- // Get original keys
- const originalKeyStore = this.webClientService.salty.keyStore;
- const originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
- // Delay connecting a bit to wait for old websocket to close
- // TODO: Make this more robust and hopefully faster
- const startTimeout = 500;
- this.$log.debug(this.logTag, 'Stopping old connection');
- this.webClientService.stop({
- reason: DisconnectReason.SessionStopped,
- send: true,
- close: false,
- connectionBuildupState: 'push',
- });
- // Only send a push...
- const push = ((): { send: boolean, reason?: string } => {
- // ... if never left the 'welcome' page.
- if (this.$state.includes('welcome')) {
- return {
- send: true,
- reason: 'still on welcome page',
- };
- }
- // ... if there is at least one unacknowledged wire message.
- const pendingWireMessages = this.webClientService.unacknowledgedWireMessages;
- if (pendingWireMessages > 0) {
- return {
- send: true,
- reason: `${pendingWireMessages} unacknowledged wire messages`,
- };
- }
- // ... if there are one or more cached chunks that require immediate
- // sending.
- const immediateChunksPending = this.webClientService.immediateChunksPending;
- if (immediateChunksPending > 0) {
- return {
- send: true,
- reason: `${immediateChunksPending} chunks that require acknowledgement`,
- };
- }
- // ... otherwise, don't push!
- return {
- send: false,
- };
- })();
- this.$timeout(() => {
- if (push.send) {
- this.$log.debug(`Starting new connection with push, reason: ${push.reason}`);
- } else {
- this.$log.debug('Starting new connection without push');
- }
- this.webClientService.init({
- keyStore: originalKeyStore,
- peerTrustedKey: originalPeerPermanentKeyBytes,
- resume: true,
- });
- this.webClientService.start(!push.send).then(
- () => { /* ok */ },
- (error) => {
- this.$log.error(this.logTag, 'Error state:', error);
- this.webClientService.stop({
- reason: DisconnectReason.SessionError,
- send: false,
- // TODO: Use welcome.error once we have it
- close: 'welcome',
- connectionBuildupState: 'reconnect_failed',
- });
- },
- // 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';
- }
- }
|