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_ =
strf('CLOUD_IMPORT_ITEMS_REMAINING', task.remainingFilesCount);
item.progressMax = task.totalBytes;
item.cancelCallback = function() {
// TODO(kenobi): Deal with import cancellation.
task.requestCancel();
};
}
......@@ -122,6 +122,10 @@ importer.MediaImportHandler.prototype.onTaskProgress_ =
item.progressValue = item.progressMax;
item.state = ProgressItemState.ERROR;
break;
case UpdateType.CANCELED:
item.message = '';
item.state = ProgressItemState.CANCELED;
break;
}
this.progressCenter_.updateItem(item);
......@@ -132,11 +136,6 @@ importer.MediaImportHandler.prototype.onTaskProgress_ =
* the FileOperationManager (and thus *spawns* an associated
* 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
* @extends {importer.TaskQueue.BaseTask}
* @struct
......@@ -177,6 +176,12 @@ importer.MediaImportHandler.ImportTask = function(
/** @private {number} */
this.remainingFilesCount_ = 0;
/** @private {?function()} */
this.cancelCallback_ = null;
/** @private {boolean} Indicates whether this task was canceled. */
this.canceled_ = false;
};
/** @struct */
......@@ -207,6 +212,21 @@ importer.MediaImportHandler.ImportTask.prototype.run = function() {
.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 */
importer.MediaImportHandler.ImportTask.prototype.initialize_ = function() {
this.remainingFilesCount_ = this.scanResult_.getFileEntries().length;
......@@ -251,7 +271,10 @@ importer.MediaImportHandler.ImportTask.prototype.importTo_ =
*/
importer.MediaImportHandler.ImportTask.prototype.importOne_ =
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.
var currentBytes = 0;
......@@ -285,19 +308,26 @@ importer.MediaImportHandler.ImportTask.prototype.importOne_ =
/** @this {importer.MediaImportHandler.ImportTask} */
var onComplete = function() {
this.cancelCallback_ = null;
this.markAsCopied_(entry, destination);
this.notify(importer.TaskQueue.UpdateType.PROGRESS);
completionCallback();
};
fileOperationUtil.copyTo(
/** @this {importer.MediaImportHandler.ImportTask} */
var onError = function(error) {
this.cancelCallback_ = null;
this.onError_(error);
};
this.cancelCallback_ = fileOperationUtil.copyTo(
entry,
destination,
entry.name, // TODO(kenobi): account for duplicate filenames
onEntryChanged.bind(this),
onProgress.bind(this),
onComplete.bind(this),
this.onError_.bind(this));
onError.bind(this));
};
/**
......@@ -420,34 +450,3 @@ importer.MediaImportHandler.defaultDestination.getImportDestination =
return defaultDestination.getDriveRoot_()
.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;
/** @type {!MockFileSystem} */
var fileSystem;
/**
* @typedef {{
* source: source,
* destination: parent,
* newName: newName
* }}
*/
var CopyCapture;
/** @type {!MockCopyTo} */
var mockCopier;
/**
* @type {!Array<!CopyCapture>}
*/
var importedMedia = [];
// Set up string assets.
loadTimeData.data = {
CLOUD_IMPORT_ITEMS_REMAINING: '',
DRIVE_DIRECTORY_LABEL: 'My Drive',
DOWNLOADS_DIRECTORY_LABEL: 'Downloads'
};
function setUp() {
// Set up string assets.
loadTimeData.data = {
DRIVE_DIRECTORY_LABEL: 'My Drive',
DOWNLOADS_DIRECTORY_LABEL: 'Downloads'
};
progressCenter = new MockProgressCenter();
// Replace with test function.
fileOperationUtil.copyTo = function(source, parent, newName,
entryChangedCallback, progressCallback, successCallback, errorCallback) {
importedMedia.push({
source: source,
destination: parent,
newName: newName
});
successCallback();
};
importedMedia = [];
// Replaces fileOperationUtil.copyTo with test function.
mockCopier = new MockCopyTo();
var volumeManager = new MockVolumeManager();
drive = volumeManager.getCurrentProfileVolumeInfo(
......@@ -98,7 +79,7 @@ function testImportMedia(callback) {
function(updateType, task) {
switch (updateType) {
case importer.TaskQueue.UpdateType.SUCCESS:
resolve(importedMedia);
resolve();
break;
case importer.TaskQueue.UpdateType.ERROR:
reject(new Error(importer.TaskQueue.UpdateType.ERROR));
......@@ -109,16 +90,15 @@ function testImportMedia(callback) {
reportPromise(
whenImportDone.then(
/** @param {!Array<!CopyCapture>} importedMedia */
function(importedMedia) {
assertEquals(media.length, importedMedia.length);
importedMedia.forEach(
/** @param {!CopyCapture} imported */
function(imported) {
function() {
assertEquals(media.length, 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(imported.source) >= 0);
assertTrue(media.indexOf(copy.source) >= 0);
// Verify that the files are being copied to the right locations.
assertEquals(destination(), imported.destination);
assertEquals(destination(), copy.destination);
});
}),
callback);
......@@ -147,7 +127,7 @@ function testUpdatesHistoryAfterImport(callback) {
function(updateType, task) {
switch (updateType) {
case importer.TaskQueue.UpdateType.SUCCESS:
resolve(importedMedia);
resolve();
break;
case importer.TaskQueue.UpdateType.ERROR:
reject(new Error(importer.TaskQueue.UpdateType.ERROR));
......@@ -158,13 +138,12 @@ function testUpdatesHistoryAfterImport(callback) {
reportPromise(
whenImportDone.then(
/** @param {!Array<!CopyCapture>} importedMedia */
function(importedMedia) {
importedMedia.forEach(
/** @param {!CopyCapture} */
function(capture) {
function() {
mockCopier.copiedFiles.forEach(
/** @param {!MockCopyTo.CopyInfo} copy */
function(copy) {
importHistory.assertCopied(
capture.source, importer.Destination.GOOGLE_DRIVE);
copy.source, importer.Destination.GOOGLE_DRIVE);
});
}),
callback);
......@@ -172,6 +151,68 @@ function testUpdatesHistoryAfterImport(callback) {
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
* @return {!Array.<!Entry>}
......@@ -186,3 +227,80 @@ function setupFileSystem(fileNames) {
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() {
importer.TaskQueue.UpdateType = {
PROGRESS: 'PROGRESS',
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