diff --git a/package.json b/package.json index 5652abe4e..e006c48b2 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,11 @@ "@angular/http": "5.0.0", "@angular/platform-browser": "5.0.0", "@angular/platform-browser-dynamic": "5.0.0", + "@ionic-native/badge": "^4.5.3", "@ionic-native/camera": "^4.5.2", "@ionic-native/clipboard": "^4.3.2", "@ionic-native/core": "4.3.0", + "@ionic-native/device": "^4.5.3", "@ionic-native/file": "^4.3.3", "@ionic-native/file-transfer": "^4.3.3", "@ionic-native/globalization": "^4.3.2", @@ -49,6 +51,7 @@ "@ionic-native/local-notifications": "^4.4.0", "@ionic-native/media-capture": "^4.5.2", "@ionic-native/network": "^4.3.2", + "@ionic-native/push": "^4.5.3", "@ionic-native/splash-screen": "4.3.0", "@ionic-native/sqlite": "^4.3.2", "@ionic-native/status-bar": "4.3.0", diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts index 4999b90f9..1fda12a49 100644 --- a/src/addon/messages/messages.module.ts +++ b/src/addon/messages/messages.module.ts @@ -33,6 +33,8 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { AddonMessagesSettingsHandler } from './providers/settings-handler'; +import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; +import { CoreUtilsProvider } from '@providers/utils/utils'; @NgModule({ declarations: [ @@ -59,7 +61,8 @@ export class AddonMessagesModule { network: Network, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider, localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider, sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, - settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate) { + settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate, +pushNotificationsDelegate: AddonPushNotificationsDelegate, utils: CoreUtilsProvider) { // Register handlers. mainMenuDelegate.registerHandler(mainmenuHandler); contentLinksDelegate.registerHandler(indexLinkHandler); @@ -93,5 +96,17 @@ export class AddonMessagesModule { // Listen for clicks in simulated push notifications. localNotifications.registerClick(AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, notificationClicked); } + + // @todo: use addon manager $mmPushNotificationsDelegate = $mmAddonManager.get('$mmPushNotificationsDelegate'); + // Register push notification clicks. + if (pushNotificationsDelegate) { + pushNotificationsDelegate.registerHandler('mmaMessages', (notification) => { + if (utils.isFalseOrZero(notification.notif)) { + notificationClicked(notification); + + return true; + } + }); + } } } diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index 496251bfc..414e4fa38 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -97,12 +97,11 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => { // Leave badge enter if there is a 0+ or a 0. this.badge = parseInt(unread, 10) > 0 ? unread : ''; + // @todo: use addon manager $mmaPushNotifications = $mmAddonManager.get('$mmaPushNotifications'); // Update badge. - /* - $mmaPushNotifications = $mmAddonManager.get('$mmaPushNotifications'); - if ($mmaPushNotifications) { - $mmaPushNotifications.updateAddonCounter(siteId, 'mmaMessages', unread); - }*/ + if (this.pushNotificationsProvider) { + this.pushNotificationsProvider.updateAddonCounter('mmaMessages', unread, siteId); + } }).catch(() => { this.badge = ''; }).finally(() => { diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index cfcad478f..ed6e368df 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -14,10 +14,18 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; +import { Badge } from '@ionic-native/badge'; +import { Push, PushObject, PushOptions } from '@ionic-native/push'; +import { Device } from '@ionic-native/device'; import { CoreAppProvider } from '@providers/app'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { AddonPushNotificationsDelegate } from './delegate'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreConfigProvider } from '@providers/config'; +import { CoreConfigConstants } from '.././../../configconstants'; /** * Service to handle push notifications. @@ -28,6 +36,7 @@ export class AddonPushNotificationsProvider { protected logger; protected pushID: string; protected appDB: any; + static COMPONENT = 'mmaPushNotifications'; // Variables for database. protected BADGE_TABLE = 'mma_pushnotifications_badge'; @@ -47,17 +56,64 @@ export class AddonPushNotificationsProvider { name: 'number', type: 'INTEGER' } - ] + ], + primaryKeys: ['siteid', 'addon'] } ]; constructor(logger: CoreLoggerProvider, protected appProvider: CoreAppProvider, private platform: Platform, - protected pushNotificationsDelegate: AddonPushNotificationsDelegate, protected sitesProvider: CoreSitesProvider) { + 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) { this.logger = logger.getInstance('AddonPushNotificationsProvider'); this.appDB = appProvider.getDB(); this.appDB.createTablesFromSchema(this.tablesSchema); } + /** + * Delete all badge records for a given site. + * + * @param {string} siteId Site ID. + * @return {Promise} Resolved when done. + */ + cleanSiteCounters(siteId: string): Promise { + return this.appDB.getRecords(this.BADGE_TABLE, {siteid: siteId} ).then((entries) => { + const promises = []; + entries.forEach((entry) => { + promises.push(this.appDB.remove(this.BADGE_TABLE, { siteid: entry.siteid, addon: entry.addon })); + }); + + return Promise.all(promises); + }).finally(() => { + this.updateAppCounter(); + }); + } + + /** + * Returns options for push notifications based on + * @return {Promise} [description] + */ + protected getOptions(): Promise { + // @TODO: CoreSettingsProvider.NOTIFICATION_SOUND + return this.configProvider.get('CoreSettingsProvider.NOTIFICATION_SOUND', true).then((soundEnabled) => { + return { + android: { + senderID: CoreConfigConstants.gcmpn, + sound: !!soundEnabled + }, + ios: { + alert: 'true', + badge: true, + sound: !!soundEnabled + }, + windows: { + sound: !!soundEnabled + } + }; + }); + } + /** * Get the pushID for this device. * @@ -67,6 +123,16 @@ export class AddonPushNotificationsProvider { return this.pushID; } + /** + * Get Sitebadge counter from the database. + * + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved with the stored badge counter for the site. + */ + getSiteCounter(siteId: string): Promise { + return this.getAddonBadge(siteId); + } + /** * Function called when a push notification is clicked. Redirect the user to the right state. * @@ -77,4 +143,271 @@ export class AddonPushNotificationsProvider { this.pushNotificationsDelegate.clicked(notification); }); } + + /** + * This function is called when we receive a Notification from APNS or a message notification from GCM. + * The app can be in foreground or background, + * if we are in background this code is executed when we open the app clicking in the notification bar. + * + * @param {any} notification Notification received. + */ + onMessageReceived(notification: any): void { + const data = notification ? notification.additionalData : {}; + + this.sitesProvider.getSite(data.site).then(() => { + if (this.utils.isTrueOrOne(data.foreground)) { + // If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves. + if (this.localNotificationsProvider.isAvailable()) { + const localNotif = { + id: 1, + at: new Date(), + data: { + notif: data.notif, + site: data.site + }, + title: '', + text: '' + }, + promises = []; + + // Apply formatText to title and message. + promises.push(this.textUtils.formatText(notification.title, true, true).then((formattedTitle) => { + localNotif.title = formattedTitle; + }).catch(() => { + localNotif.title = notification.title; + })); + + promises.push(this.textUtils.formatText(notification.message, true, true).then((formattedMessage) => { + localNotif.text = formattedMessage; + }).catch(() => { + localNotif.text = notification.message; + })); + + Promise.all(promises).then(() => { + this.localNotificationsProvider.schedule(localNotif, AddonPushNotificationsProvider.COMPONENT, data.site); + }); + } + + // Trigger a notification received event. + this.platform.ready().then(() => { + data.title = notification.title; + data.message = notification.message; + this.pushNotificationsDelegate.received(data); + }); + } else { + // The notification was clicked. For compatibility with old push plugin implementation + // we'll merge all the notification data in a single object. + data.title = notification.title; + data.message = notification.message; + this.notificationClicked(data); + } + }); + } + + /** + * Unregisters a device from a certain Moodle site. + * + * @param {any} site Site to unregister from. + * @return {Promise} Promise resolved when device is unregistered. + */ + unregisterDeviceOnMoodle(site: any): Promise { + if (!site || !this.appProvider.isMobile()) { + return Promise.reject(null); + } + + this.logger.debug(`Unregister device on Moodle: '${site.id}'`); + + const data = { + appid: CoreConfigConstants.app_id, + uuid: this.device.uuid + }; + + return site.write('core_user_remove_user_device', data).then((response) => { + if (!response || !response.removed) { + return Promise.reject(null); + } + }); + } + + /** + * Update Counter for an addon. It will update the refered siteId counter and the total badge. + * It will return the updated addon counter. + * + * @param {string} addon Registered addon name to set the badge number. + * @param {number} value The number to be stored. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with the stored badge counter for the addon on the site. + */ + updateAddonCounter(addon: string, value: number, siteId?: string): Promise { + if (this.pushNotificationsDelegate.isCounterHandlerRegistered(addon)) { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.saveAddonBadge(value, siteId, addon).then(() => { + return this.updateSiteCounter(siteId).then(() => { + return value; + }); + }); + } + + return Promise.resolve(0); + } + + /** + * Update total badge counter of the app. + * + * @return {Promise} Promise resolved with the stored badge counter for the site. + */ + updateAppCounter(): Promise { + return this.sitesProvider.getSitesIds().then((sites) => { + const promises = []; + sites.forEach((siteId) => { + promises.push(this.getAddonBadge(siteId)); + }); + + return Promise.all(promises).then((counters) => { + const total = counters.reduce((previous, counter) => { + // The app badge counter does not support strings, so parse to int before. + return previous + parseInt(counter, 10); + }, 0); + + // Set the app badge. + return this.badge.set(total).then(() => { + return total; + }); + }); + }); + } + + /** + * Update counter for a site using the stored addon data. It will update the total badge application number. + * It will return the updated site counter. + * + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved with the stored badge counter for the site. + */ + updateSiteCounter(siteId: string): Promise { + const addons = this.pushNotificationsDelegate.getCounterHandlers(), + promises = []; + + addons.forEach((addon) => { + promises.push(this.getAddonBadge(siteId, addon)); + }); + + return Promise.all(promises).then((counters) => { + let plus = false, + total = counters.reduce((previous, counter) => { + // Check if there is a plus sign at the end of the counter. + if (counter != parseInt(counter, 10)) { + plus = true; + counter = parseInt(counter, 10); + } + + return previous + counter; + }, 0); + + total = plus && total > 0 ? total + '+' : total; + + // Save the counter on site. + return this.saveAddonBadge(total, siteId); + }).then((siteTotal) => { + return this.updateAppCounter().then(() => { + return siteTotal; + }); + }); + } + + /** + * Register a device in Apple APNS or Google GCM. + * + * @return {Promise} Promise resolved when the device is registered. + */ + registerDevice(): Promise { + try { + // Check if sound is enabled for notifications. + return this.getOptions().then((options) => { + const pushObject: PushObject = this.push.init(options); + + pushObject.on('notification').subscribe((notification: any) => { + this.logger.log('Received a notification', notification); + this.onMessageReceived(notification); + }); + + pushObject.on('registration').subscribe((registrationId: any) => { + this.pushID = registrationId; + this.registerDeviceOnMoodle(); + }); + + pushObject.on('error').subscribe((error: any) => { + this.logger.warn('Error with Push plugin', error); + }); + }); + } catch (ex) { + // Ignore errors. + this.logger.warn(ex); + } + + return Promise.reject(null); + } + + /** + * Registers a device on current Moodle site. + * + * @return {Promise} Promise resolved when device is registered. + */ + registerDeviceOnMoodle(): Promise { + this.logger.debug('Register device on Moodle.'); + + if (!this.sitesProvider.isLoggedIn() || !this.pushID || !this.appProvider.isMobile()) { + return Promise.reject(null); + } + + const data = { + appid: CoreConfigConstants.app_id, + name: this.device.manufacturer || '', + model: this.device.model, + platform: this.device.platform, + version: this.device.version, + pushid: this.pushID, + uuid: this.device.uuid + }; + + return this.sitesProvider.getCurrentSite().write('core_user_add_user_device', data); + } + + /** + * Get the addon/site badge counter from the database. + * + * @param {string} siteId Site ID. + * @param {string} [addon='site'] Registered addon name. If not defined it will store the site total. + * @return {Promise} Promise resolved with the stored badge counter for the addon or site or 0 if none. + */ + protected getAddonBadge(siteId?: string, addon: string = 'site'): Promise { + return this.appDB.getRecord(this.BADGE_TABLE, {siteid: siteId, addon: addon}).then((entry) => { + return (entry && entry.number) || 0; + }).catch(() => { + return 0; + }); + } + + /** + * Save the addon/site badgecounter on the database. + * + * @param {number} value The number to be stored. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @param {string} [addon='site'] Registered addon name. If not defined it will store the site total. + * @return {Promise} Promise resolved with the stored badge counter for the addon or site. + */ + protected saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const entry = { + siteid: siteId, + addon: addon, + number: value + }; + + return this.appDB.insertOrUpdateRecord(this.BADGE_TABLE, entry, {siteid: siteId, addon: addon}).then(() => { + return value; + }); + } } diff --git a/src/addon/pushnotifications/pushnotifications.module.ts b/src/addon/pushnotifications/pushnotifications.module.ts index ba2118a1d..ea2b0a360 100644 --- a/src/addon/pushnotifications/pushnotifications.module.ts +++ b/src/addon/pushnotifications/pushnotifications.module.ts @@ -13,8 +13,11 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { Platform } from 'ionic-angular'; import { AddonPushNotificationsProvider } from './providers/pushnotifications'; import { AddonPushNotificationsDelegate } from './providers/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @NgModule({ declarations: [ @@ -27,5 +30,31 @@ import { AddonPushNotificationsDelegate } from './providers/delegate'; ] }) export class AddonPushNotificationsModule { - constructor() {} + constructor(platform: Platform, pushNotificationsProvider: AddonPushNotificationsProvider, eventsProvider: CoreEventsProvider, + localNotificationsProvider: CoreLocalNotificationsProvider) { + + // Register device on GCM or APNS server. + platform.ready().then(() => { + pushNotificationsProvider.registerDevice(); + }); + + eventsProvider.on(CoreEventsProvider.NOTIFICATION_SOUND_CHANGED, () => { + // Notification sound has changed, register the device again to update the sound setting. + pushNotificationsProvider.registerDevice(); + }); + + // Register device on Moodle site when login. + eventsProvider.on(CoreEventsProvider.LOGIN, () => { + pushNotificationsProvider.registerDeviceOnMoodle(); + }); + + eventsProvider.on(CoreEventsProvider.SITE_DELETED, (site) => { + pushNotificationsProvider.unregisterDeviceOnMoodle(site); + pushNotificationsProvider.cleanSiteCounters(site.id); + }); + + // Listen for local notification clicks (generated by the app). + localNotificationsProvider.registerClick(AddonPushNotificationsProvider.COMPONENT, + pushNotificationsProvider.notificationClicked); + } } diff --git a/src/providers/app.ts b/src/providers/app.ts index 054ef2467..1cac19e4b 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -19,7 +19,7 @@ import { Network } from '@ionic-native/network'; import { CoreDbProvider } from './db'; import { CoreLoggerProvider } from './logger'; -import { SQLiteDB } from '../classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Data stored for a redirect to another page/site. diff --git a/tsconfig.json b/tsconfig.json index 369260cfb..9654b0fb9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,4 +33,4 @@ "atom": { "rewriteTsconfig": false } -} \ No newline at end of file +} diff --git a/webpack.config.js b/webpack.config.js index 1269ea7d1..2118f9cb2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,4 +19,4 @@ const customConfig = { module.exports = { dev: webpackMerge(dev, customConfig), prod: webpackMerge(prod, customConfig), -} \ No newline at end of file +}