diff --git a/scripts/langindex.json b/scripts/langindex.json
index 9df725629..8e06a8e3f 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1408,6 +1408,7 @@
   "core.confirmdeletefile": "repository",
   "core.confirmgotabroot": "local_moodlemobileapp",
   "core.confirmgotabrootdefault": "local_moodlemobileapp",
+  "core.confirmleaveunknownchanges": "local_moodlemobileapp",
   "core.confirmloss": "local_moodlemobileapp",
   "core.confirmopeninbrowser": "local_moodlemobileapp",
   "core.considereddigitalminor": "moodle",
@@ -1861,6 +1862,7 @@
   "core.mod_folder": "folder/pluginname",
   "core.mod_forum": "forum/pluginname",
   "core.mod_glossary": "glossary/pluginname",
+  "core.mod_h5pactivity": "h5pactivity/pluginname",
   "core.mod_ims": "imscp/pluginname",
   "core.mod_imscp": "imscp/pluginname",
   "core.mod_label": "label/pluginname",
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 da77d63c7..23df4f6ad 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,7 +5,8 @@
         
         
         
-        
+        
+        
         
         
     
@@ -16,6 +17,11 @@
 
     
 
+    
+    
+         {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
+    
+
     
     
          {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts
index 36167eb04..0244d338f 100644
--- a/src/addon/mod/h5pactivity/components/index/index.ts
+++ b/src/addon/mod/h5pactivity/components/index/index.ts
@@ -25,12 +25,14 @@ import { CoreH5P } from '@core/h5p/providers/h5p';
 import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
 import { CoreH5PHelper } from '@core/h5p/classes/helper';
 import { CoreXAPI } from '@core/xapi/providers/xapi';
+import { CoreXAPIOffline } from '@core/xapi/providers/offline';
 import { CoreConstants } from '@core/constants';
 import { CoreSite } from '@classes/site';
 
 import {
     AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo
 } from '../../providers/h5pactivity';
+import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync';
 
 /**
  * Component that displays an H5P activity entry page.
@@ -59,8 +61,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
     state: string; // State of the file.
     siteCanDownload: boolean;
     trackComponent: string; // Component for tracking.
+    hasOffline: boolean;
+    isOpeningPage: boolean;
 
     protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
+    protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
     protected site: CoreSite;
     protected observer;
     protected messageListenerFunction: (event: MessageEvent) => Promise;
@@ -103,13 +108,18 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
      */
     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise {
         try {
-            this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id);
+            this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId);
 
             this.dataRetrieved.emit(this.h5pActivity);
             this.description = this.h5pActivity.intro;
             this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
 
+            if (sync) {
+                await this.syncActivity(showErrors);
+            }
+
             await Promise.all([
+                this.checkHasOffline(),
                 this.fetchAccessInfo(),
                 this.fetchDeployedFileData(),
             ]);
@@ -136,13 +146,22 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
         }
     }
 
+    /**
+     * Fetch the access info and store it in the right variables.
+     *
+     * @return Promise resolved when done.
+     */
+    protected async checkHasOffline(): Promise {
+        this.hasOffline = await CoreXAPIOffline.instance.contextHasStatements(this.h5pActivity.context, this.siteId);
+    }
+
     /**
      * Fetch the access info and store it in the right variables.
      *
      * @return Promise resolved when done.
      */
     protected async fetchAccessInfo(): Promise {
-        this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id);
+        this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId);
     }
 
     /**
@@ -331,8 +350,17 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
     /**
      * Go to view user events.
      */
-    viewMyAttempts(): void {
-        this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
+    async viewMyAttempts(): Promise {
+        this.isOpeningPage = true;
+
+        try {
+            await this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {
+                courseId: this.courseId,
+                h5pActivityId: this.h5pActivity.id,
+            });
+        } finally {
+            this.isOpeningPage = false;
+        }
     }
 
     /**
@@ -342,12 +370,31 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
      * @return Promise resolved when done.
      */
     protected async onIframeMessage(event: MessageEvent): Promise {
-        if (!event.data || !CoreXAPI.instance.canPostStatementInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
+        if (!event.data || !CoreXAPI.instance.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
             return;
         }
 
         try {
-            await CoreXAPI.instance.postStatement(event.data.component, JSON.stringify(event.data.statements));
+            const options = {
+                offline: this.hasOffline,
+                courseId: this.courseId,
+                extra: this.h5pActivity.name,
+                siteId: this.site.getId(),
+            };
+
+            const sent = await CoreXAPI.instance.postStatements(this.h5pActivity.context, event.data.component,
+                    JSON.stringify(event.data.statements), options);
+
+            this.hasOffline = !sent;
+
+            if (sent) {
+                try {
+                    // Invalidate attempts.
+                    await AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId);
+                } catch (error) {
+                    // Ignore errors.
+                }
+            }
         } catch (error) {
             CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.');
         }
@@ -380,6 +427,39 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
         return match && match[1] == this.h5pActivity.context;
     }
 
