From f090f8f33a85aaf5265fbed7e9e9b48f38011c7a Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Tue, 28 Aug 2018 11:46:53 +0200
Subject: [PATCH 1/3] MOBILE-2539 url: Support appearance settings

---
 .../components/index/addon-mod-url-index.html | 33 ++++++--
 src/addon/mod/url/components/index/index.scss |  6 ++
 src/addon/mod/url/components/index/index.ts   | 59 ++++++++++++-
 src/addon/mod/url/providers/module-handler.ts | 83 ++++++++++++++++---
 src/addon/mod/url/providers/url.ts            | 83 ++++++++++++++++++-
 src/core/constants.ts                         |  9 ++
 src/providers/utils/mimetype.ts               | 12 ++-
 7 files changed, 260 insertions(+), 25 deletions(-)
 create mode 100644 src/addon/mod/url/components/index/index.scss

diff --git a/src/addon/mod/url/components/index/addon-mod-url-index.html b/src/addon/mod/url/components/index/addon-mod-url-index.html
index 7f4f8dd28..00e14a049 100644
--- a/src/addon/mod/url/components/index/addon-mod-url-index.html
+++ b/src/addon/mod/url/components/index/addon-mod-url-index.html
@@ -12,14 +12,29 @@
 
     <core-course-module-description *ngIf="mode != 'iframe'" [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
 
-    <ion-item text-wrap>
-        <h2>{{ 'addon.mod_url.pointingtourl' | translate }}</h2>
-        <p>{{ url }}</p>
-    </ion-item>
-    <div padding>
-        <a ion-button block (click)="go()" icon-start>
-            <ion-icon name="link" start></ion-icon>
-            {{ 'addon.mod_url.accessurl' | translate }}
-        </a>
+    <div *ngIf="shouldEmbed" class="addon-mod_url-embedded-url">
+        <img *ngIf="isImage" title="{{name}}" [src]="url">
+        <video *ngIf="isVideo" title="{{name}}" controls>
+            <source [src]="url" [type]="mimetype">
+        </video>
+        <audio *ngIf="isAudio" title="{{name}}" controls>
+            <source [src]="url" [type]="mimetype">
+        </audio>
+        <core-iframe *ngIf="!isImage && !isVideo && !isAudio" [src]="url"></core-iframe>
     </div>
+
+    <core-iframe *ngIf="shouldIframe" [src]="url"></core-iframe>
+
+    <ion-list *ngIf="!shouldEmbed && !shouldIframe">
+        <ion-item text-wrap>
+            <h2>{{ 'addon.mod_url.pointingtourl' | translate }}</h2>
+            <p>{{ url }}</p>
+        </ion-item>
+        <div padding>
+            <a ion-button block (click)="go()" icon-start>
+                <ion-icon name="link" start></ion-icon>
+                {{ 'addon.mod_url.accessurl' | translate }}
+            </a>
+        </div>
+    </ion-list>
 </core-loading>
diff --git a/src/addon/mod/url/components/index/index.scss b/src/addon/mod/url/components/index/index.scss
new file mode 100644
index 000000000..a9d107124
--- /dev/null
+++ b/src/addon/mod/url/components/index/index.scss
@@ -0,0 +1,6 @@
+ion-app.app-root addon-mod-url-index {
+
+    .addon-mod_url-embedded-url {
+        height: 100%;
+    }
+}
diff --git a/src/addon/mod/url/components/index/index.ts b/src/addon/mod/url/components/index/index.ts
index d1602299f..16c7942a8 100644
--- a/src/addon/mod/url/components/index/index.ts
+++ b/src/addon/mod/url/components/index/index.ts
@@ -13,10 +13,13 @@
 // limitations under the License.
 
 import { Component, Injector } from '@angular/core';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
 import { CoreCourseProvider } from '@core/course/providers/course';
 import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
 import { AddonModUrlProvider } from '../../providers/url';
 import { AddonModUrlHelperProvider } from '../../providers/helper';
+import { CoreConstants } from '@core/constants';
 
 /**
  * Component that displays a url.
@@ -30,9 +33,17 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
 
     canGetUrl: boolean;
     url: string;
+    name: string;
+    shouldEmbed = false;
+    shouldIframe = false;
+    isImage = false;
+    isAudio = false;
+    isVideo = false;
+    mimetype: string;
 
     constructor(injector: Injector, private urlProvider: AddonModUrlProvider, private courseProvider: CoreCourseProvider,
-            private urlHelper: AddonModUrlHelperProvider) {
+            private urlHelper: AddonModUrlHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider,
+            private sitesProvider: CoreSitesProvider) {
         super(injector);
     }
 
@@ -65,6 +76,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
     protected fetchContent(refresh?: boolean): Promise<any> {
         let canGetUrl = this.canGetUrl,
             mod,
+            url,
             promise;
 
         // Fetch the module data.
@@ -79,7 +91,10 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
 
             // Fallback in case is not prefetched or not available.
             return this.courseProvider.getModule(this.module.id, this.courseId, undefined, false, false, undefined, 'url');
-        }).then((url) => {
+        }).then((urlData) => {
+            url = urlData;
+
+            this.name = url.name || this.module.name;
             this.description = url.intro || url.description;
             this.dataRetrieved.emit(url);
 
@@ -101,9 +116,49 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
         }).then(() => {
             // Always use the URL from the module because it already includes the parameters.
             this.url = mod.contents && mod.contents[0] && mod.contents[0].fileurl ? mod.contents[0].fileurl : undefined;
+
+            if (canGetUrl) {
+                return this.calculateDisplayOptions(url);
+            }
         });
     }
 
+    /**
+     * Calculate the display options to determine how the URL should be rendered.
+     *
+     * @param {any} url Object with the URL data.
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    protected calculateDisplayOptions(url: any): Promise<any> {
+        const displayType = this.urlProvider.getFinalDisplayType(url);
+
+        this.shouldEmbed = displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED;
+        this.shouldIframe = displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME;
+
+        if (this.shouldEmbed) {
+            const extension = this.mimeUtils.guessExtensionFromUrl(url.externalurl);
+
+            this.mimetype = this.mimeUtils.getMimeType(extension);
+            this.isImage = this.mimeUtils.isExtensionInGroup(extension, ['web_image']);
+            this.isAudio = this.mimeUtils.isExtensionInGroup(extension, ['web_audio']);
+            this.isVideo = this.mimeUtils.isExtensionInGroup(extension, ['web_video']);
+        }
+
+        if (this.shouldIframe || (this.shouldEmbed && !this.isImage && !this.isAudio && !this.isVideo)) {
+            // Will be displayed in an iframe. Check if we need to auto-login.
+            const currentSite = this.sitesProvider.getCurrentSite();
+
+            if (currentSite && currentSite.containsUrl(this.url)) {
+                // Format the URL to add auto-login.
+                return currentSite.getAutoLoginUrl(this.url, false).then((url) => {
+                    this.url = url;
+                });
+            }
+        }
+
+        return Promise.resolve();
+    }
+
     /**
      * Opens a file.
      */
diff --git a/src/addon/mod/url/providers/module-handler.ts b/src/addon/mod/url/providers/module-handler.ts
index 57480c0d5..54ccb6ead 100644
--- a/src/addon/mod/url/providers/module-handler.ts
+++ b/src/addon/mod/url/providers/module-handler.ts
@@ -14,11 +14,13 @@
 
 import { Injectable } from '@angular/core';
 import { NavController, NavOptions } from 'ionic-angular';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
 import { AddonModUrlIndexComponent } from '../components/index/index';
 import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
 import { CoreCourseProvider } from '@core/course/providers/course';
 import { AddonModUrlProvider } from './url';
 import { AddonModUrlHelperProvider } from './helper';
+import { CoreConstants } from '@core/constants';
 
 /**
  * Handler to support url modules.
@@ -29,7 +31,7 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
     modName = 'url';
 
     constructor(private courseProvider: CoreCourseProvider, private urlProvider: AddonModUrlProvider,
-        private urlHelper: AddonModUrlHelperProvider) { }
+        private urlHelper: AddonModUrlHelperProvider, private domUtils: CoreDomUtilsProvider) { }
 
     /**
      * Check if the handler is enabled on a site level.
@@ -49,33 +51,60 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
      * @return {CoreCourseModuleHandlerData} Data to render the module.
      */
     getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
+        // tslint:disable: no-this-assignment
+        const handler = this;
         const handlerData = {
             icon: this.courseProvider.getModuleIconSrc(this.modName),
             title: module.name,
             class: 'addon-mod_url-handler',
             showDownloadButton: false,
             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
-                navCtrl.push('AddonModUrlIndexPage', {module: module, courseId: courseId}, options);
+                // Check if we need to open the URL directly.
+                let promise;
+
+                if (handler.urlProvider.isGetUrlWSAvailable()) {
+                    const modal = handler.domUtils.showModalLoading();
+
+                    promise = handler.urlProvider.getUrl(courseId, module.id).catch(() => {
+                        // Ignore errors.
+                    }).then((url) => {
+                        modal.dismiss();
+
+                        const displayType = handler.urlProvider.getFinalDisplayType(url);
+
+                        return displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN ||
+                               displayType == CoreConstants.RESOURCELIB_DISPLAY_POPUP;
+                    });
+                } else {
+                    promise = Promise.resolve(false);
+                }
+
+                return promise.then((shouldOpen) => {
+                    if (shouldOpen) {
+                        handler.openUrl(module, courseId);
+                    } else {
+                        navCtrl.push('AddonModUrlIndexPage', {module: module, courseId: courseId}, options);
+                    }
+                });
             },
             buttons: [ {
-                hidden: !(module.contents && module.contents[0] && module.contents[0].fileurl),
+                hidden: true, // Hide it until we calculate if it should be displayed or not.
                 icon: 'link',
                 label: 'core.openinbrowser',
                 action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => {
-                    this.hideLinkButton(module, courseId).then((hide) => {
-                        if (!hide) {
-                            this.urlProvider.logView(module.instance).then(() => {
-                                this.courseProvider.checkModuleCompletion(courseId, module.completionstatus);
-                            });
-                            this.urlHelper.open(module.contents[0].fileurl);
-                        }
-                    });
+                    handler.openUrl(module, courseId);
                 }
             } ]
         };
 
         this.hideLinkButton(module, courseId).then((hideButton) => {
             handlerData.buttons[0].hidden = hideButton;
+
+            if (module.contents && module.contents[0]) {
+                // Calculate the icon to use.
+                handlerData.icon = this.urlProvider.guessIcon(module.contents[0].fileurl) ||
+                        this.courseProvider.getModuleIconSrc(this.modName);
+            }
         });
 
         return handlerData;
