Skip to content

Commit 159056a

Browse files
committed
MOBILE-3412: Support offline and sync
1 parent 90dfe5a commit 159056a

File tree

15 files changed

+656
-17
lines changed

15 files changed

+656
-17
lines changed

scripts/langindex.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,7 @@
14081408
"core.confirmdeletefile": "repository",
14091409
"core.confirmgotabroot": "local_moodlemobileapp",
14101410
"core.confirmgotabrootdefault": "local_moodlemobileapp",
1411+
"core.confirmleaveunknownchanges": "local_moodlemobileapp",
14111412
"core.confirmloss": "local_moodlemobileapp",
14121413
"core.confirmopeninbrowser": "local_moodlemobileapp",
14131414
"core.considereddigitalminor": "moodle",
@@ -1861,6 +1862,7 @@
18611862
"core.mod_folder": "folder/pluginname",
18621863
"core.mod_forum": "forum/pluginname",
18631864
"core.mod_glossary": "glossary/pluginname",
1865+
"core.mod_h5pactivity": "h5pactivity/pluginname",
18641866
"core.mod_ims": "imscp/pluginname",
18651867
"core.mod_imscp": "imscp/pluginname",
18661868
"core.mod_label": "label/pluginname",

src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
66
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
77
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
8-
<core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
8+
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
9+
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
910
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
1011
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
1112
</core-context-menu>
@@ -16,6 +17,11 @@
1617

1718
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
1819

20+
<!-- Offline data stored. -->
21+
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
22+
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
23+
</ion-card>
24+
1925
<!-- Offline disabled. -->
2026
<ion-card class="core-warning-card" icon-start *ngIf="!siteCanDownload && playing">
2127
<ion-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}

src/addon/mod/h5pactivity/components/index/index.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ import { CoreH5P } from '@core/h5p/providers/h5p';
2525
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
2626
import { CoreH5PHelper } from '@core/h5p/classes/helper';
2727
import { CoreXAPI } from '@core/xapi/providers/xapi';
28+
import { CoreXAPIOffline } from '@core/xapi/providers/offline';
2829
import { CoreConstants } from '@core/constants';
2930
import { CoreSite } from '@classes/site';
3031

3132
import {
3233
AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo
3334
} from '../../providers/h5pactivity';
35+
import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync';
3436

3537
/**
3638
* Component that displays an H5P activity entry page.
@@ -59,8 +61,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
5961
state: string; // State of the file.
6062
siteCanDownload: boolean;
6163
trackComponent: string; // Component for tracking.
64+
hasOffline: boolean;
65+
isOpeningPage: boolean;
6266

6367
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
68+
protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
6469
protected site: CoreSite;
6570
protected observer;
6671
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
@@ -103,13 +108,18 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
103108
*/
104109
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
105110
try {
106-
this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id);
111+
this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId);
107112

108113
this.dataRetrieved.emit(this.h5pActivity);
109114
this.description = this.h5pActivity.intro;
110115
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
111116

117+
if (sync) {
118+
await this.syncActivity(showErrors);
119+
}
120+
112121
await Promise.all([
122+
this.checkHasOffline(),
113123
this.fetchAccessInfo(),
114124
this.fetchDeployedFileData(),
115125
]);
@@ -136,13 +146,22 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
136146
}
137147
}
138148

149+
/**
150+
* Fetch the access info and store it in the right variables.
151+
*
152+
* @return Promise resolved when done.
153+
*/
154+
protected async checkHasOffline(): Promise<void> {
155+
this.hasOffline = await CoreXAPIOffline.instance.contextHasStatements(this.h5pActivity.context, this.siteId);
156+
}
157+
139158
/**
140159
* Fetch the access info and store it in the right variables.
141160
*
142161
* @return Promise resolved when done.
143162
*/
144163
protected async fetchAccessInfo(): Promise<void> {
145-
this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id);
164+
this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId);
146165
}
147166

148167
/**
@@ -331,8 +350,17 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
331350
/**
332351
* Go to view user events.
333352
*/
334-
viewMyAttempts(): void {
335-
this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
353+
async viewMyAttempts(): Promise<void> {
354+
this.isOpeningPage = true;
355+
356+
try {
357+
await this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {
358+
courseId: this.courseId,
359+
h5pActivityId: this.h5pActivity.id,
360+
});
361+
} finally {
362+
this.isOpeningPage = false;
363+
}
336364
}
337365

338366
/**
@@ -342,12 +370,31 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
342370
* @return Promise resolved when done.
343371
*/
344372
protected async onIframeMessage(event: MessageEvent): Promise<void> {
345-
if (!event.data || !CoreXAPI.instance.canPostStatementInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
373+
if (!event.data || !CoreXAPI.instance.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
346374
return;
347375
}
348376

349377
try {
350-
await CoreXAPI.instance.postStatement(event.data.component, JSON.stringify(event.data.statements));
378+
const options = {
379+
offline: this.hasOffline,
380+
courseId: this.courseId,
381+
extra: this.h5pActivity.name,
382+
siteId: this.site.getId(),
383+
};
384+
385+
const sent = await CoreXAPI.instance.postStatements(this.h5pActivity.context, event.data.component,
386+
JSON.stringify(event.data.statements), options);
387+
388+
this.hasOffline = !sent;
389+
390+
if (sent) {
391+
try {
392+
// Invalidate attempts.
393+
await AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId);
394+
} catch (error) {
395+
// Ignore errors.
396+
}
397+
}
351398
} catch (error) {
352399
CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.');
353400
}
@@ -380,6 +427,39 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
380427
return match && match[1] == this.h5pActivity.context;
381428
}
382429

