MOBILE-3936 core: Add LocalNotifications mock

main
Pau Ferrer Ocaña 2022-09-16 13:58:35 +02:00
parent 43cec87112
commit 12c19080f2
9 changed files with 438 additions and 27 deletions

View File

@ -15,8 +15,8 @@
iconAction="fas-th-list" (action)="toggleDisplay()"></core-context-menu-item>
<core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate"
iconAction="fas-calendar-alt" (action)="toggleDisplay()"></core-context-menu-item>
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate"
(action)="openSettings()" iconAction="fas-cogs">
<core-context-menu-item [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()"
iconAction="fas-cogs">
</core-context-menu-item>
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400"
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"

View File

@ -31,7 +31,6 @@ import { AddonCalendarCalendarComponent } from '../../components/calendar/calend
import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events';
import { AddonCalendarFilterComponent } from '../../components/filter/filter';
import { CoreNavigator } from '@services/navigator';
import { CoreLocalNotifications } from '@services/local-notifications';
import { CoreConstants } from '@/core/constants';
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
@ -64,7 +63,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
month?: number;
canCreate = false;
courses: Partial<CoreEnrolledCourseData>[] = [];
notificationsEnabled = false;
loaded = false;
hasOffline = false;
isOnline = false;
@ -165,8 +163,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
* View loaded.
*/
ngOnInit(): void {
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
this.showCalendar = !this.loadUpcoming;

View File

@ -1384,10 +1384,6 @@ export class AddonCalendarProvider {
async updateAllSitesEventReminders(): Promise<void> {
await CorePlatform.ready();
if (!CoreLocalNotifications.isAvailable()) {
return;
}
const siteIds = await CoreSites.getSitesIds();
await Promise.all(siteIds.map((siteId: string) => async () => {
@ -1430,11 +1426,6 @@ export class AddonCalendarProvider {
events: ({ id: number; timestart: number; name: string})[],
siteId: string,
): Promise<void> {
if (!CoreLocalNotifications.isAvailable()) {
return;
}
await Promise.all(events.map(async (event) => {
if (event.timestart * 1000 <= Date.now()) {
// The event has already started, don't schedule it.

View File

@ -25,6 +25,7 @@ import { FileOpener } from '@ionic-native/file-opener/ngx';
import { FileTransfer } from '@ionic-native/file-transfer/ngx';
import { Geolocation } from '@ionic-native/geolocation/ngx';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
import { MediaCapture } from '@ionic-native/media-capture/ngx';
import { Zip } from '@ionic-native/zip/ngx';
@ -36,9 +37,11 @@ import { FileOpenerMock } from './services/file-opener';
import { FileTransferMock } from './services/file-transfer';
import { GeolocationMock } from './services/geolocation';
import { InAppBrowserMock } from './services/inappbrowser';
import { LocalNotificationsMock } from './services/local-notifications';
import { MediaCaptureMock } from './services/media-capture';
import { ZipMock } from './services/zip';
import { CorePlatform } from '@services/platform';
import { CoreLocalNotifications } from '@services/local-notifications';
/**
* This module handles the emulation of Cordova plugins in browser and desktop.
@ -90,6 +93,12 @@ import { CorePlatform } from '@services/platform';
provide: Zip,
useFactory: (): Zip => CorePlatform.is('cordova') ? new Zip() : new ZipMock(),
},
{
provide: LocalNotifications,
useFactory: (): LocalNotifications => CoreLocalNotifications.isPluginAvailable()
? new LocalNotifications()
: new LocalNotificationsMock(),
},
{
provide: APP_INITIALIZER,
useFactory: () => () => {

View File

@ -0,0 +1,407 @@
// (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 { CoreError } from '@classes/errors/error';
import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@ionic-native/local-notifications/ngx';
import { Observable, Subject } from 'rxjs';
/**
* Mock LocalNotifications service.
*/
export class LocalNotificationsMock extends LocalNotifications {
protected scheduledNotifications: ILocalNotification[] = [];
protected triggeredNotifications: ILocalNotification[] = [];
protected presentNotifications: Record<number, Notification> = {};
protected nextTimeout = 0;
protected hasGranted?: boolean;
protected observables = {
trigger: new Subject<ILocalNotification>(),
click: new Subject<ILocalNotification>(),
clear: new Subject<Notification>(),
clearall: new Subject<void>(),
cancel: new Subject<ILocalNotification>(),
cancelall: new Subject<void>(),
schedule: new Subject<ILocalNotification>(),
update: new Subject<ILocalNotification>(),
};
/**
* @inheritdoc
*/
schedule(options?: ILocalNotification | Array<ILocalNotification>): void {
this.hasPermission().then(() => {
// Do not check permission here, it could be denied by Selenium.
if (!options) {
return;
}
if (!Array.isArray(options)) {
options = [options];
}
this.scheduledNotifications = this.scheduledNotifications.concat(options);
this.scheduledNotifications.sort((a, b) =>
(a.trigger?.at?.getTime() || 0) - (b.trigger?.at?.getTime() || 0));
options.forEach((notification) => {
this.observables.schedule.next(notification);
});
this.scheduleNotifications();
return;
}).catch(() => {
// Ignore errors.
});
}
/**
* Sets timeout for next nofitication.
*/
protected scheduleNotifications(): void {
window.clearTimeout(this.nextTimeout);
const nextNotification = this.scheduledNotifications[0];
if (!nextNotification) {
return;
}
const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
const timeout = notificationTime - Date.now();
if (timeout <= 0) {
this.triggerNextNotification();
return;
}
this.nextTimeout = window.setTimeout(() => {
this.triggerNextNotification();
}, timeout);
}
/**
* Shows the next notification.
*/
protected triggerNextNotification(): void {
const dateNow = Date.now();
const nextNotification = this.scheduledNotifications[0];
if (!nextNotification) {
return;
}
const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
if (notificationTime === 0 || notificationTime <= dateNow) {
const body = Array.isArray(nextNotification.text) ? nextNotification.text.join() : nextNotification.text;
const notification = new Notification(nextNotification.title || '', {
body,
data: nextNotification.data,
icon: nextNotification.icon,
requireInteraction: true,
tag: nextNotification.data?.component,
});
this.triggeredNotifications.push(nextNotification);
this.observables.trigger.next(nextNotification);
notification.addEventListener('click', () => {
this.observables.click.next(nextNotification);
});
if (nextNotification.id) {
this.presentNotifications[nextNotification.id] = notification;
notification.addEventListener('close', () => {
delete(this.presentNotifications[nextNotification.id ?? 0]);
});
}
this.scheduledNotifications.shift();
this.triggerNextNotification();
} else {
this.scheduleNotifications();
}
}
/**
* @inheritdoc
*/
update(options?: ILocalNotification): void {
if (!options?.id) {
return;
}
const index = this.scheduledNotifications.findIndex((notification) => notification.id === options.id);
if (index < 0) {
return;
}
this.observables.update.next(options);
this.scheduledNotifications[index] = options;
}
/**
* @inheritdoc
*/
async clear(notificationId: number | Array<number>): Promise<void> {
if (!Array.isArray(notificationId)) {
notificationId = [notificationId];
}
notificationId.forEach((id) => {
if (!this.presentNotifications[id]) {
return;
}
this.presentNotifications[id].close();
this.observables.clear.next(this.presentNotifications[id]);
delete this.presentNotifications[id];
});
}
/**
* @inheritdoc
*/
async clearAll(): Promise<void> {
for (const x in this.presentNotifications) {
this.presentNotifications[x].close();
}
this.presentNotifications = {};
this.observables.clearall.next();
}
/**
* @inheritdoc
*/
async cancel(notificationId: number | Array<number>): Promise<void> {
if (!Array.isArray(notificationId)) {
notificationId = [notificationId];
}
notificationId.forEach((id) => {
const index = this.scheduledNotifications.findIndex((notification) => notification.id === id);
this.observables.cancel.next(this.scheduledNotifications[index]);
this.scheduledNotifications.splice(index, 1);
});
this.scheduleNotifications();
}
/**
* @inheritdoc
*/
async cancelAll(): Promise<void> {
window.clearTimeout(this.nextTimeout);
this.scheduledNotifications = [];
this.observables.cancelall.next();
}
/**
* @inheritdoc
*/
async isPresent(notificationId: number): Promise<boolean> {
return !!this.presentNotifications[notificationId];
}
/**
* @inheritdoc
*/
async isScheduled(notificationId: number): Promise<boolean> {
return this.scheduledNotifications.some((notification) => notification.id === notificationId);
}
/**
* @inheritdoc
*/
async isTriggered(notificationId: number): Promise<boolean> {
return this.triggeredNotifications.some((notification) => notification.id === notificationId);
}
/**
* @inheritdoc
*/
async getIds(): Promise<Array<number>> {
const ids = await this.getScheduledIds();
const triggeredIds = await this.getTriggeredIds();
return Promise.resolve(ids.concat(triggeredIds));
}
/**
* @inheritdoc
*/
async getTriggeredIds(): Promise<Array<number>> {
const ids = this.triggeredNotifications
.map((notification) => notification.id || 0)
.filter((id) => id > 0);
return ids;
}
/**
* @inheritdoc
*/
async getScheduledIds(): Promise<Array<number>> {
const ids = this.scheduledNotifications
.map((notification) => notification.id || 0)
.filter((id) => id > 0);
return ids;
}
/**
* @inheritdoc
*/
async get(notificationId: number): Promise<ILocalNotification> {
const notification = this.scheduledNotifications
.find((notification) => notification.id === notificationId);
if (!notification) {
throw new Error('Invalid Notification Id.');
}
return notification;
}
/**
* @inheritdoc
*/
async getAll(): Promise<Array<ILocalNotification>> {
return this.scheduledNotifications.concat(this.triggeredNotifications);
}
/**
* @inheritdoc
*/
async getAllScheduled(): Promise<Array<ILocalNotification>> {
return this.scheduledNotifications;
}
/**
* @inheritdoc
*/
async getAllTriggered(): Promise<Array<ILocalNotification>> {
return this.triggeredNotifications;
}
/**
* @inheritdoc
*/
async registerPermission(): Promise<boolean> {
// We need to ask the user for permission
const permission = await Notification.requestPermission();
this.hasGranted = permission === 'granted';
// If the user accepts, let's create a notification
return this.hasGranted;
}
/**
* @inheritdoc
*/
async hasPermission(): Promise<boolean> {
if (this.hasGranted !== undefined) {
return this.hasGranted;
}
if (!('Notification' in window)) {
// Check if the browser supports notifications
throw new CoreError('This browser does not support desktop notification');
}
return this.registerPermission();
}
/**
* @inheritdoc
*/
async addActions(groupId: unknown, actions: Array<ILocalNotificationAction>): Promise<Array<ILocalNotificationAction>> {
// Not implemented.
return actions;
}
/**
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeActions(groupId: unknown): Promise<unknown> {
// Not implemented.
return;
}
/**
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async hasActions(groupId: unknown): Promise<boolean> {
// Not implemented.
return false;
}
/**
* @inheritdoc
*/
async getDefaults(): Promise<unknown> {
// Not implemented.
return;
}
/**
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async setDefaults(defaults: unknown): Promise<unknown> {
// Not implemented.
return;
}
/**
* @inheritdoc
*/
on(eventName: string): Observable<unknown> {
if (!this.observables[eventName]) {
this.observables[eventName] = new Subject<ILocalNotification>();
}
return this.observables[eventName];
}
/**
* @inheritdoc
*/
fireEvent(eventName: string, args: unknown): void {
if (!this.observables[eventName]) {
return;
}
this.observables[eventName].next(args);
}
/**
* @inheritdoc
*/
async fireQueuedEvents(): Promise<unknown> {
return this.triggerNextNotification();
}
}

View File

@ -473,11 +473,6 @@ export class CorePushNotificationsProvider {
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.isAvailable()) {
return this.notifyReceived(notification, data);
}
const localNotif: ILocalNotification = {
id: Number(data.notId) || 1,
data: data,

View File

@ -67,7 +67,7 @@ export class CoreRemindersService {
* @return Promise resolved when done.
*/
async initialize(): Promise<void> {
if (!CoreLocalNotifications.isAvailable()) {
if (!this.isEnabled()) {
return;
}
@ -92,7 +92,7 @@ export class CoreRemindersService {
* @return True if reminders are enabled and available, false otherwise.
*/
isEnabled(): boolean {
return CoreLocalNotifications.isAvailable();
return true;
}
/**
@ -314,7 +314,8 @@ export class CoreRemindersService {
async scheduleAllNotifications(): Promise<void> {
await CorePlatform.ready();
if (!this.isEnabled()) {
if (CoreLocalNotifications.isPluginAvailable()) {
// Notifications are already scheduled.
return;
}

View File

@ -94,7 +94,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy {
lastCommit: CoreConstants.BUILD.lastCommitHash || '',
networkStatus: CoreNetwork.isOnline() ? 'online' : 'offline',
wifiConnection: CoreNetwork.isWifi() ? 'yes' : 'no',
localNotifAvailable: CoreLocalNotifications.isAvailable() ? 'yes' : 'no',
localNotifAvailable: CoreLocalNotifications.isPluginAvailable() ? 'yes' : 'no',
pushId: CorePushNotifications.getPushId(),
deviceType: '',
};

View File

@ -319,14 +319,26 @@ export class CoreLocalNotificationsProvider {
}
/**
* Returns whether local notifications plugin is installed.
* Returns whether local notifications are available.
*
* @return Whether local notifications plugin is installed.
* @return Whether local notifications are available.
* @deprecated since 4.1. It will always return true.
*/
isAvailable(): boolean {
return true;
}
/**
* Returns whether local notifications plugin is available.
*
* @return Whether local notifications plugin is available.
*/
isPluginAvailable(): boolean {
const win = <any> window; // eslint-disable-line @typescript-eslint/no-explicit-any
return !!win.cordova?.plugins?.notification?.local;
const enabled = !!win.cordova?.plugins?.notification?.local;
return enabled && CorePlatform.is('cordova');
}
/**