From 0020880c6a0196be19e61cc6753b9dd99b43bab0 Mon Sep 17 00:00:00 2001
From: Albert Gasset <albertgasset@fsfe.org>
Date: Thu, 21 Jun 2018 15:59:06 +0200
Subject: [PATCH] MOBILE-2431: Wrap Ionic Native callbacks with NgZone.run

---
 src/addon/messages/messages.module.ts         |  9 ++++---
 src/addon/mod/chat/pages/chat/chat.ts         |  9 ++++---
 src/addon/mod/chat/pages/users/users.ts       |  9 ++++---
 src/addon/mod/feedback/pages/form/form.ts     |  9 ++++---
 .../mod/forum/pages/discussion/discussion.ts  |  8 ++++--
 .../providers/pushnotifications.ts            | 25 +++++++++++++------
 src/app/app.component.ts                      | 23 +++++++++--------
 .../course/classes/main-activity-component.ts |  8 ++++--
 .../pages/course-preview/course-preview.ts    | 15 ++++++++---
 src/providers/app.ts                          | 19 ++++++++------
 src/providers/cron.ts                         |  9 ++++---
 src/providers/filepool.ts                     | 10 +++++---
 12 files changed, 102 insertions(+), 51 deletions(-)

diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts
index 90f4abff9..7eab2f334 100644
--- a/src/addon/messages/messages.module.ts
+++ b/src/addon/messages/messages.module.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { NgModule } from '@angular/core';
+import { NgModule, NgZone } from '@angular/core';
 import { Network } from '@ionic-native/network';
 import { AddonMessagesProvider } from './providers/messages';
 import { AddonMessagesOfflineProvider } from './providers/messages-offline';
@@ -69,7 +69,7 @@ export class AddonMessagesModule {
             contentLinksDelegate: CoreContentLinksDelegate, indexLinkHandler: AddonMessagesIndexLinkHandler,
             discussionLinkHandler: AddonMessagesDiscussionLinkHandler, sendMessageHandler: AddonMessagesSendMessageUserHandler,
             userDelegate: CoreUserDelegate, cronDelegate: CoreCronDelegate, syncHandler: AddonMessagesSyncCronHandler,
-            network: Network, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider,
+            network: Network, zone: NgZone, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider,
             localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider,
             sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, updateManager: CoreUpdateManagerProvider,
             settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate,
@@ -88,7 +88,10 @@ export class AddonMessagesModule {
 
         // Sync some discussions when device goes online.
         network.onConnect().subscribe(() => {
-            messagesSync.syncAllDiscussions(undefined, true);
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                messagesSync.syncAllDiscussions(undefined, true);
+            });
         });
 
         const notificationClicked = (notification: any): void => {
diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts
index 082509e21..d724549c2 100644
--- a/src/addon/mod/chat/pages/chat/chat.ts
+++ b/src/addon/mod/chat/pages/chat/chat.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, ViewChild } from '@angular/core';
+import { Component, ViewChild, NgZone } from '@angular/core';
 import { Content, IonicPage, ModalController, NavController, NavParams } from 'ionic-angular';
 import { CoreAppProvider } from '@providers/app';
 import { CoreLoggerProvider } from '@providers/logger';
@@ -52,7 +52,7 @@ export class AddonModChatChatPage {
     protected viewDestroyed = false;
     protected pollingRunning = false;
 
-    constructor(navParams: NavParams, logger: CoreLoggerProvider, network: Network, private navCtrl: NavController,
+    constructor(navParams: NavParams, logger: CoreLoggerProvider, network: Network,  zone: NgZone, private navCtrl: NavController,
             private chatProvider: AddonModChatProvider, private appProvider: CoreAppProvider, sitesProvider: CoreSitesProvider,
             private modalCtrl: ModalController, private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider) {
 
@@ -63,7 +63,10 @@ export class AddonModChatChatPage {
         this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId();
         this.isOnline = this.appProvider.isOnline();
         this.onlineObserver = network.onchange().subscribe((online) => {
-            this.isOnline = this.appProvider.isOnline();
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.isOnline = this.appProvider.isOnline();
+            });
         });
     }
 
diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts
index ee6a4f126..a9f4f175a 100644
--- a/src/addon/mod/chat/pages/users/users.ts
+++ b/src/addon/mod/chat/pages/users/users.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component } from '@angular/core';
+import { Component, NgZone } from '@angular/core';
 import { IonicPage, NavParams, ViewController } from 'ionic-angular';
 import { CoreAppProvider } from '@providers/app';
 import { CoreSitesProvider } from '@providers/sites';
@@ -38,14 +38,17 @@ export class AddonModChatUsersPage {
     protected sessionId: number;
     protected onlineObserver: any;
 
-    constructor(navParams: NavParams, network: Network, private appProvider: CoreAppProvider,
+    constructor(navParams: NavParams, network: Network,  zone: NgZone, private appProvider: CoreAppProvider,
             private sitesProvider: CoreSitesProvider, private viewCtrl: ViewController,
             private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) {
         this.sessionId = navParams.get('sessionId');
         this.isOnline = this.appProvider.isOnline();
         this.currentUserId = this.sitesProvider.getCurrentSiteUserId();
         this.onlineObserver = network.onchange().subscribe((online) => {
-            this.isOnline = this.appProvider.isOnline();
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.isOnline = this.appProvider.isOnline();
+            });
         });
     }
 
diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts
index a1641c98c..fc1677027 100644
--- a/src/addon/mod/feedback/pages/form/form.ts
+++ b/src/addon/mod/feedback/pages/form/form.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, OnDestroy, Optional } from '@angular/core';
+import { Component, OnDestroy, Optional, NgZone } from '@angular/core';
 import { IonicPage, NavParams, NavController, Content } from 'ionic-angular';
 import { Network } from '@ionic-native/network';
 import { TranslateService } from '@ngx-translate/core';
@@ -69,7 +69,7 @@ export class AddonModFeedbackFormPage implements OnDestroy {
             protected eventsProvider: CoreEventsProvider, protected feedbackSync: AddonModFeedbackSyncProvider, network: Network,
             protected translate: TranslateService, protected loginHelper: CoreLoginHelperProvider,
             protected linkHelper: CoreContentLinksHelperProvider, sitesProvider: CoreSitesProvider,
-            @Optional() private content: Content) {
+            @Optional() private content: Content, zone: NgZone) {
 
         this.module = navParams.get('module');
         this.courseId = navParams.get('courseId');
@@ -82,7 +82,10 @@ export class AddonModFeedbackFormPage implements OnDestroy {
 
         // Refresh online status when changes.
         this.onlineObserver = network.onchange().subscribe((online) => {
-            this.offline = !online;
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.offline = !online;
+            });
         });
     }
 
diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts
index 94d36121b..752e73f4b 100644
--- a/src/addon/mod/forum/pages/discussion/discussion.ts
+++ b/src/addon/mod/forum/pages/discussion/discussion.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, Optional, OnDestroy, ViewChild } from '@angular/core';
+import { Component, Optional, OnDestroy, ViewChild, NgZone } from '@angular/core';
 import { IonicPage, NavParams, Content } from 'ionic-angular';
 import { Network } from '@ionic-native/network';
 import { TranslateService } from '@ngx-translate/core';
@@ -78,6 +78,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
 
     constructor(navParams: NavParams,
             network: Network,
+            zone: NgZone,
             private appProvider: CoreAppProvider,
             private eventsProvider: CoreEventsProvider,
             private sitesProvider: CoreSitesProvider,
@@ -99,7 +100,10 @@ export class AddonModForumDiscussionPage implements OnDestroy {
 
         this.isOnline = this.appProvider.isOnline();
         this.onlineObserver = network.onchange().subscribe((online) => {
-            this.isOnline = this.appProvider.isOnline();
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.isOnline = this.appProvider.isOnline();
+            });
         });
         this.isSplitViewOn = this.svComponent && this.svComponent.isOn();
 
diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts
index 444a7b1c9..971e8da42 100644
--- a/src/addon/pushnotifications/providers/pushnotifications.ts
+++ b/src/addon/pushnotifications/providers/pushnotifications.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
 import { Platform } from 'ionic-angular';
 import { Badge } from '@ionic-native/badge';
 import { Push, PushObject, PushOptions } from '@ionic-native/push';