@@ -91,7 +120,24 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
     protected hideLinkButton(module: any, courseId: number): Promise<boolean> {
         return this.courseProvider.loadModuleContents(module, courseId, undefined, false, false, undefined, this.modName)
                 .then(() => {
-            return !(module.contents && module.contents[0] && module.contents[0].fileurl);
+
+            if (!module.contents || !module.contents[0] || !module.contents[0].fileurl) {
+                // No module contents, hide the button.
+                return true;
+            }
+
+            if (!this.urlProvider.isGetUrlWSAvailable()) {
+                return false;
+            }
+
+            // Get the URL data.
+            return this.urlProvider.getUrl(courseId, module.id).then((url) => {
+                const displayType = this.urlProvider.getFinalDisplayType(url);
+
+                // Don't display the button if the URL should be embedded.
+                return displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED ||
+                        displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME;
+            });
         });
     }
 
@@ -106,4 +152,17 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
     getMainComponent(course: any, module: any): any {
         return AddonModUrlIndexComponent;
     }
+
+    /**
+     * Open the URL.
+     *
+     * @param {any} module The module object.
+     * @param {number} courseId The course ID.
+     */
+    protected openUrl(module: any, courseId: number): void {
+        this.urlProvider.logView(module.instance).then(() => {
+            this.courseProvider.checkModuleCompletion(courseId, module.completionstatus);
+        });
+        this.urlHelper.open(module.contents[0].fileurl);
+    }
 }
