Commit 3e53fe55 authored by kenobi's avatar kenobi Committed by Commit bot

Files.app: Implement media import cancellation.

Add a cancellation callback so that the active import task is cancelled when the user clicks on the cancel button in the progress notification.

BUG=420680
TEST=browser_test: FileManagerJsTest.MediaImportHandlerTest

Review URL: https://codereview.chromium.org/846903004

Cr-Commit-Position: refs/heads/master@{#311516}
parent 74e86ded
...@@ -102,7 +102,7 @@ importer.MediaImportHandler.prototype.onTaskProgress_ = ...@@ -102,7 +102,7 @@ importer.MediaImportHandler.prototype.onTaskProgress_ =
strf('CLOUD_IMPORT_ITEMS_REMAINING', task.remainingFilesCount); strf('CLOUD_IMPORT_ITEMS_REMAINING', task.remainingFilesCount);
item.progressMax = task.totalBytes; item.progressMax = task.totalBytes;
item.cancelCallback = function() { item.cancelCallback = function() {
// TODO(kenobi): Deal with import cancellation. task.requestCancel();
}; };
} }
...@@ -122,6 +122,10 @@ importer.MediaImportHandler.prototype.onTaskProgress_ = ...@@ -122,6 +122,10 @@ importer.MediaImportHandler.prototype.onTaskProgress_ =
item.progressValue = item.progressMax; item.progressValue = item.progressMax;
item.state = ProgressItemState.ERROR; item.state = ProgressItemState.ERROR;
break; break;
case UpdateType.CANCELED:
item.message = '';
item.state = ProgressItemState.CANCELED;
break;
} }
this.progressCenter_.updateItem(item); this.progressCenter_.updateItem(item);
...@@ -132,11 +136,6 @@ importer.MediaImportHandler.prototype.onTaskProgress_ = ...@@ -132,11 +136,6 @@ importer.MediaImportHandler.prototype.onTaskProgress_ =
* the FileOperationManager (and thus *spawns* an associated * the FileOperationManager (and thus *spawns* an associated
* FileOperationManager.CopyTask) but this is a temporary state of affairs. * FileOperationManager.CopyTask) but this is a temporary state of affairs.
* *
* TODO(kenobi): Add a proper implementation that doesn't use
* FileOperationManager, but instead actually performs the copy using the
* fileManagerPrivate API directly.
* TODO(kenobi): Add task cancellation.
*
* @constructor * @constructor
* @extends {importer.TaskQueue.BaseTask} * @extends {importer.TaskQueue.BaseTask}
* @struct * @struct
...@@ -177,6 +176,12 @@ importer.MediaImportHandler.ImportTask = function( ...@@ -177,6 +176,12 @@ importer.MediaImportHandler.ImportTask = function(
/** @private {number} */ /** @private {number} */
this.remainingFilesCount_ = 0; this.remainingFilesCount_ = 0;
/** @private {?function()} */
this.cancelCallback_ = null;
/** @private {boolean} Indicates whether this task was canceled. */
this.canceled_ = false;
}; };
/** @struct */ /** @struct */
...@@ -207,6 +212,21 @@ importer.MediaImportHandler.ImportTask.prototype.run = function() { ...@@ -207,6 +212,21 @@ importer.MediaImportHandler.ImportTask.prototype.run = function() {
.then(this.importTo_.bind(this)); .then(this.importTo_.bind(this));
}; };
/**
* Request cancellation of this task. An update will be sent to observers once
* the task is actually cancelled.
*/
importer.MediaImportHandler.ImportTask.prototype.requestCancel = function() {
this.canceled_ = true;
if (this.cancelCallback_) {
// Reset the callback before calling it, as the callback might do anything
// (including calling #requestCancel again).
var cancelCallback = this.cancelCallback_;
this.cancelCallback_ = null;
cancelCallback();
}
};
/** @private */ /** @private */
importer.MediaImportHandler.ImportTask.prototype.initialize_ = function() { importer.MediaImportHandler.ImportTask.prototype.initialize_ = function() {
this.remainingFilesCount_ = this.scanResult_.getFileEntries().length; this.remainingFilesCount_ = this.scanResult_.getFileEntries().length;
...@@ -251,7 +271,10 @@ importer.MediaImportHandler.ImportTask.prototype.importTo_ = ...@@ -251,7 +271,10 @@ importer.MediaImportHandler.ImportTask.prototype.importTo_ =
*/ */
importer.MediaImportHandler.ImportTask.prototype.importOne_ = importer.MediaImportHandler.ImportTask.prototype.importOne_ =
function(destination, completionCallback, entry, index) { function(destination, completionCallback, entry, index) {
// TODO(kenobi): Check for cancellation. if (this.canceled_) {
this.notify(importer.TaskQueue.UpdateType.CANCELED);
return;
}
// A count of the current number of processed bytes for this entry. // A count of the current number of processed bytes for this entry.
var currentBytes = 0; var currentBytes = 0;
...@@ -285,19 +308,26 @@ importer.MediaImportHandler.ImportTask.prototype.importOne_ = ...@@ -285,19 +308,26 @@ importer.MediaImportHandler.ImportTask.prototype.importOne_ =
/** @this {importer.MediaImportHandler.ImportTask} */ /** @this {importer.MediaImportHandler.ImportTask} */
var onComplete = function() { var onComplete = function() {
this.cancelCallback_ = null;
this.markAsCopied_(entry, destination); this.markAsCopied_(entry, destination);
this.notify(importer.TaskQueue.UpdateType.PROGRESS); this.notify(importer.TaskQueue.UpdateType.PROGRESS);
completionCallback(); completionCallback();
}; };
fileOperationUtil.copyTo( /** @this {importer.MediaImportHandler.ImportTask} */
var onError = function(error) {
this.cancelCallback_ = null;
this.onError_(error);
};
this.cancelCallback_ = fileOperationUtil.copyTo(
entry, entry,
destination, destination,
entry.name, // TODO(kenobi): account for duplicate filenames entry.name, // TODO(kenobi): account for duplicate filenames
onEntryChanged.bind(this), onEntryChanged.bind(this),
onProgress.bind(this), onProgress.bind(this),
onComplete.bind(this), onComplete.bind(this),
this.onError_.bind(this)); onError.bind(this));
}; };
/** /**
...@@ -420,34 +450,3 @@ importer.MediaImportHandler.defaultDestination.getImportDestination = ...@@ -420,34 +450,3 @@ importer.MediaImportHandler.defaultDestination.getImportDestination =
return defaultDestination.getDriveRoot_() return defaultDestination.getDriveRoot_()
.then(defaultDestination.getOrCreateImportDestination_); .then(defaultDestination.getOrCreateImportDestination_);
}; };
/**
* Sends events for progress updates and creation of file entries.
*
* TODO: File entry-related events might need to be handled via callback and not
* events - see crbug.com/358491
*
* @constructor
* @extends {cr.EventTarget}
*/
importer.MediaImportHandler.EventRouter = function() {
};
/**
* Extends cr.EventTarget.
*/
importer.MediaImportHandler.EventRouter.prototype.__proto__ =
cr.EventTarget.prototype;
/**
* @param {!importer.MediaImportHandler.ImportTask} task
*/
importer.MediaImportHandler.EventRouter.prototype.sendUpdate = function(task) {
};
/**
* @param {!FileEntry} entry The new entry.
*/
importer.MediaImportHandler.EventRouter.prototype.sendEntryCreated =
function(entry) {
};
...@@ -20,40 +20,21 @@ var drive; ...@@ -20,40 +20,21 @@ var drive;
/** @type {!MockFileSystem} */ /** @type {!MockFileSystem} */
var fileSystem; var fileSystem;
/** /** @type {!MockCopyTo} */
* @typedef {{ var mockCopier;
* source: source,
* destination: parent,
* newName: newName
* }}
*/
var CopyCapture;
/** // Set up string assets.
* @type {!Array<!CopyCapture>} loadTimeData.data = {
*/ CLOUD_IMPORT_ITEMS_REMAINING: '',
var importedMedia = []; DRIVE_DIRECTORY_LABEL: 'My Drive',
DOWNLOADS_DIRECTORY_LABEL: 'Downloads'
};
function setUp() { function setUp() {
// Set up string assets.
loadTimeData.data = {
DRIVE_DIRECTORY_LABEL: 'My Drive',
DOWNLOADS_DIRECTORY_LABEL: 'Downloads'
};
progressCenter = new MockProgressCenter(); progressCenter = new MockProgressCenter();
// Replace with test function. // Replaces fileOperationUtil.copyTo with test function.
fileOperationUtil.copyTo = function(source, parent, newName, mockCopier = new MockCopyTo();
entryChangedCallback, progressCallback, successCallback, errorCallback) {
importedMedia.push({
source: source,
destination: parent,
newName: newName
});
successCallback();
};
importedMedia = [];
var volumeManager = new MockVolumeManager(); var volumeManager = new MockVolumeManager();
drive = volumeManager.getCurrentProfileVolumeInfo( drive = volumeManager.getCurrentProfileVolumeInfo(
...@@ -98,7 +79,7 @@ function testImportMedia(callback) { ...@@ -98,7 +79,7 @@ function testImportMedia(callback) {
function(updateType, task) { function(updateType, task) {
switch (updateType) { switch (updateType) {
case importer.TaskQueue.UpdateType.SUCCESS: case importer.TaskQueue.UpdateType.SUCCESS:
resolve(importedMedia); resolve();
break; break;
case importer.TaskQueue.UpdateType.ERROR: case importer.TaskQueue.UpdateType.ERROR:
reject(new Error(importer.TaskQueue.UpdateType.ERROR)); reject(new Error(importer.TaskQueue.UpdateType.ERROR));
...@@ -109,16 +90,15 @@ function testImportMedia(callback) { ...@@ -109,16 +90,15 @@ function testImportMedia(callback) {
reportPromise( reportPromise(
whenImportDone.then( whenImportDone.then(
/** @param {!Array<!CopyCapture>} importedMedia */ function() {
function(importedMedia) { assertEquals(media.length, mockCopier.copiedFiles.length);
assertEquals(media.length, importedMedia.length); mockCopier.copiedFiles.forEach(
importedMedia.forEach( /** @param {!MockCopyTo.CopyInfo} copy */
/** @param {!CopyCapture} imported */ function(copy) {
function(imported) {
// Verify the copied file is one of the expected files. // Verify the copied file is one of the expected files.
assertTrue(media.indexOf(imported.source) >= 0); assertTrue(media.indexOf(copy.source) >= 0);
// Verify that the files are being copied to the right locations. // Verify that the files are being copied to the right locations.
assertEquals(destination(), imported.destination); assertEquals(destination(), copy.destination);
}); });
}), }),
callback); callback);
...@@ -147,7 +127,7 @@ function testUpdatesHistoryAfterImport(callback) { ...@@ -147,7 +127,7 @@ function testUpdatesHistoryAfterImport(callback) {
function(updateType, task) { function(updateType, task) {
switch (updateType) { switch (updateType) {
case importer.TaskQueue.UpdateType.SUCCESS: case importer.TaskQueue.UpdateType.SUCCESS:
resolve(importedMedia); resolve();
break; break;
case importer.TaskQueue.UpdateType.ERROR: case importer.TaskQueue.UpdateType.ERROR:
reject(new Error(importer.TaskQueue.UpdateType.ERROR)); reject(new Error(importer.TaskQueue.UpdateType.ERROR));
...@@ -158,13 +138,12 @@ function testUpdatesHistoryAfterImport(callback) { ...@@ -158,13 +138,12 @@ function testUpdatesHistoryAfterImport(callback) {
reportPromise( reportPromise(
whenImportDone.then( whenImportDone.then(
/** @param {!Array<!CopyCapture>} importedMedia */ function() {
function(importedMedia) { mockCopier.copiedFiles.forEach(
importedMedia.forEach( /** @param {!MockCopyTo.CopyInfo} copy */
/** @param {!CopyCapture} */ function(copy) {
function(capture) {
importHistory.assertCopied( importHistory.assertCopied(
capture.source, importer.Destination.GOOGLE_DRIVE); copy.source, importer.Destination.GOOGLE_DRIVE);
}); });
}), }),
callback); callback);
...@@ -172,6 +151,68 @@ function testUpdatesHistoryAfterImport(callback) { ...@@ -172,6 +151,68 @@ function testUpdatesHistoryAfterImport(callback) {
scanResult.finalize(); scanResult.finalize();
} }
// Tests that cancelling an import works properly.
function testImportCancellation(callback) {
var media = setupFileSystem([
'/DCIM/photos0/IMG00001.jpg',
'/DCIM/photos0/IMG00002.jpg',
'/DCIM/photos0/IMG00003.jpg',
'/DCIM/photos1/IMG00001.jpg',
'/DCIM/photos1/IMG00002.jpg',
'/DCIM/photos1/IMG00003.jpg'
]);
/** @const {number} */
var EXPECTED_COPY_COUNT = 3;
var destinationFileSystem = new MockFileSystem('fake-destination');
var destination = function() { return destinationFileSystem.root; };
var scanResult = new TestScanResult(media);
var importTask = mediaImporter.importFromScanResult(scanResult, destination);
var whenImportCancelled = new Promise(
function(resolve, reject) {
importTask.addObserver(
/**
* @param {!importer.TaskQueue.UpdateType} updateType
* @param {!importer.TaskQueue.Task} task
*/
function(updateType, task) {
if (updateType === importer.TaskQueue.UpdateType.CANCELED) {
resolve();
}
});
});
reportPromise(
whenImportCancelled.then(
function() {
assertEquals(EXPECTED_COPY_COUNT, mockCopier.copiedFiles.length);
mockCopier.copiedFiles.forEach(
/** @param {!MockCopyTo.CopyInfo} copy */
function(copy) {
// Verify the copied file is one of the expected files.
assertTrue(media.indexOf(copy.source) >= 0);
// Verify that the files are being copied to the right locations.
assertEquals(destination(), copy.destination);
});
}),
callback);
// Simulate cancellation after the expected number of copies is done.
var copyCount = 0;
mockCopier.onCopy(
/** @param {!MockCopyTo.CopyInfo} copy */
function(copy) {
mockCopier.doCopy(copy);
if (++copyCount === EXPECTED_COPY_COUNT) {
importTask.requestCancel();
}
});
scanResult.finalize();
}
/** /**
* @param {!Array.<string>} fileNames * @param {!Array.<string>} fileNames
* @return {!Array.<!Entry>} * @return {!Array.<!Entry>}
...@@ -186,3 +227,80 @@ function setupFileSystem(fileNames) { ...@@ -186,3 +227,80 @@ function setupFileSystem(fileNames) {
return fileSystem.entries[filename]; return fileSystem.entries[filename];
}); });
} }
/**
* Replaces fileOperationUtil.copyTo with some mock functionality for testing.
* @constructor
*/
function MockCopyTo() {
/** @type {!Array<!MockCopyTo.CopyInfo>} */
this.copiedFiles = [];
// Replace with test function.
fileOperationUtil.copyTo = this.copyTo_.bind(this);
this.entryChangedCallback_ = null;
this.progressCallback_ = null;
this.successCallback_ = null;
this.errorCallback_ = null;
// Default copy callback just does the copy.
this.copyCallback_ = this.doCopy.bind(this);
}
/**
* @typedef {{
* source: source,
* destination: parent,
* newName: newName
* }}
*/
MockCopyTo.CopyInfo;
/**
* A mock to replace fileOperationUtil.copyTo. See the original for details.
* @param {Entry} source
* @param {DirectoryEntry} parent
* @param {string} newName
* @param {function(string, Entry)} entryChangedCallback
* @param {function(string, number)} progressCallback
* @param {function(Entry)} successCallback
* @param {function(DOMError)} errorCallback
* @return {function()}
*/
MockCopyTo.prototype.copyTo_ = function(source, parent, newName,
entryChangedCallback, progressCallback, successCallback, errorCallback) {
this.entryChangedCallback_ = entryChangedCallback;
this.progressCallback_ = progressCallback;
this.successCallback_ = successCallback;
this.errorCallback_ = errorCallback;
this.copyCallback_({
source: source,
destination: parent,
newName: newName
});
};
/**
* Set a callback to be called whenever #copyTo_ is called. This can be used to
* simulate errors, etc, during copying. The default copy callback just calls
* #doCopy.
* @param {!function(!MockCopyTo.CopyInfo)} copyCallback
*/
MockCopyTo.prototype.onCopy = function(copyCallback) {
this.copyCallback_ = copyCallback;
};
/**
* Completes the given copy. Call this in the callback passed to #onCopy, to
* simulate the current copy operation completing successfully.
* @param {!MockCopyTo.CopyInfo} copy
*/
MockCopyTo.prototype.doCopy = function(copy) {
this.copiedFiles.push(copy);
this.entryChangedCallback_(copy.source.toURL(),
copy.destination);
this.successCallback_();
};
...@@ -41,7 +41,8 @@ importer.TaskQueue = function() { ...@@ -41,7 +41,8 @@ importer.TaskQueue = function() {
importer.TaskQueue.UpdateType = { importer.TaskQueue.UpdateType = {
PROGRESS: 'PROGRESS', PROGRESS: 'PROGRESS',
SUCCESS: 'SUCCESS', SUCCESS: 'SUCCESS',
ERROR: 'ERROR' ERROR: 'ERROR',
CANCELED: 'CANCELED'
}; };
/** /**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment