From e6818524c51881c29e914fa7197ac5ede406e019 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 27 May 2020 15:08:23 +0200
Subject: [PATCH] MOBILE-3411 h5pactivity: Implement prefetch

---
 .../index/addon-mod-h5pactivity-index.html    |   2 +
 .../mod/h5pactivity/components/index/index.ts |  19 +-
 .../mod/h5pactivity/h5pactivity.module.ts     |   6 +
 .../mod/h5pactivity/providers/h5pactivity.ts  |  46 +++--
 .../h5pactivity/providers/module-handler.ts   |   1 +
 .../h5pactivity/providers/prefetch-handler.ts | 166 ++++++++++++++++++
 6 files changed, 218 insertions(+), 22 deletions(-)
 create mode 100644 src/addon/mod/h5pactivity/providers/prefetch-handler.ts

diff --git a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html
index bc54e8a4c..28725aa6b 100644
--- a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html
+++ b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html
@@ -5,6 +5,8 @@
         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
         <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>
+        <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
+        <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>
     </core-context-menu>
 </core-navbar-buttons>
 
diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts
index 6d28a5fda..83d6469d5 100644
--- a/src/addon/mod/h5pactivity/components/index/index.ts
+++ b/src/addon/mod/h5pactivity/components/index/index.ts
@@ -142,18 +142,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
             return;
         }
 
-        if (this.h5pActivity.deployedfile) {
-            // File already deployed and still valid, use this one.
-            this.deployedFile = this.h5pActivity.deployedfile;
-        } else {
-            if (!this.h5pActivity.package || !this.h5pActivity.package[0]) {
-                // Shouldn't happen.
-                throw 'No H5P package found.';
-            }
-
-            // Deploy the file in the server.
-            this.deployedFile = await CoreH5P.instance.getTrustedH5PFile(this.h5pActivity.package[0].fileurl, this.displayOptions);
-        }
+        this.deployedFile = await AddonModH5PActivity.instance.getDeployedFile(this.h5pActivity, {
+            displayOptions: this.displayOptions,
+            siteId: this.siteId,
+        });
 
         this.fileUrl = this.deployedFile.fileurl;
 
@@ -300,6 +292,9 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
      */
     play(): void {
         this.playing = true;
+
+        // Mark the activity as viewed.
+        AddonModH5PActivity.instance.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId);
     }
 
     /**
diff --git a/src/addon/mod/h5pactivity/h5pactivity.module.ts b/src/addon/mod/h5pactivity/h5pactivity.module.ts
index ed0fb852e..771ac2ad6 100644
--- a/src/addon/mod/h5pactivity/h5pactivity.module.ts
+++ b/src/addon/mod/h5pactivity/h5pactivity.module.ts
@@ -16,10 +16,12 @@ import { NgModule } from '@angular/core';
 
 import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
 import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
+import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
 
 import { AddonModH5PActivityComponentsModule } from './components/components.module';
 import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
 import { AddonModH5PActivityProvider } from './providers/h5pactivity';
+import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
 import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
 
 // List of providers (without handlers).
@@ -36,16 +38,20 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
     providers: [
         AddonModH5PActivityProvider,
         AddonModH5PActivityModuleHandler,
+        AddonModH5PActivityPrefetchHandler,
         AddonModH5PActivityIndexLinkHandler,
     ]
 })
 export class AddonModH5PActivityModule {
     constructor(moduleDelegate: CoreCourseModuleDelegate,
             moduleHandler: AddonModH5PActivityModuleHandler,
+            prefetchDelegate: CoreCourseModulePrefetchDelegate,
+            prefetchHandler: AddonModH5PActivityPrefetchHandler,
             linksDelegate: CoreContentLinksDelegate,
             indexHandler: AddonModH5PActivityIndexLinkHandler) {
 
         moduleDelegate.registerHandler(moduleHandler);
+        prefetchDelegate.registerHandler(prefetchHandler);
         linksDelegate.registerHandler(indexHandler);
     }
 }
diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts
index 60a6f8abf..70d42524b 100644
--- a/src/addon/mod/h5pactivity/providers/h5pactivity.ts
+++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts
@@ -18,6 +18,8 @@ import { CoreSites } from '@providers/sites';
 import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
 import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
 import { CoreCourseLogHelper } from '@core/course/providers/log-helper';
+import { CoreH5P } from '@core/h5p/providers/h5p';
+import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
 
 import { makeSingleton, Translate } from '@singletons/core.singletons';
 
@@ -63,6 +65,33 @@ export class AddonModH5PActivityProvider {
         return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets);
     }
 
+    /**
+     * Get deployed file from an H5P activity instance.
+     *
+     * @param h5pActivity Activity instance.
+     * @param options Options
+     * @return Promise resolved with the file.
+     */
+    async getDeployedFile(h5pActivity: AddonModH5PActivityData, options?: AddonModH5PActivityGetDeployedFileOptions)
+            : Promise<CoreWSExternalFile> {
+
+        if (h5pActivity.deployedfile) {
+            // File already deployed and still valid, use this one.
+            return h5pActivity.deployedfile;
+        } else {
+            if (!h5pActivity.package || !h5pActivity.package[0]) {
+                // Shouldn't happen.
+                throw 'No H5P package found.';
+            }
+
+            options = options || {};
+
+            // Deploy the file in the server.
+            return CoreH5P.instance.getTrustedH5PFile(h5pActivity.package[0].fileurl, options.displayOptions,
+                    options.ignoreCache, options.siteId);
+        }
+    }
+
     /**
      * Get cache key for H5P activity data WS calls.
      *
@@ -189,12 +218,12 @@ export class AddonModH5PActivityProvider {
      * @param siteId Site ID. If not defined, current site.
      * @return Promise resolved when the WS call is successful.
      */