diff --git a/src/addon/mod/url/providers/url.ts b/src/addon/mod/url/providers/url.ts
index 19750f8c7..c1642b6ca 100644
--- a/src/addon/mod/url/providers/url.ts
+++ b/src/addon/mod/url/providers/url.ts
@@ -15,8 +15,10 @@
 import { Injectable } from '@angular/core';
 import { CoreLoggerProvider } from '@providers/logger';
 import { CoreSitesProvider } from '@providers/sites';
+import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
 import { CoreUtilsProvider } from '@providers/utils/utils';
 import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreConstants } from '@core/constants';
 
 /**
  * Service that provides some features for urls.
@@ -29,10 +31,62 @@ export class AddonModUrlProvider {
     protected logger;
 
     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider,
-            private utils: CoreUtilsProvider) {
+            private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider) {
         this.logger = logger.getInstance('AddonModUrlProvider');
     }
 
+    /**
+     * Get the final display type for a certain URL. Based on Moodle's url_get_final_display_type.
+     *
+     * @param {any} url URL data.
+     * @return {number} Final display type.
+     */
+    getFinalDisplayType(url: any): number {
+        if (!url) {
+            return -1;
+        }
+
+        const extension = this.mimeUtils.guessExtensionFromUrl(url.externalurl);
+
+        // PDFs can be embedded in web, but not in the Mobile app.
+        if (url.display == CoreConstants.RESOURCELIB_DISPLAY_EMBED && extension == 'pdf') {
+            return CoreConstants.RESOURCELIB_DISPLAY_DOWNLOAD;
+        }
+
+        if (url.display != CoreConstants.RESOURCELIB_DISPLAY_AUTO) {
+            return url.display;
+        }
+
+        // Detect links to local moodle pages.
+        const currentSite = this.sitesProvider.getCurrentSite();
+        if (currentSite && currentSite.containsUrl(url.externalurl)) {
+            if (url.externalurl.indexOf('file.php') == -1 && url.externalurl.indexOf('.php') != -1) {
+                // Most probably our moodle page with navigation.
+                return CoreConstants.RESOURCELIB_DISPLAY_OPEN;
+            }
+        }
+
+        const download = ['application/zip', 'application/x-tar', 'application/g-zip', 'application/pdf', 'text/html'];
+        let mimetype = this.mimeUtils.getMimeType(extension);
+
+        if (url.externalurl.indexOf('.php') != -1 || url.externalurl.substr(-1) === '/' ||
+                (url.externalurl.indexOf('//') != -1 && url.externalurl.match(/\//g).length == 2)) {
+            // Seems to be a web, use HTML mimetype.
+            mimetype = 'text/html';
+        }
+
+        if (download.indexOf(mimetype) != -1) {
+            return CoreConstants.RESOURCELIB_DISPLAY_DOWNLOAD;
+        }
+
+        if (this.mimeUtils.canBeEmbedded(extension)) {
+            return CoreConstants.RESOURCELIB_DISPLAY_EMBED;
+        }
+
+        // Let the browser deal with it somehow.
+        return CoreConstants.RESOURCELIB_DISPLAY_OPEN;
+    }
+
     /**
      * Get cache key for url data WS calls.
      *
@@ -88,6 +142,33 @@ export class AddonModUrlProvider {
         return this.getUrlDataByKey(courseId, 'coursemodule', cmId, siteId);
     }
 
+    /**
+     * Guess the icon for a certain URL. Based on Moodle's url_guess_icon.
+     *
+     * @param {string} url URL to check.
+     * @return {string} Icon, empty if it should use the default icon.
+     */
+    guessIcon(url: string): string {
+        url = url || '';
+
+        const matches = url.match(/\//g),
+            extension = this.mimeUtils.getFileExtension(url);
+
+        if (!matches || matches.length < 3 || url.substr(-1) === '/' || extension == 'php') {
+            // Use default icon.
+            return '';
+        }
+
+        const icon = this.mimeUtils.getFileIcon(url);
+
+        // We do not want to return those icon types, the module icon is more appropriate.
+        if (icon === this.mimeUtils.getFileIconForType('unknown') || icon === this.mimeUtils.getFileIconForType('html')) {
+            return '';
+        }
+
+        return icon;
+    }
+
     /**
      * Invalidate the prefetched content.
      *
diff --git a/src/core/constants.ts b/src/core/constants.ts
index ac25f07b6..f93bb41d5 100644
--- a/src/core/constants.ts
+++ b/src/core/constants.ts
@@ -47,4 +47,13 @@ export class CoreConstants {
     static NOT_DOWNLOADED = 'notdownloaded';
     static OUTDATED = 'outdated';
     static NOT_DOWNLOADABLE = 'notdownloadable';
+
+    // Constants from Moodle's resourcelib.
+    static RESOURCELIB_DISPLAY_AUTO = 0; // Try the best way.
+    static RESOURCELIB_DISPLAY_EMBED = 1; // Display using object tag.
+    static RESOURCELIB_DISPLAY_FRAME = 2; // Display inside frame.
+    static RESOURCELIB_DISPLAY_NEW = 3; // Display normal link in new window.
+    static RESOURCELIB_DISPLAY_DOWNLOAD = 4; // Force download of file instead of display.
+    static RESOURCELIB_DISPLAY_OPEN = 5; // Open directly.
+    static RESOURCELIB_DISPLAY_POPUP = 6; // Open in "emulated" pop-up without navigation.
 }
diff --git a/src/providers/utils/mimetype.ts b/src/providers/utils/mimetype.ts
index a1bce44ce..9df797d94 100644
--- a/src/providers/utils/mimetype.ts
+++ b/src/providers/utils/mimetype.ts
@@ -186,7 +186,7 @@ export class CoreMimetypeUtilsProvider {
             }
         }
 
-        return 'assets/img/files/' + icon + '-64.png';
+        return this.getFileIconForType(icon);
     }
 
     /**
@@ -198,6 +198,16 @@ export class CoreMimetypeUtilsProvider {
         return 'assets/img/files/folder-64.png';
     }
 
+    /**
+     * Given a type (audio, video, html, ...), return its file icon path.
+     *
+     * @param {string} type The type to get the icon.
+     * @return {string} The icon path.
+     */
+    getFileIconForType(type: string): string {
+        return 'assets/img/files/' + type + '-64.png';
+    }
+
     /**
      * Guess the extension of a file from its URL.
      * This is very weak and unreliable.

From 6d39cd5c4ffd3aab8950c3cccac894ad1038b68a Mon Sep 17 00:00:00 2001
From: dpalou <dani@moodle.com>
Date: Tue, 25 Sep 2018 13:39:48 +0200
Subject: [PATCH 2/3] MOBILE-2539 url: Support display description setting

---
 src/addon/mod/url/components/index/addon-mod-url-index.html | 2 +-
 src/addon/mod/url/components/index/index.ts                 | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/addon/mod/url/components/index/addon-mod-url-index.html b/src/addon/mod/url/components/index/addon-mod-url-index.html
index 00e14a049..8bfe7d722 100644
--- a/src/addon/mod/url/components/index/addon-mod-url-index.html
+++ b/src/addon/mod/url/components/index/addon-mod-url-index.html
@@ -10,7 +10,7 @@
 <!-- Content. -->
 <core-loading [hideUntil]="loaded" class="core-loading-center">
 
-    <core-course-module-description *ngIf="mode != 'iframe'" [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
+    <core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
 
     <div *ngIf="shouldEmbed" class="addon-mod_url-embedded-url">
         <img *ngIf="isImage" title="{{name}}" [src]="url">
diff --git a/src/addon/mod/url/components/index/index.ts b/src/addon/mod/url/components/index/index.ts
index 16c7942a8..3b5c581ba 100644
--- a/src/addon/mod/url/components/index/index.ts
+++ b/src/addon/mod/url/components/index/index.ts
@@ -40,6 +40,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
     isAudio = false;
     isVideo = false;
     mimetype: string;
+    displayDescription = true;
 
     constructor(injector: Injector, private urlProvider: AddonModUrlProvider, private courseProvider: CoreCourseProvider,
             private urlHelper: AddonModUrlHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider,
@@ -98,6 +99,11 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
             this.description = url.intro || url.description;
             this.dataRetrieved.emit(url);
 
+            if (canGetUrl && url.displayoptions) {
+                const unserialized = this.textUtils.unserialize(url.displayoptions);
+                this.displayDescription = typeof unserialized.printintro == 'undefined' || !!unserialized.printintro;
+            }
+
             if (!canGetUrl) {
                 mod = url;
 

From 71bd461c42b3b8a7b2a520701984fc27bfaec111 Mon Sep 17 00:00:00 2001
From: dpalou <dani@moodle.com>
Date: Thu, 4 Oct 2018 16:42:14 +0200
Subject: [PATCH 3/3] MOBILE-2539 url: Always show open button and don't embed
 internal links

---
 .../components/index/addon-mod-url-index.html | 24 +++----
 src/addon/mod/url/components/index/index.ts   |  2 +
 src/addon/mod/url/providers/module-handler.ts | 62 ++++++++-----------
 src/core/contentlinks/providers/helper.ts     | 16 +++++
 4 files changed, 56 insertions(+), 48 deletions(-)

diff --git a/src/addon/mod/url/components/index/addon-mod-url-index.html b/src/addon/mod/url/components/index/addon-mod-url-index.html
index 8bfe7d722..422095419 100644
--- a/src/addon/mod/url/components/index/addon-mod-url-index.html
+++ b/src/addon/mod/url/components/index/addon-mod-url-index.html
@@ -12,20 +12,20 @@
 
     <core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
 
-    <div *ngIf="shouldEmbed" class="addon-mod_url-embedded-url">
-        <img *ngIf="isImage" title="{{name}}" [src]="url">
-        <video *ngIf="isVideo" title="{{name}}" controls>
-            <source [src]="url" [type]="mimetype">
-        </video>
-        <audio *ngIf="isAudio" title="{{name}}" controls>
-            <source [src]="url" [type]="mimetype">
-        </audio>
-        <core-iframe *ngIf="!isImage && !isVideo && !isAudio" [src]="url"></core-iframe>
+    <div *ngIf="shouldIframe || (shouldEmbed && isOther)" class="addon-mod_url-embedded-url">
+        <core-iframe [src]="url"></core-iframe>
     </div>
 
-    <core-iframe *ngIf="shouldIframe" [src]="url"></core-iframe>
-
-    <ion-list *ngIf="!shouldEmbed && !shouldIframe">
+    <ion-list *ngIf="!shouldIframe && (!shouldEmbed || !isOther)">
+        <ion-item *ngIf="shouldEmbed">
+            <img *ngIf="isImage" title="{{name}}" [src]="url">
+            <video *ngIf="isVideo" title="{{name}}" controls>
+                <source [src]="url" [type]="mimetype">
+            </video>
+            <audio *ngIf="isAudio" title="{{name}}" controls>
+                <source [src]="url" [type]="mimetype">
+            </audio>
+        </ion-item>
         <ion-item text-wrap>
             <h2>{{ 'addon.mod_url.pointingtourl' | translate }}</h2>
             <p>{{ url }}</p>
diff --git a/src/addon/mod/url/components/index/index.ts b/src/addon/mod/url/components/index/index.ts
index 3b5c581ba..589de7cbf 100644
--- a/src/addon/mod/url/components/index/index.ts
+++ b/src/addon/mod/url/components/index/index.ts
@@ -39,6 +39,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
     isImage = false;
     isAudio = false;
     isVideo = false;
+    isOther = false;
     mimetype: string;
     displayDescription = true;
 
@@ -148,6 +149,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
             this.isImage = this.mimeUtils.isExtensionInGroup(extension, ['web_image']);
             this.isAudio = this.mimeUtils.isExtensionInGroup(extension, ['web_audio']);
             this.isVideo = this.mimeUtils.isExtensionInGroup(extension, ['web_video']);
+            this.isOther = !this.isImage && !this.isAudio && !this.isVideo;
         }
 
         if (this.shouldIframe || (this.shouldEmbed && !this.isImage && !this.isAudio && !this.isVideo)) {
diff --git a/src/addon/mod/url/providers/module-handler.ts b/src/addon/mod/url/providers/module-handler.ts
index 54ccb6ead..e182343fb 100644
--- a/src/addon/mod/url/providers/module-handler.ts
+++ b/src/addon/mod/url/providers/module-handler.ts
@@ -18,6 +18,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
 import { AddonModUrlIndexComponent } from '../components/index/index';
 import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
 import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
 import { AddonModUrlProvider } from './url';
 import { AddonModUrlHelperProvider } from './helper';
 import { CoreConstants } from '@core/constants';
@@ -31,7 +32,8 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
     modName = 'url';
 
     constructor(private courseProvider: CoreCourseProvider, private urlProvider: AddonModUrlProvider,
-        private urlHelper: AddonModUrlHelperProvider, private domUtils: CoreDomUtilsProvider) { }
+        private urlHelper: AddonModUrlHelperProvider, private domUtils: CoreDomUtilsProvider,
+        private contentLinksHelper: CoreContentLinksHelperProvider) { }
 
     /**
      * Check if the handler is enabled on a site level.
@@ -59,32 +61,37 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
             class: 'addon-mod_url-handler',
             showDownloadButton: false,
             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
-                // Check if we need to open the URL directly.
-                let promise;
+                const modal = handler.domUtils.showModalLoading();
 
-                if (handler.urlProvider.isGetUrlWSAvailable()) {
-                    const modal = handler.domUtils.showModalLoading();
+                // First of all, check if the URL can be handled by the app. If so, always open it directly.
+                handler.contentLinksHelper.canHandleLink(module.contents[0].fileurl, courseId).then((canHandle) => {
+                    if (canHandle) {
+                        // URL handled by the app, open it directly.
+                        return true;
+                    }
 
-                    promise = handler.urlProvider.getUrl(courseId, module.id).catch(() => {
-                        // Ignore errors.
-                    }).then((url) => {
-                        modal.dismiss();
+                    // Not handled by the app, check the display type.
+                    if (handler.urlProvider.isGetUrlWSAvailable()) {
+                        return handler.urlProvider.getUrl(courseId, module.id).catch(() => {
+                            // Ignore errors.
+                        }).then((url) => {
+                            const displayType = handler.urlProvider.getFinalDisplayType(url);
 
-                        const displayType = handler.urlProvider.getFinalDisplayType(url);
+                            return displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN ||
+                                   displayType == CoreConstants.RESOURCELIB_DISPLAY_POPUP;
+                        });
+                    } else {
+                        return false;
+                    }
 
-                        return displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN ||
-                               displayType == CoreConstants.RESOURCELIB_DISPLAY_POPUP;
-                    });
-                } else {
-                    promise = Promise.resolve(false);
-                }
-
-                return promise.then((shouldOpen) => {
+                }).then((shouldOpen) => {
                     if (shouldOpen) {
                         handler.openUrl(module, courseId);
                     } else {
                         navCtrl.push('AddonModUrlIndexPage', {module: module, courseId: courseId}, options);
                     }
+                }).finally(() => {
+                    modal.dismiss();
                 });
             },
             buttons: [ {
@@ -120,24 +127,7 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler {
     protected hideLinkButton(module: any, courseId: number): Promise<boolean> {
         return this.courseProvider.loadModuleContents(module, courseId, undefined, false, false, undefined, this.modName)
                 .then(() => {
-
-            if (!module.contents || !module.contents[0] || !module.contents[0].fileurl) {
-                // No module contents, hide the button.
-                return true;
-            }
-
-            if (!this.urlProvider.isGetUrlWSAvailable()) {
-                return false;
-            }
-
-            // Get the URL data.
-            return this.urlProvider.getUrl(courseId, module.id).then((url) => {
-                const displayType = this.urlProvider.getFinalDisplayType(url);
-
-                // Don't display the button if the URL should be embedded.
-                return displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED ||
-                        displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME;
-            });
+            return !(module.contents && module.contents[0] && module.contents[0].fileurl);
         });
     }
 
diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts
index 8cd2660d8..ad3d9b6f5 100644
--- a/src/core/contentlinks/providers/helper.ts
+++ b/src/core/contentlinks/providers/helper.ts
@@ -47,6 +47,22 @@ export class CoreContentLinksHelperProvider {
         eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, this.handleCustomUrl.bind(this));
     }
 
+    /**
+     * Check whether a link can be handled by the app.
+     *
+     * @param {string} url URL to handle.
+     * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+     * @param {string} [username] Username to use to filter sites.
+     * @return {Promise<boolean>} Promise resolved with a boolean: whether the URL can be handled.
+     */
+    canHandleLink(url: string, courseId?: number, username?: string): Promise<boolean> {
+        return this.contentLinksDelegate.getActionsFor(url, undefined, username).then((actions) => {
+            return !!this.getFirstValidAction(actions);
+        }).catch(() => {
+            return false;
+        });
+    }
+
     /**
      * Get the first valid action in a list of actions.
      *