MOBILE-2327 messages: Implement push notifications handlers

main
Pau Ferrer Ocaña 2018-02-27 08:39:45 +01:00
parent 2911cbb1aa
commit 2f95ccccf2
8 changed files with 391 additions and 12 deletions

View File

@ -38,9 +38,11 @@
"@angular/http": "5.0.0", "@angular/http": "5.0.0",
"@angular/platform-browser": "5.0.0", "@angular/platform-browser": "5.0.0",
"@angular/platform-browser-dynamic": "5.0.0", "@angular/platform-browser-dynamic": "5.0.0",
"@ionic-native/badge": "^4.5.3",
"@ionic-native/camera": "^4.5.2", "@ionic-native/camera": "^4.5.2",
"@ionic-native/clipboard": "^4.3.2", "@ionic-native/clipboard": "^4.3.2",
"@ionic-native/core": "4.3.0", "@ionic-native/core": "4.3.0",
"@ionic-native/device": "^4.5.3",
"@ionic-native/file": "^4.3.3", "@ionic-native/file": "^4.3.3",
"@ionic-native/file-transfer": "^4.3.3", "@ionic-native/file-transfer": "^4.3.3",
"@ionic-native/globalization": "^4.3.2", "@ionic-native/globalization": "^4.3.2",
@ -49,6 +51,7 @@
"@ionic-native/local-notifications": "^4.4.0", "@ionic-native/local-notifications": "^4.4.0",
"@ionic-native/media-capture": "^4.5.2", "@ionic-native/media-capture": "^4.5.2",
"@ionic-native/network": "^4.3.2", "@ionic-native/network": "^4.3.2",
"@ionic-native/push": "^4.5.3",
"@ionic-native/splash-screen": "4.3.0", "@ionic-native/splash-screen": "4.3.0",
"@ionic-native/sqlite": "^4.3.2", "@ionic-native/sqlite": "^4.3.2",
"@ionic-native/status-bar": "4.3.0", "@ionic-native/status-bar": "4.3.0",

View File

@ -33,6 +33,8 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { CoreSettingsDelegate } from '@core/settings/providers/delegate';
import { AddonMessagesSettingsHandler } from './providers/settings-handler'; import { AddonMessagesSettingsHandler } from './providers/settings-handler';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
import { CoreUtilsProvider } from '@providers/utils/utils';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -59,7 +61,8 @@ export class AddonMessagesModule {
network: Network, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider, network: Network, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider,
localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider, localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider,
sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider,
settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate) { settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate,
pushNotificationsDelegate: AddonPushNotificationsDelegate, utils: CoreUtilsProvider) {
// Register handlers. // Register handlers.
mainMenuDelegate.registerHandler(mainmenuHandler); mainMenuDelegate.registerHandler(mainmenuHandler);
contentLinksDelegate.registerHandler(indexLinkHandler); contentLinksDelegate.registerHandler(indexLinkHandler);
@ -93,5 +96,17 @@ export class AddonMessagesModule {
// Listen for clicks in simulated push notifications. // Listen for clicks in simulated push notifications.
localNotifications.registerClick(AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, notificationClicked); 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;
}
});
}
} }
} }

View File

