status.ts 13 KB

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