+    /**
+     * Performs the sync of the activity.
+     *
+     * @return Promise resolved when done.
+     */
+    protected sync(): Promise {
+        return AddonModH5PActivitySync.instance.syncActivity(this.h5pActivity.context, this.site.getId());
+    }
+
+    /**
+     * An autosync event has been received.
+     *
+     * @param syncEventData Data receiven on sync observer.
+     */
+    protected autoSyncEventReceived(syncEventData: any): void {
+        this.checkHasOffline();
+    }
+
+    /**
+     * Go to blog posts.
+     *
+     * @param event Event.
+     */
+    async gotoBlog(event: any): Promise {
+        this.isOpeningPage = true;
+
+        try {
+            await super.gotoBlog(event);
+        } finally {
+            this.isOpeningPage = false;
+        }
+    }
+
     /**
      * Component destroyed.
      */
diff --git a/src/addon/mod/h5pactivity/h5pactivity.module.ts b/src/addon/mod/h5pactivity/h5pactivity.module.ts
index 3053c1168..85cce71a2 100644
--- a/src/addon/mod/h5pactivity/h5pactivity.module.ts
+++ b/src/addon/mod/h5pactivity/h5pactivity.module.ts
@@ -14,6 +14,7 @@
 
 import { NgModule } from '@angular/core';
 
+import { CoreCronDelegate } from '@providers/cron';
 import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
 import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
 import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
@@ -21,13 +22,16 @@ import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-
 import { AddonModH5PActivityComponentsModule } from './components/components.module';
 import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
 import { AddonModH5PActivityProvider } from './providers/h5pactivity';
+import { AddonModH5PActivitySyncProvider } from './providers/sync';
 import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
 import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
 import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler';
+import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler';
 
 // List of providers (without handlers).
 export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
     AddonModH5PActivityProvider,
+    AddonModH5PActivitySyncProvider,
 ];
 
 @NgModule({
@@ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
     ],
     providers: [
         AddonModH5PActivityProvider,
+        AddonModH5PActivitySyncProvider,
         AddonModH5PActivityModuleHandler,
         AddonModH5PActivityPrefetchHandler,
         AddonModH5PActivityIndexLinkHandler,
         AddonModH5PActivityReportLinkHandler,
+        AddonModH5PActivitySyncCronHandler,
     ]
 })
 export class AddonModH5PActivityModule {
@@ -51,11 +57,14 @@ export class AddonModH5PActivityModule {
             prefetchHandler: AddonModH5PActivityPrefetchHandler,
             linksDelegate: CoreContentLinksDelegate,
             indexHandler: AddonModH5PActivityIndexLinkHandler,
-            reportLinkHandler: AddonModH5PActivityReportLinkHandler) {
+            reportLinkHandler: AddonModH5PActivityReportLinkHandler,
+            cronDelegate: CoreCronDelegate,
+            syncHandler: AddonModH5PActivitySyncCronHandler) {
 
         moduleDelegate.registerHandler(moduleHandler);
         prefetchDelegate.registerHandler(prefetchHandler);
         linksDelegate.registerHandler(indexHandler);
         linksDelegate.registerHandler(reportLinkHandler);
+        cronDelegate.register(syncHandler);
     }
 }
diff --git a/src/addon/mod/h5pactivity/pages/index/index.ts b/src/addon/mod/h5pactivity/pages/index/index.ts
index 51a21a82f..e3bc7e304 100644
--- a/src/addon/mod/h5pactivity/pages/index/index.ts
+++ b/src/addon/mod/h5pactivity/pages/index/index.ts
@@ -56,7 +56,7 @@ export class AddonModH5PActivityIndexPage {
      * @return Resolved if we can leave it, rejected if not.
      */
     ionViewCanLeave(): Promise {
-        if (!this.h5pComponent.playing) {
+        if (!this.h5pComponent.playing || this.h5pComponent.isOpeningPage) {
             return;
         }
 
diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts
index ef7ccab9e..5e1fabc42 100644
--- a/src/addon/mod/h5pactivity/providers/h5pactivity.ts
+++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts
@@ -385,6 +385,20 @@ export class AddonModH5PActivityProvider {
         return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId);
     }
 
+    /**
+     * Get an H5P activity by context ID.
+     *
+     * @param courseId Course ID.
+     * @param contextId Context ID.
+     * @param forceCache Whether it should always return cached data.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with the activity data.
+     */
+    getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string)
+            : Promise {
+        return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId);
+    }
+
     /**
      * Get an H5P activity by instance ID.
      *
diff --git a/src/addon/mod/h5pactivity/providers/sync-cron-handler.ts b/src/addon/mod/h5pactivity/providers/sync-cron-handler.ts
new file mode 100644
index 000000000..5ffccb86b
--- /dev/null
+++ b/src/addon/mod/h5pactivity/providers/sync-cron-handler.ts
@@ -0,0 +1,46 @@
+// (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 { CoreCronHandler } from '@providers/cron';
+import { AddonModH5PActivitySync } from './sync';
+
+/**
+ * Synchronization cron handler.
+ */
+@Injectable()
+export class AddonModH5PActivitySyncCronHandler implements CoreCronHandler {
+    name = 'AddonModH5PActivitySyncCronHandler';
+
+    /**
+     * Execute the process.
+     * Receives the ID of the site affected, undefined for all sites.
+     *
+     * @param siteId ID of the site affected, undefined for all sites.
+     * @param force Wether the execution is forced (manual sync).
+     * @return Promise resolved when done, rejected if failure.
+     */
+    execute(siteId?: string, force?: boolean): Promise {
+        return AddonModH5PActivitySync.instance.syncAllActivities(siteId, force);
+    }
+
+    /**
+     * Get the time between consecutive executions.
+     *
+     * @return Time between consecutive executions (in ms).
+     */
+    getInterval(): number {
+        return AddonModH5PActivitySync.instance.syncInterval;
+    }
+}
diff --git a/src/addon/mod/h5pactivity/providers/sync.ts b/src/addon/mod/h5pactivity/providers/sync.ts
new file mode 100644
index 000000000..dafa44842
--- /dev/null
+++ b/src/addon/mod/h5pactivity/providers/sync.ts
@@ -0,0 +1,223 @@
+// (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 { TranslateService } from '@ngx-translate/core';
+import { CoreAppProvider } from '@providers/app';
+import { CoreEvents } from '@providers/events';
+import { CoreLoggerProvider } from '@providers/logger';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreSyncProvider } from '@providers/sync';
+import { CoreUtils } from '@providers/utils/utils';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreCourse } from '@core/course/providers/course';
+import { CoreCourseLogHelper } from '@core/course/providers/log-helper';
+import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
+import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync';
+import { CoreXAPI } from '@core/xapi/providers/xapi';
+import { CoreXAPIOffline } from '@core/xapi/providers/offline';
+import { AddonModH5PActivity, AddonModH5PActivityProvider } from './h5pactivity';
+import { AddonModH5PActivityPrefetchHandler } from './prefetch-handler';
+
+import { makeSingleton } from '@singletons/core.singletons';
+
+/**
+ * Service to sync H5P activities.
+ */
+@Injectable()
+export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseProvider {
+
+    static AUTO_SYNCED = 'addon_mod_h5pactivity_autom_synced';
+    protected componentTranslate: string;
+
+    constructor(sitesProvider: CoreSitesProvider,
+            loggerProvider: CoreLoggerProvider,
+            appProvider: CoreAppProvider,
+            translate: TranslateService,
+            textUtils: CoreTextUtilsProvider,
+            syncProvider: CoreSyncProvider,
+            timeUtils: CoreTimeUtilsProvider,
+            prefetchHandler: AddonModH5PActivityPrefetchHandler,
+            prefetchDelegate: CoreCourseModulePrefetchDelegate) {
+
+        super('AddonModH5PActivitySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
+                timeUtils, prefetchDelegate, prefetchHandler);
+
+        this.componentTranslate = CoreCourse.instance.translateModuleName('h5pactivity');
+    }
+
+    /**
+     * Try to synchronize all the H5P activities in a certain site or in all sites.
+     *
+     * @param siteId Site ID to sync. If not defined, sync all sites.
+     * @param force Wether to force sync not depending on last execution.
+     * @return Promise resolved if sync is successful, rejected if sync fails.
+     */
+    syncAllActivities(siteId?: string, force?: boolean): Promise {
+        return this.syncOnSites('H5P activities', this.syncAllActivitiesFunc.bind(this), [force], siteId);
+    }
+
+    /**
+     * Sync all H5P activities on a site.
+     *
+     * @param siteId Site ID to sync. If not defined, sync all sites.
+     * @param force Wether to force sync not depending on last execution.
+     * @return Promise resolved if sync is successful, rejected if sync fails.
+     */
+    protected async syncAllActivitiesFunc(siteId?: string, force?: boolean): Promise {
+        const entries = await CoreXAPIOffline.instance.getAllStatements(siteId);
+
+        // Sync all responses.
+        const promises = entries.map((response) => {
+            const promise = force ? this.syncActivity(response.contextid, siteId) :
+                    this.syncActivityIfNeeded(response.contextid, siteId);
+
+            return promise.then((result) => {
+                if (result && result.updated) {
+                    // Sync successful, send event.
+                    CoreEvents.instance.trigger(AddonModH5PActivitySyncProvider.AUTO_SYNCED, {
+                        contextId: response.contextid,
+                        warnings: result.warnings,
+                    }, siteId);
+                }
+            });
+        });
+
+        await Promise.all(promises);
+    }
+
+    /**
+     * Sync an H5P activity only if a certain time has passed since the last time.
+     *
+     * @param contextId Context ID of the activity.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved when the activity is synced or it doesn't need to be synced.
+     */
+    async syncActivityIfNeeded(contextId: number, siteId?: string): Promise {
+        const needed = await this.isSyncNeeded(contextId, siteId);
+
+        if (needed) {
+            return this.syncActivity(contextId, siteId);
+        }
+    }
+
+    /**
+     * Synchronize an H5P activity. If it's already being synced it will reuse the same promise.
+     *
+     * @param contextId Context ID of the activity.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved if sync is successful, rejected otherwise.
+     */
+    syncActivity(contextId: number, siteId?: string): Promise {
+        siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+        if (!this.appProvider.isOnline()) {
+            // Cannot sync in offline.
+            throw this.translate.instant('core.networkerrormsg');
+        }
+
+        if (this.isSyncing(contextId, siteId)) {
+            // There's already a sync ongoing for this discussion, return the promise.
+            return this.getOngoingSync(contextId, siteId);
+        }
+
+        return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId);
+    }
+
+    /**
+     * Synchronize an H5P activity.
+     *
+     * @param contextId Context ID of the activity.
+     * @param siteId Site ID.
+     * @return Promise resolved if sync is successful, rejected otherwise.
+     */
+    protected async syncActivityData(contextId: number, siteId: string): Promise<{warnings: string[], updated: boolean}> {
+
+        this.logger.debug(`Try to sync H5P activity with context ID '${contextId}'`);
+
+        const result = {
+            warnings: [],
+            updated: false,
+        };
+
+        // Get all the statements stored for the activity.
+        const entries = await CoreXAPIOffline.instance.getContextStatements(contextId, siteId);
+
+        if (!entries || !entries.length) {
+            // Nothing to sync.
+            await this.setSyncTime(contextId, siteId);
+
+            return result;
+        }
+
+        // Get the activity instance.
+        const courseId = entries[0].courseid;
+
+        const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, false, siteId);
+
+        // Sync offline logs.
+        try {
+            await CoreCourseLogHelper.instance.syncIfNeeded(AddonModH5PActivityProvider.COMPONENT, h5pActivity.id, siteId);
+        } catch (error) {
+            // Ignore errors.
+        }
+
+        // Send the statements in order.
+        for (let i = 0; i < entries.length; i++) {
+            const entry = entries[i];
+
+            try {
+                await CoreXAPI.instance.postStatementsOnline(entry.component, entry.statements, siteId);
+
+                result.updated = true;
+
+                await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId);
+            } catch (error) {
+                if (CoreUtils.instance.isWebServiceError(error)) {
+                    // The WebService has thrown an error, this means that statements cannot be submitted. Delete them.
+                    result.updated = true;
+
+                    await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId);
+
+                    // Responses deleted, add a warning.
+                    result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
+                        component: this.componentTranslate,
+                        name: entry.extra,
+                        error: this.textUtils.getErrorMessageFromError(error),
+                    }));
+                } else {
+                    // Stop synchronizing.
+                    throw error;
+                }
+            }
+        }
+
+        if (result.updated) {
+            try {
+                // Data has been sent to server, invalidate attempts.
+                await AddonModH5PActivity.instance.invalidateUserAttempts(h5pActivity.id, undefined, siteId);
+            } catch (error) {
+                // Ignore errors.
+            }
+        }
+
+        // Sync finished, set sync time.
+        await this.setSyncTime(contextId, siteId);
+
+        return result;
+    }
+}
+
+export class AddonModH5PActivitySync extends makeSingleton(AddonModH5PActivitySyncProvider) {}
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 4682e88d7..1f54d6380 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -1862,6 +1862,7 @@
     "core.mod_folder": "Folder",
     "core.mod_forum": "Forum",
     "core.mod_glossary": "Glossary",
