diff --git a/www/addons/mod_book/services/handlers.js b/www/addons/mod_book/services/handlers.js index fab00cc24f1..d386e7909f6 100644 --- a/www/addons/mod_book/services/handlers.js +++ b/www/addons/mod_book/services/handlers.js @@ -68,7 +68,7 @@ angular.module('mm.addons.mod_book') e.preventDefault(); e.stopPropagation(); var size = $mmaModBookPrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModBook, module, size, false); + $mmCourseHelper.prefetchModule($scope, $mmaModBook, module, size, false); } }; @@ -80,7 +80,7 @@ angular.module('mm.addons.mod_book') e.preventDefault(); e.stopPropagation(); var size = $mmaModBookPrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModBook, module, size, true); + $mmCourseHelper.prefetchModule($scope, $mmaModBook, module, size, true); } }; diff --git a/www/addons/mod_folder/services/handlers.js b/www/addons/mod_folder/services/handlers.js index 67092f0819f..46e5f28520e 100644 --- a/www/addons/mod_folder/services/handlers.js +++ b/www/addons/mod_folder/services/handlers.js @@ -73,7 +73,7 @@ angular.module('mm.addons.mod_folder') // Check size and show confirmation if needed. var size = $mmaModFolderPrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModFolder, module, size, false); + $mmCourseHelper.prefetchModule($scope, $mmaModFolder, module, size, false); } downloadBtn = { diff --git a/www/addons/mod_imscp/services/handlers.js b/www/addons/mod_imscp/services/handlers.js index 6c34a643a3c..c1a32ec5990 100644 --- a/www/addons/mod_imscp/services/handlers.js +++ b/www/addons/mod_imscp/services/handlers.js @@ -74,7 +74,7 @@ angular.module('mm.addons.mod_imscp') e.preventDefault(); e.stopPropagation(); var size = $mmaModImscpPrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModImscp, module, size, false); + $mmCourseHelper.prefetchModule($scope, $mmaModImscp, module, size, false); } }; @@ -86,7 +86,7 @@ angular.module('mm.addons.mod_imscp') e.preventDefault(); e.stopPropagation(); var size = $mmaModImscpPrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModImscp, module, size, true); + $mmCourseHelper.prefetchModule($scope, $mmaModImscp, module, size, true); } }; diff --git a/www/addons/mod_page/services/handlers.js b/www/addons/mod_page/services/handlers.js index 2ea3d4ebac0..a55760e59d6 100644 --- a/www/addons/mod_page/services/handlers.js +++ b/www/addons/mod_page/services/handlers.js @@ -68,7 +68,7 @@ angular.module('mm.addons.mod_page') e.preventDefault(); e.stopPropagation(); var size = $mmaModPagePrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModPage, module, size, false); + $mmCourseHelper.prefetchModule($scope, $mmaModPage, module, size, false); } }; @@ -80,7 +80,7 @@ angular.module('mm.addons.mod_page') e.preventDefault(); e.stopPropagation(); var size = $mmaModPagePrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModPage, module, size, true); + $mmCourseHelper.prefetchModule($scope, $mmaModPage, module, size, true); } }; diff --git a/www/addons/mod_resource/services/handlers.js b/www/addons/mod_resource/services/handlers.js index e1fc480749d..ba58b745369 100644 --- a/www/addons/mod_resource/services/handlers.js +++ b/www/addons/mod_resource/services/handlers.js @@ -68,7 +68,7 @@ angular.module('mm.addons.mod_resource') e.preventDefault(); e.stopPropagation(); var size = $mmaModResourcePrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModResource, module, size, false); + $mmCourseHelper.prefetchModule($scope, $mmaModResource, module, size, false); } }; @@ -80,7 +80,7 @@ angular.module('mm.addons.mod_resource') e.preventDefault(); e.stopPropagation(); var size = $mmaModResourcePrefetchHandler.getDownloadSize(module); - $mmCourseHelper.prefetchModule($mmaModResource, module, size, true); + $mmCourseHelper.prefetchModule($scope, $mmaModResource, module, size, true); } }; diff --git a/www/addons/mod_resource/services/resource.js b/www/addons/mod_resource/services/resource.js index c54c84572f6..361e9f143c9 100644 --- a/www/addons/mod_resource/services/resource.js +++ b/www/addons/mod_resource/services/resource.js @@ -22,7 +22,7 @@ angular.module('mm.addons.mod_resource') * @name $mmaModResource */ .factory('$mmaModResource', function($mmFilepool, $mmSite, $mmUtil, $mmFS, $http, $log, $q, $sce, $mmApp, $mmSitesManager, - mmaModResourceComponent) { + mmaModResourceComponent, mmCoreNotDownloaded, mmCoreDownloading, mmCoreDownloaded) { $log = $log.getInstance('$mmaModResource'); var self = {}; @@ -356,20 +356,61 @@ angular.module('mm.addons.mod_resource') siteId = $mmSite.getId(), revision = $mmFilepool.getRevisionFromFileList(files), timeMod = $mmFilepool.getTimemodifiedFromFileList(files), + component = mmaModResourceComponent, + url = contents[0].fileurl, + fixedUrl = $mmSite.fixPluginfileURL(url), promise; if ($mmFS.isAvailable()) { // The file system is available. - promise = $mmFilepool.downloadPackage(siteId, files, mmaModResourceComponent, moduleId, revision, timeMod).then(function() { - return $mmFilepool.getUrlByUrl(siteId, contents[0].fileurl, mmaModResourceComponent, moduleId, timeMod); + promise = $mmFilepool.getPackageStatus(siteId, component, moduleId, revision, timeMod).then(function(status) { + var isWifi = !$mmApp.isNetworkAccessLimited(), + isOnline = $mmApp.isOnline(); + + if (status === mmCoreDownloaded) { + // Get the local file URL. + return $mmFilepool.getUrlByUrl(siteId, url, component, moduleId, timeMod); + } else if (status === mmCoreDownloading) { + // Return the online URL. + return fixedUrl; + } else { + if (!isOnline && status === mmCoreNotDownloaded) { + // Not downloaded and we're offline, reject. + return $q.reject(); + } + + return $mmFilepool.shouldDownloadBeforeOpen(fixedUrl, contents[0].filesize).then(function() { + // Download and then return the local URL. + return $mmFilepool.downloadPackage(siteId, files, component, moduleId, revision, timeMod).then(function() { + return $mmFilepool.getUrlByUrl(siteId, url, component, moduleId, timeMod); + }); + }, function() { + // Start the download if in wifi, but return the URL right away so the file is opened. + if (isWifi && isOnline) { + $mmFilepool.downloadPackage(siteId, files, component, moduleId, revision, timeMod); + } + + if (status === mmCoreNotDownloaded || isOnline) { + // Not downloaded or outdated and online, return the online URL. + return fixedUrl; + } else { + // Outdated but offline, so we return the local URL. + return $mmFilepool.getUrlByUrl(siteId, url, component, moduleId, timeMod); + } + }); + } }); } else { // We use the live URL. - promise = $q.when($mmSite.fixPluginfileURL(url)); + promise = $q.when(fixedUrl); } - return promise.then(function(localUrl) { - return $mmUtil.openFile(localUrl); + return promise.then(function(url) { + if (url.indexOf('http') === 0) { + return $mmUtil.openOnlineFile(url); + } else { + return $mmUtil.openFile(url); + } }); }; diff --git a/www/core/components/course/services/helper.js b/www/core/components/course/services/helper.js index 6932a76e799..4f5298e0c9b 100644 --- a/www/core/components/course/services/helper.js +++ b/www/core/components/course/services/helper.js @@ -314,13 +314,14 @@ angular.module('mm.core.course') * @module mm.core.course * @ngdoc method * @name $mmCourseHelper#prefetchModule + * @param {Object} scope Scope. * @param {Object} service Service implementing 'invalidateContent' and 'prefetchContent'. * @param {Object} module Module to download. * @param {Number} size Size of the module. * @param {Boolean} refresh True if refreshing, false otherwise. * @return {Promise} Promise resolved when downloaded. */ - self.prefetchModule = function(service, module, size, refresh) { + self.prefetchModule = function(scope, service, module, size, refresh) { // Show confirmation if needed. return $mmUtil.confirmDownloadSize(size).then(function() { // Invalidate content if refreshing and download the data. @@ -329,7 +330,7 @@ angular.module('mm.core.course') // Ignore errors. }).then(function() { return service.prefetchContent(module).catch(function() { - if (!$scope.$$destroyed) { + if (!scope.$$destroyed) { $mmUtil.showErrorModal('mm.core.errordownloading', true); } }); diff --git a/www/core/directives/external_content.js b/www/core/directives/external_content.js index bfca4671bde..93f018f951d 100644 --- a/www/core/directives/external_content.js +++ b/www/core/directives/external_content.js @@ -31,7 +31,7 @@ angular.module('mm.core') * Attributes accepted: * - siteid: Reference to the site ID if different than the site the user is connected to. */ -.directive('mmExternalContent', function($log, $mmFilepool, $mmSite, $mmSitesManager, $mmUtil, $q) { +.directive('mmExternalContent', function($log, $mmFilepool, $mmSite, $mmSitesManager, $mmUtil, $q, $mmApp) { $log = $log.getInstance('mmExternalContent'); /** @@ -99,6 +99,27 @@ angular.module('mm.core') } else { dom.setAttribute(targetAttr, finalUrl); } + + // Set events to download big files (not downloaded automatically). + if (finalUrl.indexOf('http') === 0 && + (dom.tagName == 'VIDEO' || dom.tagName == 'AUDIO' || dom.tagName == 'A' || dom.tagName == 'SOURCE')) { + var eventName = dom.tagName == 'A' ? 'click' : 'play'; + + if (dom.tagName == 'SOURCE') { + dom = $mmUtil.closest(dom, 'video,audio'); + if (!dom) { + return; + } + } + + angular.element(dom).on(eventName, function() { + // User played media or opened a downloadable link. + // Download the file if in wifi and it hasn't been downloaded already (for big files). + if (!$mmApp.isNetworkAccessLimited()) { + fn(siteId, url, component, componentId, undefined, false); + } + }); + } }); }); } diff --git a/www/core/directives/file.js b/www/core/directives/file.js index a209929245e..94af2ec7e41 100644 --- a/www/core/directives/file.js +++ b/www/core/directives/file.js @@ -40,13 +40,13 @@ angular.module('mm.core') * Convenience function to get the file state and set scope variables based on it. * * @param {Object} scope Directive's scope. - * @param {String} siteid Site ID. - * @param {String} fileurl File URL. - * @param {Number} [timemodified] File's timemodified. + * @param {String} siteId Site ID. + * @param {String} fileUrl File URL. + * @param {Number} [timeModified] File's timemodified. * @return {Void} */ - function getState(scope, siteid, fileurl, timemodified) { - return $mmFilepool.getFileStateByUrl(siteid, fileurl, timemodified).then(function(state) { + function getState(scope, siteId, fileUrl, timeModified) { + return $mmFilepool.getFileStateByUrl(siteId, fileUrl, timeModified).then(function(state) { var canDownload = $mmSite.canDownloadFiles(); scope.isDownloaded = state === mmCoreDownloaded || state === mmCoreOutdated; scope.isDownloading = canDownload && state === mmCoreDownloading; @@ -58,25 +58,25 @@ angular.module('mm.core') * Convenience function to download a file. * * @param {Object} scope Directive's scope. - * @param {String} siteid Site ID. - * @param {String} fileurl File URL. + * @param {String} siteId Site ID. + * @param {String} fileUrl File URL. * @param {String} component Component the file belongs to. - * @param {Number} componentid Component ID. - * @param {Number} [timemodified] File's timemodified. + * @param {Number} componentId Component ID. + * @param {Number} [timeModified] File's timemodified. * @return {Promise} Promise resolved when file is downloaded. */ - function downloadFile(scope, siteid, fileurl, component, componentid, timemodified) { + function downloadFile(scope, siteId, fileUrl, component, componentId, timeModified) { if (!$mmSite.canDownloadFiles()) { $mmUtil.showErrorModal('mm.core.cannotdownloadfiles', true); return $q.reject(); } scope.isDownloading = true; - return $mmFilepool.downloadUrl(siteid, fileurl, true, component, componentid, timemodified).then(function(localUrl) { - getState(scope, siteid, fileurl, timemodified); // Update state. + return $mmFilepool.downloadUrl(siteId, fileUrl, false, component, componentId, timeModified).then(function(localUrl) { + getState(scope, siteId, fileUrl, timeModified); // Update state. return localUrl; }, function() { - return getState(scope, siteid, fileurl, timemodified).then(function() { + return getState(scope, siteId, fileUrl, timeModified).then(function() { if (scope.isDownloaded) { return localUrl; } else { @@ -86,6 +86,78 @@ angular.module('mm.core') }); } + /** + * Convenience function to open a file, downloading it if needed. + * + * @param {Object} scope Directive's scope. + * @param {String} siteId Site ID. + * @param {String} fileUrl File URL. + * @param {String} fileSize File size. + * @param {String} component Component the file belongs to. + * @param {Number} componentId Component ID. + * @param {Number} [timeModified] File's timemodified. + * @return {Promise} Promise resolved when file is opened. + */ + function openFile(scope, siteId, fileUrl, fileSize, component, componentId, timeModified) { + var fixedUrl = $mmSite.fixPluginfileURL(fileUrl), + promise; + + if ($mmFS.isAvailable()) { + promise = $q.when().then(function() { + // The file system is available. + var isWifi = !$mmApp.isNetworkAccessLimited(), + isOnline = $mmApp.isOnline(); + + if (scope.isDownloaded && !scope.showDownload) { + // Get the local file URL. + return $mmFilepool.getUrlByUrl(siteId, fileUrl, component, componentId, timeModified); + } else { + if (!isOnline && !scope.isDownloaded) { + // Not downloaded and we're offline, reject. + return $q.reject(); + } + + return $mmFilepool.shouldDownloadBeforeOpen(fixedUrl, fileSize).then(function() { + if (scope.isDownloading) { + // It's already downloading, stop. + return; + } + // Download and then return the local URL. + return downloadFile(scope, siteId, fileUrl, component, componentId, timeModified); + }, function() { + // Start the download if in wifi, but return the URL right away so the file is opened. + if (isWifi && isOnline) { + downloadFile(scope, siteId, fileUrl, component, componentId, timeModified); + } + + if (scope.isDownloading || !scope.isDownloaded || isOnline) { + // Not downloaded or outdated and online, return the online URL. + return fixedUrl; + } else { + // Outdated but offline, so we return the local URL. + return $mmFilepool.getUrlByUrl(siteId, fileUrl, component, componentId, timeModified); + } + }); + } + }); + } else { + // We use the live URL. + promise = $q.when(fixedUrl); + } + + return promise.then(function(url) { + if (!url) { + return; + } + + if (url.indexOf('http') === 0) { + return $mmUtil.openOnlineFile(url); + } else { + return $mmUtil.openFile(url); + } + }); + } + return { restrict: 'E', templateUrl: 'core/templates/file.html', @@ -93,22 +165,22 @@ angular.module('mm.core') file: '=' }, link: function(scope, element, attrs) { - var fileurl = scope.file.fileurl || scope.file.url, - filename = scope.file.filename, - filesize = scope.file.filesize, - timemodified = attrs.timemodified || 0, - siteid = $mmSite.getId(), + var fileUrl = scope.file.fileurl || scope.file.url, + fileName = scope.file.filename, + fileSize = scope.file.filesize, + timeModified = attrs.timemodified || 0, + siteId = $mmSite.getId(), component = attrs.component, - componentid = attrs.componentId, + componentId = attrs.componentId, observer; - scope.filename = filename; - scope.fileicon = $mmFS.getFileIcon(filename); - getState(scope, siteid, fileurl, timemodified); + scope.filename = fileName; + scope.fileicon = $mmFS.getFileIcon(fileName); + getState(scope, siteId, fileUrl, timeModified); - $mmFilepool.getFileEventNameByUrl(siteid, fileurl).then(function(eventName) { + $mmFilepool.getFileEventNameByUrl(siteId, fileUrl).then(function(eventName) { observer = $mmEvents.on(eventName, function(data) { - getState(scope, siteid, fileurl, timemodified); + getState(scope, siteId, fileUrl, timeModified); if (!data.success) { $mmUtil.showErrorModal('mm.core.errordownloading', true); } @@ -120,7 +192,7 @@ angular.module('mm.core') e.stopPropagation(); var promise; - if (scope.isDownloading) { + if (scope.isDownloading && !openAfterDownload) { return; } @@ -131,19 +203,17 @@ angular.module('mm.core') if (openAfterDownload) { // File needs to be opened now. If file needs to be downloaded, skip the queue. - downloadFile(scope, siteid, fileurl, component, componentid, timemodified).then(function(localUrl) { - $mmUtil.openFile(localUrl).catch(function(error) { - $mmUtil.showErrorModal(error); - }); + openFile(scope, siteId, fileUrl, fileSize, component, componentId, timeModified).catch(function(error) { + $mmUtil.showErrorModal(error); }); } else { // File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big. - promise = filesize ? $mmUtil.confirmDownloadSize(filesize) : $q.when(); + promise = fileSize ? $mmUtil.confirmDownloadSize(fileSize) : $q.when(); promise.then(function() { // User confirmed, add the file to queue. - $mmFilepool.invalidateFileByUrl(siteid, fileurl).finally(function() { + $mmFilepool.invalidateFileByUrl(siteId, fileUrl).finally(function() { scope.isDownloading = true; - $mmFilepool.addToQueueByUrl(siteid, fileurl, component, componentid, timemodified); + $mmFilepool.addToQueueByUrl(siteId, fileUrl, component, componentId, timeModified); }); }); } diff --git a/www/core/lib/filepool.js b/www/core/lib/filepool.js index 8759e923db7..a480cd228ea 100644 --- a/www/core/lib/filepool.js +++ b/www/core/lib/filepool.js @@ -176,7 +176,6 @@ angular.module('mm.core') $log = $log.getInstance('$mmFilepool'); var self = {}, - extensionRegex = new RegExp('^[a-z0-9]+$'), tokenRegex = new RegExp('(\\?|&)token=([A-Za-z0-9]+)'), queueState, urlAttributes = [ @@ -1238,7 +1237,7 @@ angular.module('mm.core') // part of the file name. Also, we need the mimetype to open the file with web intents. The easiest way to // provide such information is to keep the extension in the file ID. Developers should not care about it, // but as we are using the file ID in the file path, devs and system can guess it. - candidate = self._guessExtensionFromUrl(url); + candidate = $mmText.guessExtensionFromUrl(url); if (candidate && candidate !== 'php') { extension = '.' + candidate; } @@ -1274,7 +1273,7 @@ angular.module('mm.core') // web intents. The easiest way to provide such information is to keep the extension // in the file ID. Developers should not care about it, but as we are using the // file ID in the file path, devs and system can guess it. - candidate = self._guessExtensionFromUrl(url); + candidate = $mmText.guessExtensionFromUrl(url); if (candidate && candidate !== 'php') { extension = '.' + candidate; } @@ -1701,33 +1700,6 @@ angular.module('mm.core') return self._getFileUrlByUrl(siteId, fileUrl, 'url', component, componentId, timemodified, checkSize); }; - /** - * Guess the extension of a file from its URL. - * - * This is very weak and unreliable. - * - * @module mm.core - * @ngdoc method - * @name $mmFilepool#_guessExtensionFromUrl - * @param {String} fileUrl The file URL. - * @return {String} The lowercased extension without the dot, or undefined. - * @protected - */ - self._guessExtensionFromUrl = function(fileUrl) { - var split = fileUrl.split('.'), - candidate, - extension; - - if (split.length > 1) { - candidate = split.pop().toLowerCase(); - if (extensionRegex.test(candidate)) { - extension = candidate; - } - } - - return extension; - }; - /** * Guess the filename of a file from its URL. * @@ -2349,6 +2321,37 @@ angular.module('mm.core') }); }; + /** + * Convenience function to check if a file should be downloaded before opening it. + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#shouldDownloadBeforeOpen + * @param {String} url File online URL. + * @param {Number} size File size. + * @return {Promise} Promise resolved if should download before open, rejected otherwise. + * @description + * Convenience function to check if a file should be downloaded before opening it. + * + * The default behaviour in the app is to download first and then open the local file in the following cases: + * - The file is small (less than mmFilepoolDownloadThreshold). + * - The file cannot be streamed. + * If the file is big and can be streamed, the promise returned by this function will be rejected. + */ + self.shouldDownloadBeforeOpen = function(url, size) { + if (size >= 0 && size <= mmFilepoolDownloadThreshold) { + // The file is small, download it. + return $q.when(); + } + + return $mmUtil.getMimeType(url).then(function(mimetype) { + // If the file is streaming (audio or video) we reject. + if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) { + return $q.reject(); + } + }); + }; + /** * Store package status. * diff --git a/www/core/lib/text.js b/www/core/lib/text.js index 953eda3341d..b77d08d262a 100644 --- a/www/core/lib/text.js +++ b/www/core/lib/text.js @@ -23,7 +23,8 @@ angular.module('mm.core') */ .factory('$mmText', function($q, $mmLang, $translate) { - var self = {}; + var self = {}, + extensionRegex = new RegExp('^[a-z0-9]+$'); /** * Convert size in bytes into human readable format @@ -322,5 +323,38 @@ angular.module('mm.core') return filename; }; + /** + * Guess the extension of a file from its URL. + * + * This is very weak and unreliable. + * + * @module mm.core + * @ngdoc method + * @name $mmText#guessExtensionFromUrl + * @param {String} fileUrl The file URL. + * @return {String} The lowercased extension without the dot, or undefined. + */ + self.guessExtensionFromUrl = function(fileUrl) { + var split = fileUrl.split('.'), + candidate, + extension, + position; + + if (split.length > 1) { + candidate = split.pop().toLowerCase(); + // Remove params if any. + position = candidate.indexOf('?'); + if (position > -1) { + candidate = candidate.substr(0, position); + } + + if (extensionRegex.test(candidate)) { + extension = candidate; + } + } + + return extension; + }; + return self; }); diff --git a/www/core/lib/util.js b/www/core/lib/util.js index 8115b2c8e78..3c72ecd4869 100644 --- a/www/core/lib/util.js +++ b/www/core/lib/util.js @@ -66,11 +66,12 @@ angular.module('mm.core') }; this.$get = function($ionicLoading, $ionicPopup, $injector, $translate, $http, $log, $q, $mmLang, $mmFS, $timeout, $mmApp, - $mmText, mmCoreWifiDownloadThreshold, mmCoreDownloadThreshold, $ionicScrollDelegate, $cordovaInAppBrowser) { + $mmText, mmCoreWifiDownloadThreshold, mmCoreDownloadThreshold, $ionicScrollDelegate, $mmWS, $cordovaInAppBrowser) { $log = $log.getInstance('$mmUtil'); - var self = {}; // Use 'self' to be coherent with the rest of services. + var self = {}, // Use 'self' to be coherent with the rest of services. + matchesFn; /** * Formats a URL, trim, lowercase, etc... @@ -270,27 +271,19 @@ angular.module('mm.core') * * node-webkit: Using the default application configured. * Android: Using the WebIntent plugin. - * iOs: Using the window.open method. + * iOs: Using handleDocumentWithURL. * * @module mm.core * @ngdoc method * @name $mmUtil#openFile * @param {String} path The local path of the file to be open. * @return {Void} + * @todo Restore node-webkit support. */ self.openFile = function(path) { var deferred = $q.defer(); - if (false) { - // TODO Restore node-webkit support. - - // Link is the file path in the file system. - // We use the node-webkit shell for open the file (pdf, doc) using the default application configured in the os. - // var gui = require('nw.gui'); - // gui.Shell.openItem(path); - deferred.resolve(); - - } else if (window.plugins) { + if (window.plugins) { var extension = $mmFS.getFileExtension(path), mimetype = $mmFS.getMimeType(extension); @@ -423,6 +416,94 @@ angular.module('mm.core') $cordovaInAppBrowser.close(); }; + /** + * Open an online file using platform specific method. + * Specially useful for audio and video since they can be streamed. + * + * node-webkit: Using the default application configured. + * Android: Using the WebIntent plugin. + * iOS: Using the window.open method (InAppBrowser) + * We don't use iOS quickview framework because it doesn't support streaming. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#openOnlineFile + * @param {String} url The URL of the file. + * @return {Promise} Promise resolved when opened. + * @todo Restore node-webkit support. + */ + self.openOnlineFile = function(url) { + var deferred = $q.defer(); + + if (ionic.Platform.isAndroid() && window.plugins && window.plugins.webintent) { + // In Android we need the mimetype to open it. + var extension, + iParams; + + $mmWS.getRemoteFileMimeType(url).then(function(mimetype) { + if (!mimetype) { + // Couldn't retireve mimetype. Try to guess it. + extension = $mmText.guessExtensionFromUrl(url); + mimetype = $mmFS.getMimeType(extension); + } + + iParams = { + action: "android.intent.action.VIEW", + url: url, + type: mimetype + }; + + window.plugins.webintent.startActivity( + iParams, + function() { + $log.debug('Intent launched'); + deferred.resolve(); + }, + function() { + $log.debug('Intent launching failed.'); + $log.debug('action: ' + iParams.action); + $log.debug('url: ' + iParams.url); + $log.debug('type: ' + iParams.type); + + if (!extension || extension.indexOf('/') > -1 || extension.indexOf('\\') > -1) { + // Extension not found. + $mmLang.translateAndRejectDeferred(deferred, 'mm.core.erroropenfilenoextension'); + } else { + $mmLang.translateAndRejectDeferred(deferred, 'mm.core.erroropenfilenoapp'); + } + } + ); + }); + } else { + $log.debug('Opening remote file using window.open()'); + window.open(url, '_blank'); + deferred.resolve(); + } + + return deferred.promise; + }; + + /** + * Get the mimetype of a file given its URL. It'll perform a HEAD request to get it, if that + * fails it'll try to guess it using the URL. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#getMimeType + * @param {String} url The URL of the file. + * @return {Promise} Promise resolved with the mimetype. + */ + self.getMimeType = function(url) { + return $mmWS.getRemoteFileMimeType(url).then(function(mimetype) { + if (!mimetype) { + // Couldn't retireve mimetype. Try to guess it. + extension = $mmText.guessExtensionFromUrl(url); + mimetype = $mmFS.getMimeType(extension); + } + return mimetype || ''; + }); + }; + /** * Displays a loading modal window. * @@ -1083,6 +1164,48 @@ angular.module('mm.core') return urls; }; + /** + * Equivalent to element.closest(). If the browser doesn't support element.closest, it will + * traverse the parents to achieve the same functionality. + * Returns the closest ancestor of the current element (or the current element itself) which matches the selector. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#closest + * @param {Object} element DOM Element. + * @param {String} selector Selector to search. + * @return {Object} Closest ancestor. + */ + self.closest = function(element, selector) { + // Try to use closest if the browser supports it. + if (typeof element.closest == 'function') { + return element.closest(selector); + } + + if (!matchesFn) { + // Find the matches function supported by the browser. + ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) { + if (typeof document.body[fn] == 'function') { + matchesFn = fn; + return true; + } + return false; + }); + + if (!matchesFn) { + return; + } + } + + // Traverse parents. + while (element) { + if (element[matchesFn](selector)) { + return element; + } + element = element.parentElement; + } + }; + return self; }; }); diff --git a/www/core/lib/ws.js b/www/core/lib/ws.js index ebb88619c2a..e4e25427dce 100644 --- a/www/core/lib/ws.js +++ b/www/core/lib/ws.js @@ -21,12 +21,13 @@ angular.module('mm.core') * @ngdoc service * @name $mmWS */ -.factory('$mmWS', function($http, $q, $log, $mmLang, $cordovaFileTransfer, $mmApp, $mmFS, $mmText, mmCoreSessionExpired, - mmCoreUserDeleted, $translate, $window, $mmUtil) { +.factory('$mmWS', function($http, $q, $log, $mmLang, $cordovaFileTransfer, $mmApp, $mmFS, mmCoreSessionExpired, + mmCoreUserDeleted, $translate, $window) { $log = $log.getInstance('$mmWS'); - var self = {}; + var self = {}, + mimeTypeCache = {}; // A "cache" to store file mimetypes to prevent performing too many HEAD requests. /** * A wrapper function for a moodle WebService call. @@ -216,7 +217,7 @@ angular.module('mm.core') * @module mm.core * @ngdoc method * @name $mmWS#getRemoteFileSize - * @param {Object} uri File URI. + * @param {Object} url File URL. * @return {Promise} Promise resolved with the size or -1 if failure. */ self.getRemoteFileSize = function(url) { @@ -231,6 +232,30 @@ angular.module('mm.core') }); }; + /* + * Perform a HEAD request to get the mimetype of a remote file. + * + * @module mm.core + * @ngdoc method + * @name $mmWS#getRemoteFileMimeType + * @param {Object} url File URL. + * @param {Boolean} ignoreCache True to ignore cache, false otherwise. + * @return {Promise} Promise resolved with the mimetype or '' if failure. + */ + self.getRemoteFileMimeType = function(url, ignoreCache) { + if (mimeTypeCache[url] && !ignoreCache) { + promise = $q.when(mimeTypeCache[url]); + } + + return $http.head(url).then(function(data) { + var mimeType = data.headers('Content-Type'); + mimeTypeCache[url] = mimeType; + return mimeType || ''; + }).catch(function() { + return ''; + }); + }; + /** * A wrapper function for a synchronous Moodle WebService call. * Warning: This function should only be used if synchronous is a must. It's recommended to use $mmWS#call. @@ -276,7 +301,7 @@ angular.module('mm.core') siteurl = preSets.siteurl + '/webservice/rest/server.php?moodlewsrestformat=json'; // Serialize data. - data = $mmUtil.param(data); + data = serializeParams(data); // Perform sync request using XMLHttpRequest. xhr = new $window.XMLHttpRequest(); @@ -331,6 +356,42 @@ angular.module('mm.core') return data; }; + /** + * Serialize an object to be used in a request. + * + * @param {Object} obj Object to serialize. + * @return {String} Serialization of the object. + */ + function serializeParams(obj) { + var query = '', name, value, fullSubName, subName, subValue, innerObj, i; + + for (name in obj) { + value = obj[name]; + + if (value instanceof Array) { + for (i = 0; i < value.length; ++i) { + subValue = value[i]; + fullSubName = name + '[' + i + ']'; + innerObj = {}; + innerObj[fullSubName] = subValue; + query += serializeParams(innerObj) + '&'; + } + } + else if (value instanceof Object) { + for (subName in value) { + subValue = value[subName]; + fullSubName = name + '[' + subName + ']'; + innerObj = {}; + innerObj[fullSubName] = subValue; + query += serializeParams(innerObj) + '&'; + } + } + else if (value !== undefined && value !== null) query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&'; + } + + return query.length ? query.substr(0, query.length - 1) : query; + } + return self; });