@@ -65,7 +65,7 @@ export class AddonPushNotificationsProvider {
             protected pushNotificationsDelegate: AddonPushNotificationsDelegate, protected sitesProvider: CoreSitesProvider,
             private badge: Badge, private localNotificationsProvider: CoreLocalNotificationsProvider,
             private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private push: Push,
-            private configProvider: CoreConfigProvider, private device: Device) {
+            private configProvider: CoreConfigProvider, private device: Device, private zone: NgZone) {
         this.logger = logger.getInstance('AddonPushNotificationsProvider');
         this.appDB = appProvider.getDB();
         this.appDB.createTablesFromSchema(this.tablesSchema);
@@ -326,19 +326,28 @@ export class AddonPushNotificationsProvider {
                 const pushObject: PushObject = this.push.init(options);
 
                 pushObject.on('notification').subscribe((notification: any) => {
-                    this.logger.log('Received a notification', notification);
-                    this.onMessageReceived(notification);
+                    // Execute the callback in the Angular zone, so change detection doesn't stop working.
+                    this.zone.run(() => {
+                        this.logger.log('Received a notification', notification);
+                        this.onMessageReceived(notification);
+                    });
                 });
 
                 pushObject.on('registration').subscribe((data: any) => {
-                    this.pushID = data.registrationId;
-                    this.registerDeviceOnMoodle().catch((error) => {
-                        this.logger.warn('Can\'t register device', error);
+                    // Execute the callback in the Angular zone, so change detection doesn't stop working.
+                    this.zone.run(() => {
+                        this.pushID = data.registrationId;
+                        this.registerDeviceOnMoodle().catch((error) => {
+                            this.logger.warn('Can\'t register device', error);
+                        });
                     });
                 });
 
                 pushObject.on('error').subscribe((error: any) => {
-                    this.logger.warn('Error with Push plugin', error);
+                    // Execute the callback in the Angular zone, so change detection doesn't stop working.
+                    this.zone.run(() => {
+                        this.logger.warn('Error with Push plugin', error);
+                    });
                 });
             });
         } catch (ex) {
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index d93ed1754..8988711b5 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, NgZone } from '@angular/core';
 import { Platform } from 'ionic-angular';
 import { StatusBar } from '@ionic-native/status-bar';
 import { CoreAppProvider } from '@providers/app';
@@ -33,7 +33,7 @@ export class MoodleMobileApp implements OnInit {
     protected lastUrls = {};
 
     constructor(private platform: Platform, statusBar: StatusBar, logger: CoreLoggerProvider,
-        private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider,
+        private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private zone: NgZone,
         private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider) {
         this.logger = logger.getInstance('AppComponent');
 
@@ -101,17 +101,20 @@ export class MoodleMobileApp implements OnInit {
 
         // Handle app launched with a certain URL (custom URL scheme).
         (<any> window).handleOpenURL = (url: string): void => {
-            // First check that the URL hasn't been treated a few seconds ago. Sometimes this function is called more than once.
-            if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
-                // Function called more than once, stop.
-                return;
-            }
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            this.zone.run(() => {
+                // First check that the URL hasn't been treated a few seconds ago. Sometimes this function is called more than once.
+                if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
+                    // Function called more than once, stop.
+                    return;
+                }
 
-            this.logger.debug('App launched by URL ', url);
+                this.logger.debug('App launched by URL ', url);
 
-            this.lastUrls[url] = Date.now();
+                this.lastUrls[url] = Date.now();
 
-            this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url);
+                this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url);
+            });
         };
 
         // Listen for app launched URLs. If we receive one, check if it's a SSO authentication.
diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts
index 909a9bbb6..28950d6bf 100644
--- a/src/core/course/classes/main-activity-component.ts
+++ b/src/core/course/classes/main-activity-component.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Injector, Input } from '@angular/core';
+import { Injector, Input, NgZone } from '@angular/core';
 import { Content } from 'ionic-angular';
 import { CoreSitesProvider } from '@providers/sites';
 import { CoreCourseProvider } from '@core/course/providers/course';
@@ -60,10 +60,14 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
         this.modulePrefetchDelegate = injector.get(CoreCourseModulePrefetchDelegate);
 
         const network = injector.get(Network);
+        const zone = injector.get(NgZone);
 
         // Refresh online status when changes.
         this.onlineObserver = network.onchange().subscribe((online) => {
-            this.isOnline = online;
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.isOnline = online;
+            });
         });
     }
 
diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts
index 7189d54a5..3d672fb12 100644
--- a/src/core/courses/pages/course-preview/course-preview.ts
+++ b/src/core/courses/pages/course-preview/course-preview.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, OnDestroy } from '@angular/core';
+import { Component, OnDestroy, NgZone } from '@angular/core';
 import { IonicPage, NavController, NavParams, Platform, ModalController, Modal } from 'ionic-angular';
 import { TranslateService } from '@ngx-translate/core';
 import { CoreAppProvider } from '@providers/app';
@@ -69,7 +69,8 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
             private coursesProvider: CoreCoursesProvider, private platform: Platform, private modalCtrl: ModalController,
             private translate: TranslateService, private eventsProvider: CoreEventsProvider,
             private courseOptionsDelegate: CoreCourseOptionsDelegate, private courseHelper: CoreCourseHelperProvider,
