status.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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 {StateService as UiStateService} from '@uirouter/angularjs';
  18. import {ControllerService} from '../services/controller';
  19. import {StateService} from '../services/state';
  20. import {WebClientService} from '../services/webclient';
  21. import GlobalConnectionState = threema.GlobalConnectionState;
  22. /**
  23. * This controller handles state changes globally.
  24. *
  25. * It also controls auto-reconnecting and the connection status indicator bar.
  26. *
  27. * Status updates should be done through the status service.
  28. */
  29. export class StatusController {
  30. private logTag: string = '[StatusController]';
  31. // State variable
  32. private state = GlobalConnectionState.Error;
  33. // Expanded status bar
  34. public expandStatusBar = false;
  35. private expandStatusBarTimer: ng.IPromise<void> | null = null;
  36. private expandStatusBarTimeout = 3000;
  37. // Reconnect
  38. private reconnectTimeout: ng.IPromise<void>;
  39. // Angular services
  40. private $timeout: ng.ITimeoutService;
  41. private $log: ng.ILogService;
  42. private $state: UiStateService;
  43. // Custom services
  44. private stateService: StateService;
  45. private webClientService: WebClientService;
  46. private controllerService: ControllerService;
  47. public static $inject = ['$scope', '$timeout', '$log', '$state', 'StateService',
  48. 'WebClientService', 'ControllerService'];
  49. constructor($scope, $timeout: ng.ITimeoutService, $log: ng.ILogService, $state: UiStateService,
  50. stateService: StateService, webClientService: WebClientService,
  51. controllerService: ControllerService) {
  52. // Angular services
  53. this.$timeout = $timeout;
  54. this.$log = $log;
  55. this.$state = $state;
  56. // Custom services
  57. this.stateService = stateService;
  58. this.webClientService = webClientService;
  59. this.controllerService = controllerService;
  60. // Register event handlers
  61. this.stateService.evtGlobalConnectionStateChange.attach(
  62. (stateChange: threema.GlobalConnectionStateChange) => {
  63. this.onStateChange(stateChange.state, stateChange.prevState);
  64. },
  65. );
  66. }
  67. /**
  68. * Return the prefixed status.
  69. */
  70. public get statusClass(): string {
  71. return 'status-task-' + this.webClientService.chosenTask + ' status-' + this.state;
  72. }
  73. /**
  74. * Handle state changes.
  75. */
  76. private onStateChange(newValue: threema.GlobalConnectionState,
  77. oldValue: threema.GlobalConnectionState): void {
  78. this.$log.debug(this.logTag, 'State change:', oldValue, '->', newValue);
  79. if (newValue === oldValue) {
  80. return;
  81. }
  82. this.state = newValue;
  83. const isWebrtc = this.webClientService.chosenTask === threema.ChosenTask.WebRTC;
  84. const isRelayedData = this.webClientService.chosenTask === threema.ChosenTask.RelayedData;
  85. switch (newValue) {
  86. case 'ok':
  87. this.collapseStatusBar();
  88. break;
  89. case 'warning':
  90. if (oldValue === 'ok' && isWebrtc) {
  91. this.scheduleStatusBar();
  92. }
  93. if (this.stateService.wasConnected) {
  94. this.webClientService.clearIsTypingFlags();
  95. }
  96. if (this.stateService.wasConnected && isRelayedData) {
  97. this.reconnectIos();
  98. }
  99. break;
  100. case 'error':
  101. if (this.stateService.wasConnected && isWebrtc) {
  102. if (oldValue === 'ok') {
  103. this.scheduleStatusBar();
  104. }
  105. this.reconnectAndroid();
  106. }
  107. break;
  108. default:
  109. this.$log.error(this.logTag, 'Invalid state change: From', oldValue, 'to', newValue);
  110. }
  111. }
  112. /**
  113. * Show full status bar with a certain delay.
  114. */
  115. private scheduleStatusBar(): void {
  116. this.expandStatusBarTimer = this.$timeout(() => {
  117. this.expandStatusBar = true;
  118. }, this.expandStatusBarTimeout);
  119. }
  120. /**
  121. * Collapse the status bar if expanded.
  122. */
  123. private collapseStatusBar(): void {
  124. this.expandStatusBar = false;
  125. if (this.expandStatusBarTimer !== null) {
  126. this.$timeout.cancel(this.expandStatusBarTimer);
  127. }
  128. }
  129. /**
  130. * Attempt to reconnect an Android device after a connection loss.
  131. */
  132. private reconnectAndroid(): void {
  133. this.$log.warn(this.logTag, 'Connection lost (Android). Attempting to reconnect...');
  134. // Get original keys
  135. const originalKeyStore = this.webClientService.salty.keyStore;
  136. const originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
  137. // Timeout durations
  138. const TIMEOUT1 = 20 * 1000; // Duration per step for first reconnect
  139. const TIMEOUT2 = 20 * 1000; // Duration per step for second reconnect
  140. // Reconnect state
  141. let reconnectTry: 1 | 2 = 1;
  142. // Handler for failed reconnection attempts
  143. const reconnectionFailed = () => {
  144. // Collapse status bar
  145. this.collapseStatusBar();
  146. // Reset state
  147. this.stateService.reset();
  148. // Redirect to welcome page
  149. this.$state.go('welcome', {
  150. initParams: {
  151. keyStore: originalKeyStore,
  152. peerTrustedKey: originalPeerPermanentKeyBytes,
  153. },
  154. });
  155. };
  156. // Handlers for reconnecting timeout
  157. const reconnect2Timeout = () => {
  158. // Give up
  159. this.$log.error(this.logTag, 'Reconnect timeout 2. Going back to initial loading screen...');
  160. reconnectionFailed();
  161. };
  162. const reconnect1Timeout = () => {
  163. // Could not connect so far.
  164. this.$log.error(this.logTag, 'Reconnect timeout 1. Retrying...');
  165. reconnectTry = 2;
  166. this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
  167. doSoftReconnect();
  168. };
  169. // Function to soft-reconnect. Does not reset the loaded data.
  170. const doSoftReconnect = () => {
  171. const deleteStoredData = false;
  172. const resetPush = false;
  173. const redirect = false;
  174. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  175. this.webClientService.init(originalKeyStore, originalPeerPermanentKeyBytes, false);
  176. this.webClientService.start().then(
  177. () => {
  178. // Cancel timeout
  179. this.$timeout.cancel(this.reconnectTimeout);
  180. // Hide expanded status bar
  181. this.collapseStatusBar();
  182. },
  183. (error) => {
  184. this.$log.error(this.logTag, 'Error state:', error);
  185. this.$timeout.cancel(this.reconnectTimeout);
  186. reconnectionFailed();
  187. },
  188. (progress: threema.ConnectionBuildupStateChange) => {
  189. if (progress.state === 'peer_handshake' || progress.state === 'loading') {
  190. this.$log.debug(this.logTag, 'Connection buildup advanced, resetting timeout');
  191. // Restart timeout
  192. this.$timeout.cancel(this.reconnectTimeout);
  193. if (reconnectTry === 1) {
  194. this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
  195. } else if (reconnectTry === 2) {
  196. this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
  197. } else {
  198. throw new Error('Invalid reconnectTry value: ' + reconnectTry);
  199. }
  200. }
  201. },
  202. );
  203. };
  204. // Start timeout
  205. this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
  206. // Start reconnecting process
  207. doSoftReconnect();
  208. // TODO: Handle server closing state
  209. }
  210. /**
  211. * Attempt to reconnect an iOS device after a connection loss.
  212. */
  213. private reconnectIos(): void {
  214. this.$log.warn(this.logTag, 'Connection lost (iOS). Attempting to reconnect...');
  215. // Get original keys
  216. const originalKeyStore = this.webClientService.salty.keyStore;
  217. const originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
  218. // Handler for failed reconnection attempts
  219. const reconnectionFailed = () => {
  220. // Reset state
  221. this.stateService.reset();
  222. // Redirect to welcome page
  223. this.$state.go('welcome', {
  224. initParams: {
  225. keyStore: originalKeyStore,
  226. peerTrustedKey: originalPeerPermanentKeyBytes,
  227. },
  228. });
  229. };
  230. const deleteStoredData = false;
  231. const resetPush = false;
  232. const skipPush = true;
  233. const redirect = false;
  234. const startTimeout = 500; // Delay connecting a bit to wait for old websocket to close
  235. this.$log.debug(this.logTag, 'Stopping old connection');
  236. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  237. this.$timeout(() => {
  238. this.$log.debug(this.logTag, 'Starting new connection');
  239. this.webClientService.init(originalKeyStore, originalPeerPermanentKeyBytes, false);
  240. this.webClientService.start(skipPush).then(
  241. () => { /* ok */ },
  242. (error) => {
  243. this.$log.error(this.logTag, 'Error state:', error);
  244. reconnectionFailed();
  245. },
  246. // Progress
  247. (progress: threema.ConnectionBuildupStateChange) => {
  248. this.$log.debug(this.logTag, 'Connection buildup advanced:', progress);
  249. },
  250. );
  251. }, startTimeout);
  252. }
  253. public wide(): boolean {
  254. return this.controllerService.getControllerName() !== undefined
  255. && this.controllerService.getControllerName() === 'messenger';
  256. }
  257. }