+    "core.mod_h5pactivity": "H5P",
     "core.mod_ims": "IMS content package",
     "core.mod_imscp": "IMS content package",
     "core.mod_label": "Label",
diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts
index 0c9b84f9a..85d063b14 100644
--- a/src/core/compile/providers/compile.ts
+++ b/src/core/compile/providers/compile.ts
@@ -41,6 +41,7 @@ import { CORE_PUSHNOTIFICATIONS_PROVIDERS } from '@core/pushnotifications/pushno
 import { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module';
 import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module';
 import { CORE_SEARCH_PROVIDERS } from '@core/search/search.module';
+import { CORE_XAPI_PROVIDERS } from '@core/xapi/xapi.module';
 
 // Import only this provider to prevent circular dependencies.
 import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins';
@@ -243,7 +244,7 @@ export class CoreCompileProvider {
                 .concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS)
                 .concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS)
                 .concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS).concat(CORE_EDITOR_PROVIDERS)
-                .concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS);
+                .concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS).concat(CORE_XAPI_PROVIDERS);
 
         // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
         for (const i in providers) {
diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts
index 0a333639b..67a5985d3 100644
--- a/src/core/course/classes/main-resource-component.ts
+++ b/src/core/course/classes/main-resource-component.ts
@@ -280,8 +280,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
      *
      * @param event Event.
      */
-    gotoBlog(event: any): void {
-        this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id });
+    gotoBlog(event: any): Promise {
+        return this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id });
     }
 
     /**
diff --git a/src/core/xapi/providers/offline.ts b/src/core/xapi/providers/offline.ts
new file mode 100644
index 000000000..26005962c
--- /dev/null
+++ b/src/core/xapi/providers/offline.ts
@@ -0,0 +1,201 @@
+// (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 { CoreSites, CoreSiteSchema } from '@providers/sites';
+
+import { makeSingleton } from '@singletons/core.singletons';
+
+/**
+ * Service to handle offline xAPI.
+ */
+@Injectable()
+export class CoreXAPIOfflineProvider {
+
+    // Variables for database.
+    static STATEMENTS_TABLE = 'core_xapi_statements';
+
+    protected siteSchema: CoreSiteSchema = {
+        name: 'CoreXAPIOfflineProvider',
+        version: 1,
+        tables: [
+            {
+                name: CoreXAPIOfflineProvider.STATEMENTS_TABLE,
+                columns: [
+                    {
+                        name: 'id',
+                        type: 'INTEGER',
+                        primaryKey: true,
+                        autoIncrement: true,
+                    },
+                    {
+                        name: 'contextid',
+                        type: 'INTEGER'
+                    },
+                    {
+                        name: 'component',
+                        type: 'TEXT'
+                    },
+                    {
+                        name: 'statements',
+                        type: 'TEXT'
+                    },
+                    {
+                        name: 'timecreated',
+                        type: 'INTEGER'
+                    },
+                    {
+                        name: 'courseid',
+                        type: 'INTEGER'
+                    },
+                    {
+                        name: 'extra',
+                        type: 'TEXT'
+                    },
+                ],
+            }
+        ]
+    };
+
+    constructor() {
+        CoreSites.instance.registerSiteSchema(this.siteSchema);
+    }
+
+    /**
+     * Check if there are offline statements to send for a context.
+     *
+     * @param contextId Context ID.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with boolean: true if has offline statements, false otherwise.
+     */
+    async contextHasStatements(contextId: number, siteId?: string): Promise {
+        const statementsList = await this.getContextStatements(contextId, siteId);
+
+        return statementsList && statementsList.length > 0;
+    }
+
+    /**
+     * Delete certain statements.
+     *
+     * @param id ID of the statements.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved if stored, rejected if failure.
+     */
+    async deleteStatements(id: number, siteId?: string): Promise {
+        const site = await CoreSites.instance.getSite(siteId);
+
+        await site.getDb().deleteRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {id});
+    }
+
+    /**
+     * Delete all statements of a certain context.
+     *
+     * @param contextId Context ID.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved if stored, rejected if failure.
+     */
+    async deleteStatementsForContext(contextId: number, siteId?: string): Promise {
+        const site = await CoreSites.instance.getSite(siteId);
+
+        await site.getDb().deleteRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {contextid: contextId});
+    }
+
+    /**
+     * Get all offline statements.
+     *
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with all the data.
+     */
+    async getAllStatements(siteId?: string): Promise {
+        const site = await CoreSites.instance.getSite(siteId);
+
+        return site.getDb().getRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, undefined, 'timecreated ASC');
+    }
+
+    /**
+     * Get statements for a context.
+     *
+     * @param contextId Context ID.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with the data.
+     */
+    async getContextStatements(contextId: number, siteId?: string): Promise {
+        const site = await CoreSites.instance.getSite(siteId);
+
+        return site.getDb().getRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {contextid: contextId}, 'timecreated ASC');
+    }
+
+    /**
+     * Get certain statements.
+     *
+     * @param id ID of the statements.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with the data.
+     */
+    async getStatements(id: number, siteId?: string): Promise {
+        const site = await CoreSites.instance.getSite(siteId);
+
+        return site.getDb().getRecord(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {id});
+    }
+
+    /**
+     * Save statements.
+     *
+     * @param contextId Context ID.
+     * @param component  Component to send the statements to.
+     * @param statements Statements (JSON-encoded).
+     * @param options Options.
+     * @return Promise resolved when statements are successfully saved.
+     */
+    async saveStatements(contextId: number, component: string, statements: string, options?: CoreXAPIOfflineSaveStatementsOptions)
+            : Promise {
+
+        const site = await CoreSites.instance.getSite(options.siteId);
+
+        const entry = {
+            contextid: contextId,
+            component: component,
+            statements: statements,
+            timecreated: Date.now(),
+            courseid: options.courseId,
+            extra: options.extra,
+        };
+
+        await site.getDb().insertRecord(CoreXAPIOfflineProvider.STATEMENTS_TABLE, entry);
+    }
+}
+
+export class CoreXAPIOffline extends makeSingleton(CoreXAPIOfflineProvider) {}
+
+/**
+ * DB data stored for statements.
+ */
+export type CoreXAPIOfflineStatementsDBData = {
+    id: number; // ID.
+    contextid: number; // Context ID of the statements.
+    component: string; // Component to send the statements to.
+    statements: string; // Statements (JSON-encoded).
+    timecreated: number; // When were the statements created.
+    courseid?: number; // Course ID if the context is inside a course.
+    extra?: string; // Extra data.
+};
+
+/**
+ * Options to pass to saveStatements function.
+ */
+export type CoreXAPIOfflineSaveStatementsOptions = {
+    courseId?: number; // Course ID if the context is inside a course.
+    extra?: string; // Extra data to store.
+    siteId?: string; // Site ID. If not defined, current site.
+};
diff --git a/src/core/xapi/providers/xapi.ts b/src/core/xapi/providers/xapi.ts
index e9cccc985..997d02763 100644
--- a/src/core/xapi/providers/xapi.ts
+++ b/src/core/xapi/providers/xapi.ts
@@ -13,9 +13,12 @@
 // limitations under the License.
 
 import { Injectable } from '@angular/core';