430+
/**
431+
* Performs the sync of the activity.
432+
*
433+
* @return Promise resolved when done.
434+
*/
435+
protected sync(): Promise<any> {
436+
return AddonModH5PActivitySync.instance.syncActivity(this.h5pActivity.context, this.site.getId());
437+
}
438+
439+
/**
440+
* An autosync event has been received.
441+
*
442+
* @param syncEventData Data receiven on sync observer.
443+
*/
444+
protected autoSyncEventReceived(syncEventData: any): void {
445+
this.checkHasOffline();
446+
}
447+
448+
/**
449+
* Go to blog posts.
450+
*
451+
* @param event Event.
452+
*/
453+
async gotoBlog(event: any): Promise<void> {
454+
this.isOpeningPage = true;
455+
456+
try {
457+
await super.gotoBlog(event);
458+
} finally {
459+
this.isOpeningPage = false;
460+
}
461+
}
462+
383463
/**
384464
* Component destroyed.
385465
*/

src/addon/mod/h5pactivity/h5pactivity.module.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,24 @@
1414

1515
import { NgModule } from '@angular/core';
1616

17+
import { CoreCronDelegate } from '@providers/cron';
1718
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
1819
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
1920
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
2021

2122
import { AddonModH5PActivityComponentsModule } from './components/components.module';
2223
import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
2324
import { AddonModH5PActivityProvider } from './providers/h5pactivity';
25+
import { AddonModH5PActivitySyncProvider } from './providers/sync';
2426
import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
2527
import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
2628
import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler';
29+
import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler';
2730

2831
// List of providers (without handlers).
2932
export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
3033
AddonModH5PActivityProvider,
34+
AddonModH5PActivitySyncProvider,
3135
];
3236

3337
@NgModule({
@@ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
3842
],
3943
providers: [
4044
AddonModH5PActivityProvider,
45+
AddonModH5PActivitySyncProvider,
4146
AddonModH5PActivityModuleHandler,
4247
AddonModH5PActivityPrefetchHandler,
4348
AddonModH5PActivityIndexLinkHandler,
4449
AddonModH5PActivityReportLinkHandler,
50+
AddonModH5PActivitySyncCronHandler,
4551
]
4652
})
4753
export class AddonModH5PActivityModule {
@@ -51,11 +57,14 @@ export class AddonModH5PActivityModule {
5157
prefetchHandler: AddonModH5PActivityPrefetchHandler,
5258
linksDelegate: CoreContentLinksDelegate,
5359
indexHandler: AddonModH5PActivityIndexLinkHandler,
54-
reportLinkHandler: AddonModH5PActivityReportLinkHandler) {
60+
reportLinkHandler: AddonModH5PActivityReportLinkHandler,
61+
cronDelegate: CoreCronDelegate,
62+
syncHandler: AddonModH5PActivitySyncCronHandler) {
5563

5664
moduleDelegate.registerHandler(moduleHandler);
5765
prefetchDelegate.registerHandler(prefetchHandler);
5866
linksDelegate.registerHandler(indexHandler);
5967
linksDelegate.registerHandler(reportLinkHandler);
68+
cronDelegate.register(syncHandler);
6069
}
6170
}

src/addon/mod/h5pactivity/pages/index/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class AddonModH5PActivityIndexPage {
5656
* @return Resolved if we can leave it, rejected if not.
5757
*/
5858
ionViewCanLeave(): Promise<void> {
59-
if (!this.h5pComponent.playing) {
59+
if (!this.h5pComponent.playing || this.h5pComponent.isOpeningPage) {
6060
return;
6161
}
6262

src/addon/mod/h5pactivity/providers/h5pactivity.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,20 @@ export class AddonModH5PActivityProvider {
385385
return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId);
386386
}
387387

388+
/**
389+
* Get an H5P activity by context ID.
390+
*
391+
* @param courseId Course ID.
392+
* @param contextId Context ID.
393+
* @param forceCache Whether it should always return cached data.
394+
* @param siteId Site ID. If not defined, current site.
395+
* @return Promise resolved with the activity data.
396+
*/
397+
getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string)
398+
: Promise<AddonModH5PActivityData> {
399+
return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId);
400+
}
401+
388402
/**
389403
* Get an H5P activity by instance ID.
390404
*
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { Injectable } from '@angular/core';
16+
import { CoreCronHandler } from '@providers/cron';
17+
import { AddonModH5PActivitySync } from './sync';
18+
19+
/**
20+
* Synchronization cron handler.
21+
*/
22+
@Injectable()
23+
export class AddonModH5PActivitySyncCronHandler implements CoreCronHandler {
24+
name = 'AddonModH5PActivitySyncCronHandler';
25+
26+
/**
27+
* Execute the process.
28+
* Receives the ID of the site affected, undefined for all sites.
29+
*
30+
* @param siteId ID of the site affected, undefined for all sites.
31+
* @param force Wether the execution is forced (manual sync).
32+
* @return Promise resolved when done, rejected if failure.
33+
*/
34+
execute(siteId?: string, force?: boolean): Promise<any> {
35+
return AddonModH5PActivitySync.instance.syncAllActivities(siteId, force);
36+
}
37+
38+
/**
39+
* Get the time between consecutive executions.
40+
*
41+
* @return Time between consecutive executions (in ms).
42+
*/
43+
getInterval(): number {
44+
return AddonModH5PActivitySync.instance.syncInterval;
45+
}
46+
}

0 commit comments

Comments
 (0)