-            private courseProvider: CoreCourseProvider, private courseFormatDelegate: CoreCourseFormatDelegate) {
+            private courseProvider: CoreCourseProvider, private courseFormatDelegate: CoreCourseFormatDelegate,
+            private zone: NgZone) {
 
         this.course = navParams.get('course');
         this.avoidOpenCourse = navParams.get('avoidOpenCourse');
@@ -292,9 +293,15 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
 
             if (this.isDesktop || this.isMobile) {
                 // Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
-                inAppLoadSubscription = window.on('loadstart').subscribe(urlLoaded);
+                inAppLoadSubscription = window.on('loadstart').subscribe((event) => {
+                    // Execute the callback in the Angular zone, so change detection doesn't stop working.
+                    this.zone.run(() => urlLoaded(event));
+                });
                 // Observe window closed.
-                inAppExitSubscription = window.on('exit').subscribe(inAppClosed);
+                inAppExitSubscription = window.on('exit').subscribe(() => {
+                    // Execute the callback in the Angular zone, so change detection doesn't stop working.
+                    this.zone.run(inAppClosed);
+                });
             }
 
             if (this.isDesktop) {
diff --git a/src/providers/app.ts b/src/providers/app.ts
index 0a7a2d2e2..bac647c0a 100644
--- a/src/providers/app.ts
+++ b/src/providers/app.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
 import { Platform, App, NavController } from 'ionic-angular';
 import { Keyboard } from '@ionic-native/keyboard';
 import { Network } from '@ionic-native/network';
@@ -70,18 +70,23 @@ export class CoreAppProvider {
     protected isKeyboardShown = false;
 
     constructor(dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, private appCtrl: App,
-            private network: Network, logger: CoreLoggerProvider, events: CoreEventsProvider) {
+            private network: Network, logger: CoreLoggerProvider, events: CoreEventsProvider, zone: NgZone) {
         this.logger = logger.getInstance('CoreAppProvider');
         this.db = dbProvider.getDB(this.DBNAME);
 
         this.keyboard.onKeyboardShow().subscribe((data) => {
-            this.isKeyboardShown = true;
-            events.trigger(CoreEventsProvider.KEYBOARD_CHANGE, this.isKeyboardShown);
-
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.isKeyboardShown = true;
+                events.trigger(CoreEventsProvider.KEYBOARD_CHANGE, this.isKeyboardShown);
+            });
         });
         this.keyboard.onKeyboardHide().subscribe((data) => {
-            this.isKeyboardShown = false;
-            events.trigger(CoreEventsProvider.KEYBOARD_CHANGE, this.isKeyboardShown);
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.isKeyboardShown = false;
+                events.trigger(CoreEventsProvider.KEYBOARD_CHANGE, this.isKeyboardShown);
+            });
         });
     }
 
diff --git a/src/providers/cron.ts b/src/providers/cron.ts
index d891d44b2..d578fd1f8 100644
--- a/src/providers/cron.ts
+++ b/src/providers/cron.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
 import { Network } from '@ionic-native/network';
 import { CoreAppProvider } from './app';
 import { CoreConfigProvider } from './config';
@@ -115,7 +115,7 @@ export class CoreCronDelegate {
     protected queuePromise = Promise.resolve();
 
     constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private configProvider: CoreConfigProvider,
-            private utils: CoreUtilsProvider, network: Network) {
+            private utils: CoreUtilsProvider, network: Network, zone: NgZone) {
         this.logger = logger.getInstance('CoreCronDelegate');
 
         this.appDB = this.appProvider.getDB();
@@ -123,7 +123,10 @@ export class CoreCronDelegate {
 
         // When the app is re-connected, start network handlers that were stopped.
         network.onConnect().subscribe(() => {
-            this.startNetworkHandlers();
+            // Execute the callback in the Angular zone, so change detection doesn't stop working.
+            zone.run(() => {
+                this.startNetworkHandlers();
+            });
         });
     }
 
diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts
index 8a350fc0a..31c53a230 100644
--- a/src/providers/filepool.ts
+++ b/src/providers/filepool.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
 import { Network } from '@ionic-native/network';
 import { CoreAppProvider } from './app';
 import { CoreEventsProvider } from './events';
@@ -436,7 +436,8 @@ export class CoreFilepoolProvider {
             private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider,
             private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, private urlUtils: CoreUrlUtilsProvider,
             private timeUtils: CoreTimeUtilsProvider, private eventsProvider: CoreEventsProvider, initDelegate: CoreInitDelegate,
-            network: Network, private pluginFileDelegate: CorePluginFileDelegate, private domUtils: CoreDomUtilsProvider) {
+            network: Network, private pluginFileDelegate: CorePluginFileDelegate, private domUtils: CoreDomUtilsProvider,
+            zone: NgZone) {
         this.logger = logger.getInstance('CoreFilepoolProvider');
 
         this.appDB = this.appProvider.getDB();
@@ -450,7 +451,10 @@ export class CoreFilepoolProvider {
 
             // Start queue when device goes online.
             network.onConnect().subscribe(() => {
-                this.checkQueueProcessing();
+                // Execute the callback in the Angular zone, so change detection doesn't stop working.
+                zone.run(() => {
+                    this.checkQueueProcessing();
+                });
             });
         });
     }