-    async logView(id: number, name?: string, siteId?: string): Promise<void> {
+    logView(id: number, name?: string, siteId?: string): Promise<void> {
         const params = {
             h5pactivityid: id,
         };
 
-        const result: AddonModH5PActivityViewResult = await CoreCourseLogHelper.instance.logSingle(
+        return CoreCourseLogHelper.instance.logSingle(
             'mod_h5pactivity_view_h5pactivity',
             params,
             AddonModH5PActivityProvider.COMPONENT,
@@ -204,10 +233,6 @@ export class AddonModH5PActivityProvider {
             {},
             siteId
         );
-
-        if (!result.status) {
-            throw result.warnings[0] || 'Error marking H5P activity as viewed.';
-        }
     }
 }
 
@@ -262,9 +287,10 @@ export type AddonModH5PActivityAccessInfo = {
 };
 
 /**
- * Result of WS mod_h5pactivity_view_h5pactivity.
+ * Options to pass to getDeployedFile function.
  */
-export type AddonModH5PActivityViewResult = {
-    status: boolean; // Status: true if success.
-    warnings?: CoreWSExternalWarning[];
+export type AddonModH5PActivityGetDeployedFileOptions = {
+    displayOptions?: CoreH5PDisplayOptions; // Display options
+    ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down.
+    siteId?: string; // Site ID. If not defined, current site.
 };
diff --git a/src/addon/mod/h5pactivity/providers/module-handler.ts b/src/addon/mod/h5pactivity/providers/module-handler.ts
index e6ad04b83..6f544a16c 100644
--- a/src/addon/mod/h5pactivity/providers/module-handler.ts
+++ b/src/addon/mod/h5pactivity/providers/module-handler.ts
@@ -65,6 +65,7 @@ export class AddonModH5PActivityModuleHandler implements CoreCourseModuleHandler
             icon: CoreCourse.instance.getModuleIconSrc(this.modName, module.modicon),
             title: module.name,
             class: 'addon-mod_h5pactivity-handler',
+            showDownloadButton: true,
             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void {
                 const pageParams = {module: module, courseId: courseId};
                 if (params) {
diff --git a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts
new file mode 100644
index 000000000..ccb03a168
--- /dev/null
+++ b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts
@@ -0,0 +1,166 @@
+// (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, Injector } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreAppProvider } from '@providers/app';
+import { CoreFilepoolProvider } from '@providers/filepool';
+import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreWSExternalFile } from '@providers/ws';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
+import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
+import { CoreH5PHelper } from '@core/h5p/classes/helper';
+import { CoreH5P } from '@core/h5p/providers/h5p';
+import { CoreUserProvider } from '@core/user/providers/user';
+import { AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData } from './h5pactivity';
+
+/**
+ * Handler to prefetch h5p activity.
+ */
+@Injectable()
+export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase {
+    name = 'AddonModH5PActivity';
+    modName = 'h5pactivity';
+    component = AddonModH5PActivityProvider.COMPONENT;
+    updatesNames = /^configuration$|^.*files$|^tracks$|^usertracks$/;
+
+    constructor(translate: TranslateService,
+            appProvider: CoreAppProvider,
+            utils: CoreUtilsProvider,
+            courseProvider: CoreCourseProvider,
+            filepoolProvider: CoreFilepoolProvider,
+            sitesProvider: CoreSitesProvider,
+            domUtils: CoreDomUtilsProvider,
+            filterHelper: CoreFilterHelperProvider,
+            pluginFileDelegate: CorePluginFileDelegate,
+            protected userProvider: CoreUserProvider,
+            protected injector: Injector) {
+
+        super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
+                pluginFileDelegate);
+    }
+
+    /**
+     * Get list of files.
+     *
+     * @param module Module.
+     * @param courseId Course ID the module belongs to.
+     * @param single True if we're downloading a single module, false if we're downloading a whole section.
+     * @return Promise resolved with the list of files.
+     */
+    async getFiles(module: any, courseId: number, single?: boolean): Promise<CoreWSExternalFile[]> {
+
+        const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id);
+
+        const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions);
+
+        const deployedFile = await AddonModH5PActivity.instance.getDeployedFile(h5pActivity, {
+            displayOptions: displayOptions,
+        });
+
+        return [deployedFile].concat(this.getIntroFilesFromInstance(module, h5pActivity));
+    }
+
+    /**
+     * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
+     * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
+     *
+     * @param module Module.
+     * @param courseId Course ID the module belongs to.
+     * @return Promise resolved when invalidated.
+     */
+    async invalidateModule(module: any, courseId: number): Promise<void> {
+        // No need to invalidate anything.
+    }
+
+    /**
+     * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
+     *
+     * @param module Module.
+     * @param courseId Course ID the module belongs to.
+     * @return Whether the module can be downloaded. The promise should never be rejected.
+     */
+    isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
+        return this.sitesProvider.getCurrentSite().canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
+    }
+
+    /**
+     * Whether or not the handler is enabled on a site level.
+     *
+     * @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return AddonModH5PActivity.instance.isPluginEnabled();
+    }
+
+    /**
+     * Prefetch a module.
+     *
+     * @param module Module.
+     * @param courseId Course ID the module belongs to.
+     * @param single True if we're downloading a single module, false if we're downloading a whole section.
+     * @param dirPath Path of the directory where to store all the content files.
+     * @return Promise resolved when done.
+     */
+    prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
+        return this.prefetchPackage(module, courseId, single, this.prefetchActivity.bind(this));
+    }
+
+    /**
+     * Prefetch an H5P activity.
+     *
+     * @param module Module.
+     * @param courseId Course ID the module belongs to.
+     * @param single True if we're downloading a single module, false if we're downloading a whole section.
+     * @param siteId Site ID.
+     * @return Promise resolved when done.
+     */
+    protected async prefetchActivity(module: any, courseId: number, single: boolean, siteId: string): Promise<void> {
+
+        const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id, true, siteId);
+
+        const introFiles = this.getIntroFilesFromInstance(module, h5pActivity);
+
+        await Promise.all([
+            AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, true, siteId),
+            this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModH5PActivityProvider.COMPONENT, module.id),
+            this.prefetchMainFile(module, h5pActivity, siteId),
+        ]);
+    }
+
+    /**
+     * Prefetch the deployed file of the activity.
+     *
+     * @param module Module.
+     * @param h5pActivity Activity instance.
+     * @param siteId Site ID.
+     * @return Promise resolved when done.
+     */
+    protected async prefetchMainFile(module: any, h5pActivity: AddonModH5PActivityData, siteId: string): Promise<void> {
+
+        const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions);
+
+        const deployedFile = await AddonModH5PActivity.instance.getDeployedFile(h5pActivity, {
+            displayOptions: displayOptions,
+            ignoreCache: true,
+            siteId: siteId,
+        });
+
+        await this.filepoolProvider.addFilesToQueue(siteId, [deployedFile], AddonModH5PActivityProvider.COMPONENT, module.id);
+    }
+}