@ -97,12 +97,11 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => { this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => {
// Leave badge enter if there is a 0+ or a 0. // Leave badge enter if there is a 0+ or a 0.
this.badge = parseInt(unread, 10) > 0 ? unread : ''; this.badge = parseInt(unread, 10) > 0 ? unread : '';
// @todo: use addon manager $mmaPushNotifications = $mmAddonManager.get('$mmaPushNotifications');
// Update badge. // Update badge.
/* if (this.pushNotificationsProvider) {
$mmaPushNotifications = $mmAddonManager.get('$mmaPushNotifications'); this.pushNotificationsProvider.updateAddonCounter('mmaMessages', unread, siteId);
if ($mmaPushNotifications) { }
$mmaPushNotifications.updateAddonCounter(siteId, 'mmaMessages', unread);
}*/
}).catch(() => { }).catch(() => {
this.badge = ''; this.badge = '';
}).finally(() => { }).finally(() => {

View File

@ -14,10 +14,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular'; 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 { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { AddonPushNotificationsDelegate } from './delegate'; 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. * Service to handle push notifications.
@ -28,6 +36,7 @@ export class AddonPushNotificationsProvider {
protected logger; protected logger;
protected pushID: string; protected pushID: string;
protected appDB: any; protected appDB: any;
static COMPONENT = 'mmaPushNotifications';
// Variables for database. // Variables for database.
protected BADGE_TABLE = 'mma_pushnotifications_badge'; protected BADGE_TABLE = 'mma_pushnotifications_badge';
@ -47,17 +56,64 @@ export class AddonPushNotificationsProvider {
name: 'number', name: 'number',
type: 'INTEGER' type: 'INTEGER'
} }
] ],
primaryKeys: ['siteid', 'addon']
} }
]; ];
constructor(logger: CoreLoggerProvider, protected appProvider: CoreAppProvider, private platform: Platform, 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.logger = logger.getInstance('AddonPushNotificationsProvider');
this.appDB = appProvider.getDB(); this.appDB = appProvider.getDB();
this.appDB.createTablesFromSchema(this.tablesSchema); this.appDB.createTablesFromSchema(this.tablesSchema);
} }
/**
* Delete all badge records for a given site.
*
* @param {string} siteId Site ID.
* @return {Promise<any>} Resolved when done.
*/
cleanSiteCounters(siteId: string): Promise<any> {
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<PushOptions>} [description]
*/
protected getOptions(): Promise<PushOptions> {
// @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. * Get the pushID for this device.
* *
@ -67,6 +123,16 @@ export class AddonPushNotificationsProvider {
return this.pushID; return this.pushID;
} }
/**
* Get Sitebadge counter from the database.
*
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved with the stored badge counter for the site.
*/
getSiteCounter(siteId: string): Promise<any> {
return this.getAddonBadge(siteId);
}
/** /**
* Function called when a push notification is clicked. Redirect the user to the right state. * 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.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<any>} Promise resolved when device is unregistered.
*/
unregisterDeviceOnMoodle(site: any): Promise<any> {
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<any>} Promise resolved with the stored badge counter for the addon on the site.
*/
updateAddonCounter(addon: string, value: number, siteId?: string): Promise<any> {
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<any>} Promise resolved with the stored badge counter for the site.
*/
updateAppCounter(): Promise<any> {
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<any>} Promise resolved with the stored badge counter for the site.
*/
updateSiteCounter(siteId: string): Promise<any> {
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<any>} Promise resolved when the device is registered.
*/
registerDevice(): Promise<any> {
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<any>} Promise resolved when device is registered.
*/
registerDeviceOnMoodle(): Promise<any> {
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<any>} Promise resolved with the stored badge counter for the addon or site or 0 if none.
*/
protected getAddonBadge(siteId?: string, addon: string = 'site'): Promise<any> {
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<any>} Promise resolved with the stored badge counter for the addon or site.
*/
protected saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise<any> {
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;
});
}
} }

View File

@ -13,8 +13,11 @@
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Platform } from 'ionic-angular';
import { AddonPushNotificationsProvider } from './providers/pushnotifications'; import { AddonPushNotificationsProvider } from './providers/pushnotifications';
import { AddonPushNotificationsDelegate } from './providers/delegate'; import { AddonPushNotificationsDelegate } from './providers/delegate';
import { CoreEventsProvider } from '@providers/events';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -27,5 +30,31 @@ import { AddonPushNotificationsDelegate } from './providers/delegate';
] ]
}) })
export class AddonPushNotificationsModule { 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);
}
} }

View File

@ -19,7 +19,7 @@ import { Network } from '@ionic-native/network';
import { CoreDbProvider } from './db'; import { CoreDbProvider } from './db';
import { CoreLoggerProvider } from './logger'; import { CoreLoggerProvider } from './logger';
import { SQLiteDB } from '../classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
/** /**
* Data stored for a redirect to another page/site. * Data stored for a redirect to another page/site.

View File

@ -33,4 +33,4 @@
"atom": { "atom": {
"rewriteTsconfig": false "rewriteTsconfig": false
} }
} }

View File

@ -19,4 +19,4 @@ const customConfig = {
module.exports = { module.exports = {
dev: webpackMerge(dev, customConfig), dev: webpackMerge(dev, customConfig),
prod: webpackMerge(prod, customConfig), prod: webpackMerge(prod, customConfig),
} }