+import { CoreApp } from '@providers/app';
 import { CoreSites } from '@providers/sites';
 import { CoreTextUtils } from '@providers/utils/text';
+import { CoreUtils } from '@providers/utils/utils';
 import { CoreSite } from '@classes/site';
+import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline';
 
 import { makeSingleton } from '@singletons/core.singletons';
 
@@ -34,10 +37,10 @@ export class CoreXAPIProvider {
      * @return Promise resolved with true if ws is available, false otherwise.
      * @since 3.9
      */
-    async canPostStatement(siteId?: string): Promise {
+    async canPostStatements(siteId?: string): Promise {
         const site = await CoreSites.instance.getSite(siteId);
 
-        return this.canPostStatementInSite(site);
+        return this.canPostStatementsInSite(site);
     }
 
     /**
@@ -47,7 +50,7 @@ export class CoreXAPIProvider {
      * @return Promise resolved with true if ws is available, false otherwise.
      * @since 3.9
      */
-    canPostStatementInSite(site?: CoreSite): boolean {
+    canPostStatementsInSite(site?: CoreSite): boolean {
         site = site || CoreSites.instance.getCurrentSite();
 
         return site.wsAvailable('core_xapi_statement_post');
@@ -68,14 +71,56 @@ export class CoreXAPIProvider {
     }
 
     /**
-     * Post an statement.
+     * Post statements.
+     *
+     * @param contextId Context ID.
+     * @param component Component.
+     * @param json JSON string to send.
+     * @param options Options.
+     * @return Promise resolved with boolean: true if response was sent to server, false if stored in device.
+     */
+    async postStatements(contextId: number, component: string, json: string, options?: CoreXAPIPostStatementsOptions)
+            : Promise {
+
+        options = options || {};
+        options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
+
+        // Convenience function to store a message to be synchronized later.
+        const storeOffline = async (): Promise => {
+            await CoreXAPIOffline.instance.saveStatements(contextId, component, json, options);
+
+            return false;
+        };
+
+        if (!CoreApp.instance.isOnline() || options.offline) {
+            // App is offline, store the action.
+            return storeOffline();
+        }
+
+        try {
+            await this.postStatementsOnline(component, json, options.siteId);
+
+            return true;
+        } catch (error) {
+            if (CoreUtils.instance.isWebServiceError(error)) {
+                // The WebService has thrown an error, this means that responses cannot be submitted.
+                throw error;
+            } else {
+                // Couldn't connect to server, store it offline.
+                return storeOffline();
+            }
+        }
+    }
+
+    /**
+     * Post statements. It will fail if offline or cannot connect.
      *
      * @param component Component.
      * @param json JSON string to send.
      * @param siteId Site ID. If not defined, current site.
      * @return Promise resolved when done.
      */
-    async postStatement(component: string, json: string, siteId?: string): Promise {
+    async postStatementsOnline(component: string, json: string, siteId?: string): Promise {
 
         const site = await CoreSites.instance.getSite(siteId);
 
@@ -89,3 +134,10 @@ export class CoreXAPIProvider {
 }
 
 export class CoreXAPI extends makeSingleton(CoreXAPIProvider) {}
+
+/**
+ * Options to pass to postStatements function.
+ */
+export type CoreXAPIPostStatementsOptions = CoreXAPIOfflineSaveStatementsOptions & {
+    offline?: boolean; // Whether to force storing it in offline.
+};
diff --git a/src/core/xapi/xapi.module.ts b/src/core/xapi/xapi.module.ts
index 0d3a9ee2f..7a955bffe 100644
--- a/src/core/xapi/xapi.module.ts
+++ b/src/core/xapi/xapi.module.ts
@@ -14,10 +14,12 @@
 
 import { NgModule } from '@angular/core';
 import { CoreXAPIProvider } from './providers/xapi';
+import { CoreXAPIOfflineProvider } from './providers/offline';
 
 // List of providers (without handlers).
 export const CORE_XAPI_PROVIDERS: any[] = [
     CoreXAPIProvider,
+    CoreXAPIOfflineProvider,
 ];
 
 @NgModule({
@@ -25,6 +27,7 @@ export const CORE_XAPI_PROVIDERS: any[] = [
     imports: [],
     providers: [
         CoreXAPIProvider,
+        CoreXAPIOfflineProvider,
     ],
     exports: []
 })
diff --git a/src/lang/en.json b/src/lang/en.json
index d925a1bea..5048707e4 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -154,6 +154,7 @@
     "mod_folder": "Folder",
     "mod_forum": "Forum",
     "mod_glossary": "Glossary",
+    "mod_h5pactivity": "H5P",
     "mod_ims": "IMS content package",
     "mod_imscp": "IMS content package",
     "mod_label": "Label",