status.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import {ControllerService} from '../services/controller';
  18. import {StateService} from '../services/state';
  19. import {WebClientService} from '../services/webclient';
  20. /**
  21. * This controller handles state changes globally.
  22. *
  23. * It also controls auto-reconnecting and the connection status indicator bar.
  24. *
  25. * Status updates should be done through the status service.
  26. */
  27. export class StatusController {
  28. private logTag: string = '[StatusController]';
  29. // State variable
  30. private state: threema.GlobalConnectionState = 'error';
  31. // Expanded status bar
  32. public expandStatusBar = false;
  33. private expandStatusBarTimer: ng.IPromise<void> | null = null;
  34. private expandStatusBarTimeout = 3000;
  35. // Reconnect
  36. private reconnectTimeout: ng.IPromise<void>;
  37. // Angular services
  38. private $timeout: ng.ITimeoutService;
  39. private $state: ng.ui.IStateService;
  40. private $log: ng.ILogService;
  41. // Custom services
  42. private stateService: StateService;
  43. private webClientService: WebClientService;
  44. private controllerService: ControllerService;
  45. public static $inject = ['$scope', '$timeout', '$log', '$state', 'StateService',
  46. 'WebClientService', 'ControllerService'];
  47. constructor($scope, $timeout: ng.ITimeoutService, $log: ng.ILogService, $state: ng.ui.IStateService,
  48. stateService: StateService, webClientService: WebClientService,
  49. controllerService: ControllerService) {
  50. // Angular services
  51. this.$timeout = $timeout;
  52. this.$log = $log;
  53. this.$state = $state;
  54. // Custom services
  55. this.stateService = stateService;
  56. this.webClientService = webClientService;
  57. this.controllerService = controllerService;
  58. // Watch state changes
  59. $scope.$watch(
  60. () => stateService.state,
  61. (newValue: threema.GlobalConnectionState, oldValue: threema.GlobalConnectionState) => {
  62. if (oldValue !== newValue) {
  63. this.onStateChange(newValue, oldValue);
  64. }
  65. },
  66. );
  67. }
  68. /**
  69. * Return the prefixed status.
  70. */
  71. public get statusClass(): string {
  72. return 'status-' + this.state;
  73. }
  74. /**
  75. * Handle state changes.
  76. */
  77. private onStateChange(newValue: threema.GlobalConnectionState,
  78. oldValue: threema.GlobalConnectionState): void {
  79. if (newValue === oldValue) {
  80. return;
  81. }
  82. this.state = newValue;
  83. switch (newValue) {
  84. case 'ok':
  85. this.collapseStatusBar();
  86. break;
  87. case 'warning':
  88. if (oldValue === 'ok') {
  89. this.scheduleStatusBar();
  90. }
  91. break;
  92. case 'error':
  93. if (this.stateService.wasConnected) {
  94. if (oldValue === 'ok') {
  95. this.scheduleStatusBar();
  96. }
  97. this.reconnect();
  98. }
  99. break;
  100. default:
  101. this.$log.error(this.logTag, 'Invalid state change: From', oldValue, 'to', newValue);
  102. }
  103. }
  104. /**
  105. * Show full status bar with a certain delay.
  106. */
  107. private scheduleStatusBar(): void {
  108. this.expandStatusBarTimer = this.$timeout(() => {
  109. this.expandStatusBar = true;
  110. }, this.expandStatusBarTimeout);
  111. }
  112. /**
  113. * Collapse the status bar if expanded.
  114. */
  115. private collapseStatusBar(): void {
  116. this.expandStatusBar = false;
  117. if (this.expandStatusBarTimer !== null) {
  118. this.$timeout.cancel(this.expandStatusBarTimer);
  119. }
  120. }
  121. /**
  122. * Attempt to reconnect after a connection loss.
  123. */
  124. private reconnect(): void {
  125. this.$log.warn(this.logTag, 'Connection lost. Attempting to reconnect...');
  126. // Get original keys
  127. let originalKeyStore = this.webClientService.salty.keyStore;
  128. let originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
  129. // Timeout durations
  130. const TIMEOUT1 = 20 * 1000; // Duration per step for first reconnect
  131. const TIMEOUT2 = 20 * 1000; // Duration per step for second reconnect
  132. // Reconnect state
  133. let reconnectTry: 1 | 2 = 1;
  134. // Handler for failed reconnection attempts
  135. let reconnectionFailed = () => {
  136. // Collapse status bar
  137. this.collapseStatusBar();
  138. // Reset state
  139. this.stateService.reset();
  140. // Redirect to welcome page
  141. this.$state.go('welcome', {
  142. initParams: {
  143. keyStore: originalKeyStore,
  144. peerTrustedKey: originalPeerPermanentKeyBytes,
  145. },
  146. });
  147. };
  148. // Handlers for reconnecting timeout
  149. const reconnect2Timeout = () => {
  150. // Give up
  151. this.$log.error(this.logTag, 'Reconnect timeout 2. Going back to initial loading screen...');
  152. reconnectionFailed();
  153. };
  154. const reconnect1Timeout = () => {
  155. // Could not connect so far.
  156. this.$log.error(this.logTag, 'Reconnect timeout 1. Retrying...');
  157. reconnectTry = 2;
  158. this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
  159. doSoftReconnect();
  160. };
  161. // Function to soft-reconnect. Does not reset the loaded data.
  162. let doSoftReconnect = () => {
  163. const deleteStoredData = false;
  164. const resetPush = false;
  165. const redirect = false;
  166. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  167. this.webClientService.init(originalKeyStore, originalPeerPermanentKeyBytes, false);
  168. this.webClientService.start().then(
  169. () => {
  170. // Cancel timeout
  171. this.$timeout.cancel(this.reconnectTimeout);
  172. // Hide expanded status bar
  173. this.collapseStatusBar();
  174. },
  175. (error) => {
  176. this.$log.error(this.logTag, 'Error state:', error);
  177. this.$timeout.cancel(this.reconnectTimeout);
  178. reconnectionFailed();
  179. },
  180. (progress: threema.ConnectionBuildupStateChange) => {
  181. if (progress.state === 'peer_handshake' || progress.state === 'loading') {
  182. this.$log.debug(this.logTag, 'Connection buildup advanced, resetting timeout');
  183. // Restart timeout
  184. this.$timeout.cancel(this.reconnectTimeout);
  185. if (reconnectTry === 1) {
  186. this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
  187. } else if (reconnectTry === 2) {
  188. this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
  189. } else {
  190. throw new Error('Invalid reconnectTry value: ' + reconnectTry);
  191. }
  192. }
  193. },
  194. );
  195. };
  196. // Start timeout
  197. this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
  198. // Start reconnecting process
  199. doSoftReconnect();
  200. // TODO: Handle server closing state
  201. }
  202. public wide(): boolean {
  203. return this.controllerService.getControllerName() !== undefined
  204. && this.controllerService.getControllerName() === 'messenger';
  205. }
  206. }