MOBILE-3662 pushnotifications: Implement services
parent
53c21009c3
commit
6ec641fa98
|
@ -24,6 +24,7 @@ import { CoreSettingsModule } from './settings/settings.module';
|
||||||
import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
||||||
import { CoreTagModule } from './tag/tag.module';
|
import { CoreTagModule } from './tag/tag.module';
|
||||||
import { CoreUserModule } from './user/user.module';
|
import { CoreUserModule } from './user/user.module';
|
||||||
|
import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -37,6 +38,7 @@ import { CoreUserModule } from './user/user.module';
|
||||||
CoreSiteHomeModule,
|
CoreSiteHomeModule,
|
||||||
CoreTagModule,
|
CoreTagModule,
|
||||||
CoreUserModule,
|
CoreUserModule,
|
||||||
|
CorePushNotificationsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreFeaturesModule {}
|
export class CoreFeaturesModule {}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { CorePushNotifications } from './services/pushnotifications';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => async () => {
|
||||||
|
await CorePushNotifications.instance.initialize();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CorePushNotificationsModule {}
|
|
@ -0,0 +1,143 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreAppSchema } from '@services/app';
|
||||||
|
import { CoreSiteSchema } from '@services/sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for CorePushNotificationsProvider service.
|
||||||
|
* Keep "addon" in some names for backwards compatibility.
|
||||||
|
*/
|
||||||
|
export const BADGE_TABLE_NAME = 'addon_pushnotifications_badge';
|
||||||
|
export const PENDING_UNREGISTER_TABLE_NAME = 'addon_pushnotifications_pending_unregister';
|
||||||
|
export const REGISTERED_DEVICES_TABLE_NAME = 'addon_pushnotifications_registered_devices';
|
||||||
|
export const APP_SCHEMA: CoreAppSchema = {
|
||||||
|
name: 'CorePushNotificationsProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: BADGE_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'siteid',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'addon',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['siteid', 'addon'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: PENDING_UNREGISTER_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'siteid',
|
||||||
|
type: 'TEXT',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'siteurl',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'token',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'AddonPushNotificationsProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: REGISTERED_DEVICES_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'appid',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uuid',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'platform',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'version',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pushid',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['appid', 'uuid'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data stored in DB for badge.
|
||||||
|
*/
|
||||||
|
export type CorePushNotificationsBadgeDBRecord = {
|
||||||
|
siteid: string;
|
||||||
|
addon: string;
|
||||||
|
number: number; // eslint-disable-line id-blacklist
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data stored in DB for pending unregisters.
|
||||||
|
*/
|
||||||
|
export type CorePushNotificationsPendingUnregisterDBRecord = {
|
||||||
|
siteid: string;
|
||||||
|
siteurl: string;
|
||||||
|
token: string;
|
||||||
|
info: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data stored in DB for registered devices.
|
||||||
|
*/
|
||||||
|
export type CorePushNotificationsRegisteredDeviceDBRecord = {
|
||||||
|
appid: string; // App ID.
|
||||||
|
uuid: string; // Device UUID.
|
||||||
|
name: string; // Device name.
|
||||||
|
model: string; // Device model.
|
||||||
|
platform: string; // Device platform.
|
||||||
|
version: string; // Device version.
|
||||||
|
pushid: string; // Push ID.
|
||||||
|
};
|
|
@ -0,0 +1,218 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { CorePushNotificationsNotificationBasicData } from './pushnotifications';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that all click handlers must implement.
|
||||||
|
*/
|
||||||
|
export interface CorePushNotificationsClickHandler {
|
||||||
|
/**
|
||||||
|
* A name to identify the handler.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler's priority. The highest priority is treated first.
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the feature this handler is related to.
|
||||||
|
* It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled).
|
||||||
|
*/
|
||||||
|
featureName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a notification click is handled by this handler.
|
||||||
|
*
|
||||||
|
* @param notification The notification to check.
|
||||||
|
* @return Whether the notification click is handled by this handler.
|
||||||
|
*/
|
||||||
|
handles(notification: CorePushNotificationsNotificationBasicData): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the notification click.
|
||||||
|
*
|
||||||
|
* @param notification The notification to check.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
handleClick(notification: CorePushNotificationsNotificationBasicData): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle push notifications actions to perform when clicked and received.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CorePushNotificationsDelegateService {
|
||||||
|
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
protected observables: { [s: string]: Subject<unknown> } = {};
|
||||||
|
protected clickHandlers: { [s: string]: CorePushNotificationsClickHandler } = {};
|
||||||
|
protected counterHandlers: Record<string, string> = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger = CoreLogger.getInstance('CorePushNotificationsDelegate');
|
||||||
|
this.observables['receive'] = new Subject<CorePushNotificationsNotificationBasicData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when a push notification is clicked. Sends notification to handlers.
|
||||||
|
*
|
||||||
|
* @param notification Notification clicked.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async clicked(notification: CorePushNotificationsNotificationBasicData): Promise<void> {
|
||||||
|
if (!notification) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handlers: CorePushNotificationsClickHandler[] = [];
|
||||||
|
|
||||||
|
const promises = Object.values(this.clickHandlers).map(async (handler) => {
|
||||||
|
// Check if the handler is disabled for the site.
|
||||||
|
const disabled = await this.isFeatureDisabled(handler, notification.site);
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the handler handles the notification.
|
||||||
|
const handles = await handler.handles(notification);
|
||||||
|
if (handles) {
|
||||||
|
handlers.push(handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(promises));
|
||||||
|
|
||||||
|
// Sort by priority.
|
||||||
|
handlers = handlers.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
|
||||||
|
|
||||||
|
// Execute the first one.
|
||||||
|
handlers[0]?.handleClick(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a handler's feature is disabled for a certain site.
|
||||||
|
*
|
||||||
|
* @param handler Handler to check.
|
||||||
|
* @param siteId The site ID to check.
|
||||||
|
* @return Promise resolved with boolean: whether the handler feature is disabled.
|
||||||
|
*/
|
||||||
|
protected async isFeatureDisabled(handler: CorePushNotificationsClickHandler, siteId?: string): Promise<boolean> {
|
||||||
|
if (!siteId) {
|
||||||
|
// Notification doesn't belong to a site. Assume all handlers are enabled.
|
||||||
|
return false;
|
||||||
|
} else if (handler.featureName) {
|
||||||
|
// Check if the feature is disabled.
|
||||||
|
return CoreSites.instance.isFeatureDisabled(handler.featureName, siteId);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when a push notification is received in foreground (cannot tell when it's received in background).
|
||||||
|
* Sends notification to all handlers.
|
||||||
|
*
|
||||||
|
* @param notification Notification received.
|
||||||
|
*/
|
||||||
|
received(notification: CorePushNotificationsNotificationBasicData): void {
|
||||||
|
this.observables['receive'].next(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a push notifications observable for a certain event. Right now, only receive is supported.
|
||||||
|
* let observer = pushNotificationsDelegate.on('receive').subscribe((notification) => {
|
||||||
|
* ...
|
||||||
|
* observer.unsuscribe();
|
||||||
|
*
|
||||||
|
* @param eventName Only receive is permitted.
|
||||||
|
* @return Observer to subscribe.
|
||||||
|
*/
|
||||||
|
on<T = unknown>(eventName: string): Subject<T> {
|
||||||
|
if (typeof this.observables[eventName] == 'undefined') {
|
||||||
|
const eventNames = Object.keys(this.observables).join(', ');
|
||||||
|
this.logger.warn(`'${eventName}' event name is not allowed. Use one of the following: '${eventNames}'.`);
|
||||||
|
|
||||||
|
return new Subject<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Subject<T>> this.observables[eventName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a click handler.
|
||||||
|
*
|
||||||
|
* @param handler The handler to register.
|
||||||
|
* @return True if registered successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
registerClickHandler(handler: CorePushNotificationsClickHandler): boolean {
|
||||||
|
if (typeof this.clickHandlers[handler.name] !== 'undefined') {
|
||||||
|
this.logger.log(`Addon '${handler.name}' already registered`);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Registered addon '${handler.name}'`);
|
||||||
|
this.clickHandlers[handler.name] = handler;
|
||||||
|
handler.priority = handler.priority || 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a push notifications handler for update badge counter.
|
||||||
|
*
|
||||||
|
* @param name Handler's name.
|
||||||
|
*/
|
||||||
|
registerCounterHandler(name: string): void {
|
||||||
|
if (typeof this.counterHandlers[name] == 'undefined') {
|
||||||
|
this.logger.debug(`Registered handler '${name}' as badge counter handler.`);
|
||||||
|
this.counterHandlers[name] = name;
|
||||||
|
} else {
|
||||||
|
this.logger.log(`Handler '${name}' as badge counter handler already registered.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a counter handler is present.
|
||||||
|
*
|
||||||
|
* @param name Handler's name.
|
||||||
|
* @return If handler name is present.
|
||||||
|
*/
|
||||||
|
isCounterHandlerRegistered(name: string): boolean {
|
||||||
|
return typeof this.counterHandlers[name] != 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all counter badge handlers.
|
||||||
|
*
|
||||||
|
* @return with all the handler names.
|
||||||
|
*/
|
||||||
|
getCounterHandlers(): Record<string, string> {
|
||||||
|
return this.counterHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CorePushNotificationsDelegate extends makeSingleton(CorePushNotificationsDelegateService) {}
|
|
@ -0,0 +1,910 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ILocalNotification } from '@ionic-native/local-notifications';
|
||||||
|
import { NotificationEventResponse, PushOptions, RegistrationEventResponse } from '@ionic-native/push/ngx';
|
||||||
|
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CorePushNotificationsDelegate } from './push-delegate';
|
||||||
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreConfig } from '@services/config';
|
||||||
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
|
import { CoreSite, CoreSiteInfo } from '@classes/site';
|
||||||
|
import { makeSingleton, Badge, Push, Device, Translate, Platform, ApplicationInit, NgZone } from '@singletons';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
APP_SCHEMA,
|
||||||
|
BADGE_TABLE_NAME,
|
||||||
|
PENDING_UNREGISTER_TABLE_NAME,
|
||||||
|
REGISTERED_DEVICES_TABLE_NAME,
|
||||||
|
CorePushNotificationsPendingUnregisterDBRecord,
|
||||||
|
CorePushNotificationsRegisteredDeviceDBRecord,
|
||||||
|
CorePushNotificationsBadgeDBRecord,
|
||||||
|
} from './database/pushnotifications';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle push notifications.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CorePushNotificationsProvider {
|
||||||
|
|
||||||
|
static readonly COMPONENT = 'CorePushNotificationsProvider';
|
||||||
|
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
protected pushID?: string;
|
||||||
|
|
||||||
|
// Variables for DB.
|
||||||
|
protected appDB: Promise<SQLiteDB>;
|
||||||
|
protected resolveAppDB!: (appDB: SQLiteDB) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
||||||
|
this.logger = CoreLogger.getInstance('CorePushNotificationsProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.initializeDatabase(),
|
||||||
|
this.initializeDefaultChannel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Now register the device to receive push notifications. Don't block for this.
|
||||||
|
this.registerDevice();
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.NOTIFICATION_SOUND_CHANGED, () => {
|
||||||
|
// Notification sound has changed, register the device again to update the sound setting.
|
||||||
|
this.registerDevice();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register device on Moodle site when login.
|
||||||
|
CoreEvents.on(CoreEvents.LOGIN, async () => {
|
||||||
|
try {
|
||||||
|
await this.registerDeviceOnMoodle();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Can\'t register device', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SITE_DELETED, async (site: CoreSite) => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.unregisterDeviceOnMoodle(site),
|
||||||
|
this.cleanSiteCounters(site.getId()),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Can\'t unregister device', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for local notification clicks (generated by the app).
|
||||||
|
CoreLocalNotifications.instance.registerClick<CorePushNotificationsNotificationBasicData>(
|
||||||
|
CorePushNotificationsProvider.COMPONENT,
|
||||||
|
(notification) => {
|
||||||
|
// Log notification open event.
|
||||||
|
this.logEvent('moodle_notification_open', notification, true);
|
||||||
|
|
||||||
|
this.notificationClicked(notification);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for local notification dismissed events.
|
||||||
|
CoreLocalNotifications.instance.registerObserver<CorePushNotificationsNotificationBasicData>(
|
||||||
|
'clear',
|
||||||
|
CorePushNotificationsProvider.COMPONENT,
|
||||||
|
(notification) => {
|
||||||
|
// Log notification dismissed event.
|
||||||
|
this.logEvent('moodle_notification_dismiss', notification, true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the default channel for Android.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async initializeDefaultChannel(): Promise<void> {
|
||||||
|
await Platform.instance.ready();
|
||||||
|
|
||||||
|
// Create the default channel.
|
||||||
|
this.createDefaultChannel();
|
||||||
|
|
||||||
|
Translate.instance.onLangChange.subscribe(() => {
|
||||||
|
// Update the channel name.
|
||||||
|
this.createDefaultChannel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async initializeDatabase(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await CoreApp.instance.createTablesFromSchema(APP_SCHEMA);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resolveAppDB(CoreApp.instance.getDB());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the device can be registered in Moodle to receive push notifications.
|
||||||
|
*
|
||||||
|
* @return Whether the device can be registered in Moodle.
|
||||||
|
*/
|
||||||
|
canRegisterOnMoodle(): boolean {
|
||||||
|
return !!this.pushID && CoreApp.instance.isMobile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all badge records for a given site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async cleanSiteCounters(siteId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const db = await this.appDB;
|
||||||
|
await db.deleteRecords(BADGE_TABLE_NAME, { siteid: siteId } );
|
||||||
|
} finally {
|
||||||
|
this.updateAppCounter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the default push channel. It is used to change the name.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async createDefaultChannel(): Promise<void> {
|
||||||
|
if (!CoreApp.instance.isAndroid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Push.instance.createChannel({
|
||||||
|
id: 'PushPluginChannel',
|
||||||
|
description: Translate.instance.instant('core.misc'),
|
||||||
|
importance: 4,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error changing push channel name', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable Firebase analytics.
|
||||||
|
*
|
||||||
|
* @param enable Whether to enable or disable.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async enableAnalytics(enable: boolean): Promise<void> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const win = <any> window; // This feature is only present in our fork of the plugin.
|
||||||
|
|
||||||
|
if (!CoreConstants.CONFIG.enableanalytics || !win.PushNotification?.enableAnalytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deferred = CoreUtils.instance.promiseDefer<void>();
|
||||||
|
|
||||||
|
win.PushNotification.enableAnalytics(deferred.resolve, (error) => {
|
||||||
|
this.logger.error('Error enabling or disabling Firebase analytics', enable, error);
|
||||||
|
deferred.resolve();
|
||||||
|
}, !!enable);
|
||||||
|
|
||||||
|
await deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns options for push notifications based on device.
|
||||||
|
*
|
||||||
|
* @return Promise with the push options resolved when done.
|
||||||
|
*/
|
||||||
|
protected async getOptions(): Promise<PushOptions> {
|
||||||
|
let soundEnabled = true;
|
||||||
|
|
||||||
|
if (CoreLocalNotifications.instance.canDisableSound()) {
|
||||||
|
soundEnabled = await CoreConfig.instance.get<boolean>(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
android: {
|
||||||
|
sound: !!soundEnabled,
|
||||||
|
icon: 'smallicon',
|
||||||
|
iconColor: CoreConstants.CONFIG.notificoncolor,
|
||||||
|
},
|
||||||
|
ios: {
|
||||||
|
alert: 'true',
|
||||||
|
badge: true,
|
||||||
|
sound: !!soundEnabled,
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
sound: !!soundEnabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the pushID for this device.
|
||||||
|
*
|
||||||
|
* @return Push ID.
|
||||||
|
*/
|
||||||
|
getPushId(): string | undefined {
|
||||||
|
return this.pushID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data to register the device in Moodle.
|
||||||
|
*
|
||||||
|
* @return Data.
|
||||||
|
*/
|
||||||
|
protected getRegisterData(): CoreUserAddUserDeviceWSParams {
|
||||||
|
if (!this.pushID) {
|
||||||
|
throw new CoreError('Cannot get register data because pushID is not set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appid: CoreConstants.CONFIG.app_id,
|
||||||
|
name: Device.instance.manufacturer || '',
|
||||||
|
model: Device.instance.model,
|
||||||
|
platform: Device.instance.platform + '-fcm',
|
||||||
|
version: Device.instance.version,
|
||||||
|
pushid: this.pushID,
|
||||||
|
uuid: Device.instance.uuid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Sitebadge counter from the database.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved with the stored badge counter for the site.
|
||||||
|
*/
|
||||||
|
getSiteCounter(siteId: string): Promise<number> {
|
||||||
|
return this.getAddonBadge(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a firebase event.
|
||||||
|
*
|
||||||
|
* @param name Name of the event.
|
||||||
|
* @param data Data of the event.
|
||||||
|
* @param filter Whether to filter the data. This is useful when logging a full notification.
|
||||||
|
* @return Promise resolved when done. This promise is never rejected.
|
||||||
|
*/
|
||||||
|
async logEvent(name: string, data: Record<string, unknown>, filter?: boolean): Promise<void> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const win = <any> window; // This feature is only present in our fork of the plugin.
|
||||||
|
|
||||||
|
if (!CoreConstants.CONFIG.enableanalytics || !win.PushNotification?.logEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the analytics is enabled by the user.
|
||||||
|
const enabled = await CoreConfig.instance.get<boolean>(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true);
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deferred = CoreUtils.instance.promiseDefer<void>();
|
||||||
|
|
||||||
|
win.PushNotification.logEvent(deferred.resolve, (error) => {
|
||||||
|
this.logger.error('Error logging firebase event', name, error);
|
||||||
|
deferred.resolve();
|
||||||
|
}, name, data, !!filter);
|
||||||
|
|
||||||
|
await deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a firebase view_item event.
|
||||||
|
*
|
||||||
|
* @param itemId The item ID.
|
||||||
|
* @param itemName The item name.
|
||||||
|
* @param itemCategory The item category.
|
||||||
|
* @param wsName Name of the WS.
|
||||||
|
* @param data Other data to pass to the event.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done. This promise is never rejected.
|
||||||
|
*/
|
||||||
|
logViewEvent(
|
||||||
|
itemId: number | string | undefined,
|
||||||
|
itemName: string | undefined,
|
||||||
|
itemCategory: string | undefined,
|
||||||
|
wsName: string,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
data = data || {};
|
||||||
|
|
||||||
|
// Add "moodle" to the name of all extra params.
|
||||||
|
data = CoreUtils.instance.prefixKeys(data, 'moodle');
|
||||||
|
data.moodleaction = wsName;
|
||||||
|
data.moodlesiteid = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (itemId) {
|
||||||
|
data.item_id = itemId;
|
||||||
|
}
|
||||||
|
if (itemName) {
|
||||||
|
data.item_name = itemName;
|
||||||
|
}
|
||||||
|
if (itemCategory) {
|
||||||
|
data.item_category = itemCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.logEvent('view_item', data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a firebase view_item_list event.
|
||||||
|
*
|
||||||
|
* @param itemCategory The item category.
|
||||||
|
* @param wsName Name of the WS.
|
||||||
|
* @param data Other data to pass to the event.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done. This promise is never rejected.
|
||||||
|
*/
|
||||||
|
logViewListEvent(itemCategory: string, wsName: string, data?: Record<string, unknown>, siteId?: string): Promise<void> {
|
||||||
|
data = data || {};
|
||||||
|
|
||||||
|
// Add "moodle" to the name of all extra params.
|
||||||
|
data = CoreUtils.instance.prefixKeys(data, 'moodle');
|
||||||
|
data.moodleaction = wsName;
|
||||||
|
data.moodlesiteid = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (itemCategory) {
|
||||||
|
data.item_category = itemCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.logEvent('view_item_list', data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when a push notification is clicked. Redirect the user to the right state.
|
||||||
|
*
|
||||||
|
* @param notification Notification.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise<void> {
|
||||||
|
await ApplicationInit.instance.donePromise;
|
||||||
|
|
||||||
|
CorePushNotificationsDelegate.instance.clicked(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 notification Notification received.
|
||||||
|
*/
|
||||||
|
async onMessageReceived(notification: NotificationEventResponse): Promise<void> {
|
||||||
|
const rawData: CorePushNotificationsNotificationBasicRawData = notification ? notification.additionalData : {};
|
||||||
|
|
||||||
|
// Parse some fields and add some extra data.
|
||||||
|
const data: CorePushNotificationsNotificationBasicData = Object.assign(rawData, {
|
||||||
|
title: notification.title,
|
||||||
|
message: notification.message,
|
||||||
|
customdata: typeof rawData.customdata == 'string' ?
|
||||||
|
CoreTextUtils.instance.parseJSON<Record<string, unknown>>(rawData.customdata, {}) : rawData.customdata,
|
||||||
|
});
|
||||||
|
|
||||||
|
let site: CoreSite | undefined;
|
||||||
|
|
||||||
|
if (data.site) {
|
||||||
|
site = await CoreSites.instance.getSite(data.site);
|
||||||
|
} else if (data.siteurl) {
|
||||||
|
site = await CoreSites.instance.getSiteByUrl(data.siteurl);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.site = site?.getId();
|
||||||
|
|
||||||
|
if (!CoreUtils.instance.isTrueOrOne(data.foreground)) {
|
||||||
|
// The notification was clicked.
|
||||||
|
return this.notificationClicked(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves.
|
||||||
|
if (!CoreLocalNotifications.instance.isAvailable()) {
|
||||||
|
return this.notifyReceived(notification, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const localNotif: ILocalNotification = {
|
||||||
|
id: Number(data.notId) || 1,
|
||||||
|
data: data,
|
||||||
|
title: notification.title,
|
||||||
|
text: notification.message,
|
||||||
|
channel: 'PushPluginChannel',
|
||||||
|
};
|
||||||
|
const isAndroid = CoreApp.instance.isAndroid();
|
||||||
|
const extraFeatures = CoreUtils.instance.isTrueOrOne(data.extrafeatures);
|
||||||
|
|
||||||
|
if (extraFeatures && isAndroid && CoreUtils.instance.isFalseOrZero(data.notif)) {
|
||||||
|
// It's a message, use messaging style. Ionic Native doesn't specify this option.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(<any> localNotif).text = [
|
||||||
|
{
|
||||||
|
message: notification.message,
|
||||||
|
person: data.conversationtype == '2' ? data.userfromfullname : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraFeatures && isAndroid) {
|
||||||
|
// Use a different icon if needed.
|
||||||
|
localNotif.icon = notification.image;
|
||||||
|
// This feature isn't supported by the official plugin, we use a fork.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(<any> localNotif).iconType = data['image-type'];
|
||||||
|
|
||||||
|
localNotif.summary = data.summaryText;
|
||||||
|
|
||||||
|
if (data.picture) {
|
||||||
|
localNotif.attachments = [data.picture];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreLocalNotifications.instance.schedule(localNotif, CorePushNotificationsProvider.COMPONENT, data.site || '', true);
|
||||||
|
|
||||||
|
await this.notifyReceived(notification, data);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify that a notification was received.
|
||||||
|
*
|
||||||
|
* @param notification Notification.
|
||||||
|
* @param data Notification data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async notifyReceived(
|
||||||
|
notification: NotificationEventResponse,
|
||||||
|
data: CorePushNotificationsNotificationBasicData,
|
||||||
|
): Promise<void> {
|
||||||
|
await ApplicationInit.instance.donePromise;
|
||||||
|
|
||||||
|
CorePushNotificationsDelegate.instance.received(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a device from a certain Moodle site.
|
||||||
|
*
|
||||||
|
* @param site Site to unregister from.
|
||||||
|
* @return Promise resolved when device is unregistered.
|
||||||
|
*/
|
||||||
|
async unregisterDeviceOnMoodle(site: CoreSite): Promise<void> {
|
||||||
|
if (!site || !CoreApp.instance.isMobile()) {
|
||||||
|
throw new CoreError('Cannot unregister device');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Unregister device on Moodle: '${site.getId()}'`);
|
||||||
|
|
||||||
|
const db = await this.appDB;
|
||||||
|
const data: CoreUserRemoveUserDeviceWSParams = {
|
||||||
|
appid: CoreConstants.CONFIG.app_id,
|
||||||
|
uuid: Device.instance.uuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await site.write<CoreUserRemoveUserDeviceWSResponse>('core_user_remove_user_device', data);
|
||||||
|
|
||||||
|
if (!response || !response.removed) {
|
||||||
|
throw new CoreError('Cannot unregister device');
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(Promise.all([
|
||||||
|
// Remove the device from the local DB.
|
||||||
|
site.getDb().deleteRecords(REGISTERED_DEVICES_TABLE_NAME, this.getRegisterData()),
|
||||||
|
// Remove pending unregisters for this site.
|
||||||
|
db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, { siteid: site.getId() }),
|
||||||
|
]));
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.instance.isWebServiceError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the pending unregister so it's retried again later.
|
||||||
|
const entry: CorePushNotificationsPendingUnregisterDBRecord = {
|
||||||
|
siteid: site.getId(),
|
||||||
|
siteurl: site.getURL(),
|
||||||
|
token: site.getToken(),
|
||||||
|
info: JSON.stringify(site.getInfo()),
|
||||||
|
};
|
||||||
|
await db.insertRecord(PENDING_UNREGISTER_TABLE_NAME, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Counter for an addon. It will update the refered siteId counter and the total badge.
|
||||||
|
* It will return the updated addon counter.
|
||||||
|
*
|
||||||
|
* @param addon Registered addon name to set the badge number.
|
||||||
|
* @param value The number to be stored.
|
||||||
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
|
* @return Promise resolved with the stored badge counter for the addon on the site.
|
||||||
|
*/
|
||||||
|
async updateAddonCounter(addon: string, value: number, siteId?: string): Promise<number> {
|
||||||
|
if (!CorePushNotificationsDelegate.instance.isCounterHandlerRegistered(addon)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await this.saveAddonBadge(value, siteId, addon);
|
||||||
|
await this.updateSiteCounter(siteId);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update total badge counter of the app.
|
||||||
|
*
|
||||||
|
* @return Promise resolved with the stored badge counter for the site.
|
||||||
|
*/
|
||||||
|
async updateAppCounter(): Promise<number> {
|
||||||
|
const sitesIds = await CoreSites.instance.getSitesIds();
|
||||||
|
|
||||||
|
const counters = await Promise.all(sitesIds.map((siteId) => this.getAddonBadge(siteId)));
|
||||||
|
|
||||||
|
const total = counters.reduce((previous, counter) => previous + counter, 0);
|
||||||
|
|
||||||
|
if (!CoreApp.instance.isMobile()) {
|
||||||
|
// Browser doesn't have an app badge, stop.
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the app badge.
|
||||||
|
await Badge.instance.set(total);
|
||||||
|
|
||||||
|
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 siteId Site ID.
|
||||||
|
* @return Promise resolved with the stored badge counter for the site.
|
||||||
|
*/
|
||||||
|
async updateSiteCounter(siteId: string): Promise<number> {
|
||||||
|
const addons = CorePushNotificationsDelegate.instance.getCounterHandlers();
|
||||||
|
|
||||||
|
const counters = await Promise.all(Object.values(addons).map((addon) => this.getAddonBadge(siteId, addon)));
|
||||||
|
|
||||||
|
const total = counters.reduce((previous, counter) => previous + counter, 0);
|
||||||
|
|
||||||
|
// Save the counter on site.
|
||||||
|
await this.saveAddonBadge(total, siteId);
|
||||||
|
|
||||||
|
await this.updateAppCounter();
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a device in Apple APNS or Google GCM.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when the device is registered.
|
||||||
|
*/
|
||||||
|
async registerDevice(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if sound is enabled for notifications.
|
||||||
|
const options = await this.getOptions();
|
||||||
|
|
||||||
|
const pushObject = Push.instance.init(options);
|
||||||
|
|
||||||
|
pushObject.on('notification').subscribe((notification: NotificationEventResponse) => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.instance.run(() => {
|
||||||
|
this.logger.log('Received a notification', notification);
|
||||||
|
this.onMessageReceived(notification);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pushObject.on('registration').subscribe((data: RegistrationEventResponse) => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.instance.run(() => {
|
||||||
|
this.pushID = data.registrationId;
|
||||||
|
if (!CoreSites.instance.isLoggedIn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerDeviceOnMoodle().catch((error) => {
|
||||||
|
this.logger.warn('Can\'t register device', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pushObject.on('error').subscribe((error: Error) => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.instance.run(() => {
|
||||||
|
this.logger.warn('Error with Push plugin', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(error);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a device on a Moodle site if needed.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param forceUnregister Whether to force unregister and register.
|
||||||
|
* @return Promise resolved when device is registered.
|
||||||
|
*/
|
||||||
|
async registerDeviceOnMoodle(siteId?: string, forceUnregister?: boolean): Promise<void> {
|
||||||
|
this.logger.debug('Register device on Moodle.');
|
||||||
|
|
||||||
|
if (!this.canRegisterOnMoodle()) {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const data = this.getRegisterData();
|
||||||
|
let result = {
|
||||||
|
unregister: true,
|
||||||
|
register: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!forceUnregister) {
|
||||||
|
// Check if the device is already registered.
|
||||||
|
result = await this.shouldRegister(data, site);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.unregister) {
|
||||||
|
// Unregister the device first.
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.unregisterDeviceOnMoodle(site));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.register) {
|
||||||
|
// Now register the device.
|
||||||
|
await site.write('core_user_add_user_device', CoreUtils.instance.clone(data));
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId());
|
||||||
|
|
||||||
|
// Insert the device in the local DB.
|
||||||
|
try {
|
||||||
|
await site.getDb().insertRecord(REGISTERED_DEVICES_TABLE_NAME, data);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Remove pending unregisters for this site.
|
||||||
|
const db = await this.appDB;
|
||||||
|
await CoreUtils.instance.ignoreErrors(db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, { siteid: site.getId() }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the addon/site badge counter from the database.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @param addon Registered addon name. If not defined it will store the site total.
|
||||||
|
* @return Promise resolved with the stored badge counter for the addon or site or 0 if none.
|
||||||
|
*/
|
||||||
|
protected async getAddonBadge(siteId?: string, addon: string = 'site'): Promise<number> {
|
||||||
|
try {
|
||||||
|
const db = await this.appDB;
|
||||||
|
const entry = await db.getRecord<CorePushNotificationsBadgeDBRecord>(BADGE_TABLE_NAME, { siteid: siteId, addon });
|
||||||
|
|
||||||
|
return entry?.number || 0;
|
||||||
|
} catch (err) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry pending unregisters.
|
||||||
|
*
|
||||||
|
* @param siteId If defined, retry only for that site if needed. Otherwise, retry all pending unregisters.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async retryUnregisters(siteId?: string): Promise<void> {
|
||||||
|
|
||||||
|
const db = await this.appDB;
|
||||||
|
let results: CorePushNotificationsPendingUnregisterDBRecord[];
|
||||||
|
|
||||||
|
if (siteId) {
|
||||||
|
// Check if the site has a pending unregister.
|
||||||
|
results = await db.getRecords<CorePushNotificationsPendingUnregisterDBRecord>(PENDING_UNREGISTER_TABLE_NAME, {
|
||||||
|
siteid: siteId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get all pending unregisters.
|
||||||
|
results = await db.getAllRecords<CorePushNotificationsPendingUnregisterDBRecord>(PENDING_UNREGISTER_TABLE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(results.map(async (result) => {
|
||||||
|
// Create a temporary site to unregister.
|
||||||
|
const tmpSite = new CoreSite(
|
||||||
|
result.siteid,
|
||||||
|
result.siteurl,
|
||||||
|
result.token,
|
||||||
|
CoreTextUtils.instance.parseJSON<CoreSiteInfo | null>(result.info, null) || undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.unregisterDeviceOnMoodle(tmpSite);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the addon/site badgecounter on the database.
|
||||||
|
*
|
||||||
|
* @param value The number to be stored.
|
||||||
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
|
* @param addon Registered addon name. If not defined it will store the site total.
|
||||||
|
* @return Promise resolved with the stored badge counter for the addon or site.
|
||||||
|
*/
|
||||||
|
protected async saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise<number> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
const entry: CorePushNotificationsBadgeDBRecord = {
|
||||||
|
siteid: siteId,
|
||||||
|
addon,
|
||||||
|
number: value, // eslint-disable-line id-blacklist
|
||||||
|
};
|
||||||
|
|
||||||
|
const db = await this.appDB;
|
||||||
|
await db.insertRecord(BADGE_TABLE_NAME, entry);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device should be registered (and unregistered first).
|
||||||
|
*
|
||||||
|
* @param data Data of the device.
|
||||||
|
* @param site Site to use.
|
||||||
|
* @return Promise resolved with booleans: whether to register/unregister.
|
||||||
|
*/
|
||||||
|
protected async shouldRegister(
|
||||||
|
data: CoreUserAddUserDeviceWSParams,
|
||||||
|
site: CoreSite,
|
||||||
|
): Promise<{register: boolean; unregister: boolean}> {
|
||||||
|
|
||||||
|
// Check if the device is already registered.
|
||||||
|
const records = await CoreUtils.instance.ignoreErrors(
|
||||||
|
site.getDb().getRecords<CorePushNotificationsRegisteredDeviceDBRecord>(REGISTERED_DEVICES_TABLE_NAME, {
|
||||||
|
appid: data.appid,
|
||||||
|
uuid: data.uuid,
|
||||||
|
name: data.name,
|
||||||
|
model: data.model,
|
||||||
|
platform: data.platform,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let isStored = false;
|
||||||
|
let versionOrPushChanged = false;
|
||||||
|
|
||||||
|
(records || []).forEach((record) => {
|
||||||
|
if (record.version == data.version && record.pushid == data.pushid) {
|
||||||
|
// The device is already stored.
|
||||||
|
isStored = true;
|
||||||
|
} else {
|
||||||
|
// The version or pushid has changed.
|
||||||
|
versionOrPushChanged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isStored) {
|
||||||
|
// The device has already been registered, no need to register it again.
|
||||||
|
return {
|
||||||
|
register: false,
|
||||||
|
unregister: false,
|
||||||
|
};
|
||||||
|
} else if (versionOrPushChanged) {
|
||||||
|
// This data can be updated by calling register WS, no need to call unregister.
|
||||||
|
return {
|
||||||
|
register: true,
|
||||||
|
unregister: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
register: true,
|
||||||
|
unregister: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CorePushNotifications extends makeSingleton(CorePushNotificationsProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional data sent in push notifications.
|
||||||
|
*/
|
||||||
|
export type CorePushNotificationsNotificationBasicRawData = {
|
||||||
|
customdata?: string; // Custom data.
|
||||||
|
extrafeatures?: string; // "1" if the notification uses extrafeatures, "0" otherwise.
|
||||||
|
foreground?: boolean; // Whether the app was in foreground.
|
||||||
|
'image-type'?: string; // How to display the notification image.
|
||||||
|
moodlecomponent?: string; // Moodle component that triggered the notification.
|
||||||
|
name?: string; // A name to identify the type of notification.
|
||||||
|
notId?: string; // Notification ID.
|
||||||
|
notif?: string; // "1" if it's a notification, "0" if it's a Moodle message.
|
||||||
|
site?: string; // ID of the site sending the notification.
|
||||||
|
siteurl?: string; // URL of the site the notification is related to.
|
||||||
|
usertoid?: string; // ID of user receiving the push.
|
||||||
|
conversationtype?: string; // Conversation type. Only if it's a push generated by a Moodle message.
|
||||||
|
userfromfullname?: string; // Fullname of user sending the push. Only if it's a push generated by a Moodle message.
|
||||||
|
userfromid?: string; // ID of user sending the push. Only if it's a push generated by a Moodle message.
|
||||||
|
picture?: string; // Notification big picture. "Extra" feature.
|
||||||
|
summaryText?: string; // Notification summary text. "Extra" feature.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional data sent in push notifications, with some calculated data.
|
||||||
|
*/
|
||||||
|
export type CorePushNotificationsNotificationBasicData = Omit<CorePushNotificationsNotificationBasicRawData, 'customdata'> & {
|
||||||
|
title?: string; // Notification title.
|
||||||
|
message?: string; // Notification message.
|
||||||
|
customdata?: Record<string, unknown>; // Parsed custom data.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_user_remove_user_device WS.
|
||||||
|
*/
|
||||||
|
export type CoreUserRemoveUserDeviceWSParams = {
|
||||||
|
uuid: string; // The device UUID.
|
||||||
|
appid?: string; // The app id, if empty devices matching the UUID for the user will be removed.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by core_user_remove_user_device WS.
|
||||||
|
*/
|
||||||
|
export type CoreUserRemoveUserDeviceWSResponse = {
|
||||||
|
removed: boolean; // True if removed, false if not removed because it doesn't exists.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Params of core_user_add_user_device WS.
|
||||||
|
*/
|
||||||
|
export type CoreUserAddUserDeviceWSParams = {
|
||||||
|
appid: string; // The app id, usually something like com.moodle.moodlemobile.
|
||||||
|
name: string; // The device name, 'occam' or 'iPhone' etc.
|
||||||
|
model: string; // The device model 'Nexus4' or 'iPad1,1' etc.
|
||||||
|
platform: string; // The device platform 'iOS' or 'Android' etc.
|
||||||
|
version: string; // The device version '6.1.2' or '4.2.2' etc.
|
||||||
|
pushid: string; // The device PUSH token/key/identifier/registration id.
|
||||||
|
uuid: string; // The device UUID.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by core_user_add_user_device WS.
|
||||||
|
*/
|
||||||
|
export type CoreUserAddUserDeviceWSResponse = CoreWSExternalWarning[][];
|
|
@ -971,6 +971,23 @@ export class CoreSitesProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a site with a certain URL. It will return the first site found.
|
||||||
|
*
|
||||||
|
* @param siteUrl The site URL.
|
||||||
|
* @return Promise resolved with the site.
|
||||||
|
*/
|
||||||
|
async getSiteByUrl(siteUrl: string): Promise<CoreSite> {
|
||||||
|
const db = await this.appDB;
|
||||||
|
const data = await db.getRecord<SiteDBEntry>(SITES_TABLE_NAME, { siteUrl });
|
||||||
|
|
||||||
|
if (typeof this.sites[data.id] != 'undefined') {
|
||||||
|
return this.sites[data.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.makeSiteFromSiteListEntry(data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a site from an entry of the sites list DB. The new site is added to the list of "cached" sites: this.sites.
|
* Create a site from an entry of the sites list DB. The new site is added to the list of "cached" sites: this.sites.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue