Commit 0d6bc5b7 authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

Show retry dialog when crostini unsharing folder fails

Unsharing a crostini folder can fail if a user has a file
open in some application.  When this happens, we will show
a dialog to the user to inform them that unsharing has
failed and allow them to retry the unsharing presumably
after they have closed associated files.

Bug: 920416
Change-Id: I303200f7e0e40fe8c7e1382bf19878d7ed2b31fd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2037276Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarJulian Watson <juwa@google.com>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738462}
parent a1f427ed
...@@ -632,6 +632,15 @@ ...@@ -632,6 +632,15 @@
<message name="IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_SHARING" desc="Tooltip to show when hovering on the remove icon for a crostini shared folder."> <message name="IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_SHARING" desc="Tooltip to show when hovering on the remove icon for a crostini shared folder.">
Remove sharing Remove sharing
</message> </message>
<message name="IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_FAILURE_DIALOG_MESSAGE" desc="Message to show user when unsharing a crostini shared folder fails.">
Couldn't unshare because an application is using this folder. The folder will be unshared when Linux is next shut down.
</message>
<message name="IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_FAILURE_DIALOG_TITLE" desc="Title of the error dialog shown to a user when unsharing a crostini shared folder fails.">
Unshare failed
</message>
<message name="IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_FAILURE_TRY_AGAIN" desc="Button text in the dialog shown when unsharing a crostini shared folder fails. Pressing this button will attempt to unshare the folder again.">
Try again
</message>
<message name="IDS_SETTINGS_CROSTINI_EXPORT_IMPORT_TITLE" desc="Title for crostini container export and imoprt (backup and restore) section"> <message name="IDS_SETTINGS_CROSTINI_EXPORT_IMPORT_TITLE" desc="Title for crostini container export and imoprt (backup and restore) section">
Backup &amp; restore Backup &amp; restore
</message> </message>
......
...@@ -48,6 +48,7 @@ cr.define('settings', function() { ...@@ -48,6 +48,7 @@ cr.define('settings', function() {
/** /**
* @param {string} vmName VM to stop sharing path with. * @param {string} vmName VM to stop sharing path with.
* @param {string} path Path to stop sharing. * @param {string} path Path to stop sharing.
* @return {!Promise<boolean>} Result of unsharing.
*/ */
removeCrostiniSharedPath(vmName, path) {} removeCrostiniSharedPath(vmName, path) {}
...@@ -115,7 +116,7 @@ cr.define('settings', function() { ...@@ -115,7 +116,7 @@ cr.define('settings', function() {
/** @override */ /** @override */
removeCrostiniSharedPath(vmName, path) { removeCrostiniSharedPath(vmName, path) {
chrome.send('removeCrostiniSharedPath', [vmName, path]); return cr.sendWithPromise('removeCrostiniSharedPath', vmName, path);
} }
/** @override */ /** @override */
......
...@@ -47,6 +47,24 @@ ...@@ -47,6 +47,24 @@
</template> </template>
</div> </div>
</div> </div>
<template is="dom-if" if="[[sharedPathWhichFailedRemoval_]]" restamp>
<cr-dialog id="removeSharedPathFailedDialog" close-text="$i18n{close}">
<div slot="title">
$i18n{crostiniSharedPathsRemoveFailureDialogTitle}
</div>
<div slot="body">
$i18n{crostiniSharedPathsRemoveFailureDialogMessage}
</div>
<div slot="button-container">
<cr-button id="cancel" class="cancel-button"
on-click="onRemoveFailedDismissTap_">$i18n{ok}</cr-button>
</cr-button>
<cr-button id="retry" class="action-button"
on-click="onRemoveFailedRetryTap_">
$i18n{crostiniSharedPathsRemoveFailureTryAgain}
</div>
</cr-dialog>
</template>
</template> </template>
<script src="crostini_shared_paths.js"></script> <script src="crostini_shared_paths.js"></script>
</dom-module> </dom-module>
...@@ -33,6 +33,16 @@ Polymer({ ...@@ -33,6 +33,16 @@ Polymer({
* @private {Array<!CrostiniSharedPath>} * @private {Array<!CrostiniSharedPath>}
*/ */
sharedPaths_: Array, sharedPaths_: Array,
/**
* The shared path which failed to be removed in the most recent attempt
* to remove a path. Null indicates that removal succeeded.
* @private {?string}
*/
sharedPathWhichFailedRemoval_: {
type: String,
value: null,
},
}, },
observers: [ observers: [
...@@ -59,13 +69,46 @@ Polymer({ ...@@ -59,13 +69,46 @@ Polymer({
}); });
}, },
/**
* @param {string} path
* @private
*/
removeSharedPath_(path) {
this.sharedPathWhichFailedRemoval_ = null;
settings.CrostiniBrowserProxyImpl.getInstance()
.removeCrostiniSharedPath(DEFAULT_CROSTINI_VM, path)
.then(result => {
if (!result) {
this.sharedPathWhichFailedRemoval_ = path;
// Flush to make sure that the retry dialog is attached.
Polymer.dom.flush();
this.$$('#removeSharedPathFailedDialog').showModal();
}
});
},
/** /**
* @param {!Event} event * @param {!Event} event
* @private * @private
*/ */
onRemoveSharedPathTap_(event) { onRemoveSharedPathTap_(event) {
settings.CrostiniBrowserProxyImpl.getInstance().removeCrostiniSharedPath( this.removeSharedPath_(event.model.item.path);
DEFAULT_CROSTINI_VM, event.model.item.path); },
/**
* @param {!Event} event
* @private
*/
onRemoveFailedRetryTap_(event) {
this.removeSharedPath_(assert(this.sharedPathWhichFailedRemoval_));
},
/**
* @param {!Event} event
* @private
*/
onRemoveFailedDismissTap_(event) {
this.sharedPathWhichFailedRemoval_ = null;
}, },
/** /**
......
...@@ -168,24 +168,31 @@ void CrostiniHandler::HandleGetCrostiniSharedPathsDisplayText( ...@@ -168,24 +168,31 @@ void CrostiniHandler::HandleGetCrostiniSharedPathsDisplayText(
void CrostiniHandler::HandleRemoveCrostiniSharedPath( void CrostiniHandler::HandleRemoveCrostiniSharedPath(
const base::ListValue* args) { const base::ListValue* args) {
CHECK_EQ(2U, args->GetSize()); AllowJavascript();
CHECK_EQ(3U, args->GetSize());
std::string callback_id;
std::string vm_name; std::string vm_name;
CHECK(args->GetString(0, &vm_name));
std::string path; std::string path;
CHECK(args->GetString(1, &path)); CHECK(args->GetString(0, &callback_id));
CHECK(args->GetString(1, &vm_name));
CHECK(args->GetString(2, &path));
guest_os::GuestOsSharePath::GetForProfile(profile_)->UnsharePath( guest_os::GuestOsSharePath::GetForProfile(profile_)->UnsharePath(
vm_name, base::FilePath(path), vm_name, base::FilePath(path),
/*unpersist=*/true, /*unpersist=*/true,
base::BindOnce( base::BindOnce(&CrostiniHandler::OnCrostiniSharedPathRemoved,
[](const std::string& path, bool result, weak_ptr_factory_.GetWeakPtr(), callback_id, path));
}
void CrostiniHandler::OnCrostiniSharedPathRemoved(
const std::string& callback_id,
const std::string& path,
bool result,
const std::string& failure_reason) { const std::string& failure_reason) {
if (!result) { if (!result) {
LOG(ERROR) << "Error unsharing " << path << ": " LOG(ERROR) << "Error unsharing " << path << ": " << failure_reason;
<< failure_reason;
} }
}, ResolveJavascriptCallback(base::Value(callback_id), base::Value(result));
path));
} }
namespace { namespace {
......
...@@ -45,6 +45,10 @@ class CrostiniHandler : public ::settings::SettingsPageUIHandler, ...@@ -45,6 +45,10 @@ class CrostiniHandler : public ::settings::SettingsPageUIHandler,
void HandleGetCrostiniSharedPathsDisplayText(const base::ListValue* args); void HandleGetCrostiniSharedPathsDisplayText(const base::ListValue* args);
// Remove a specified path from being shared. // Remove a specified path from being shared.
void HandleRemoveCrostiniSharedPath(const base::ListValue* args); void HandleRemoveCrostiniSharedPath(const base::ListValue* args);
void OnCrostiniSharedPathRemoved(const std::string& callback_id,
const std::string& path,
bool result,
const std::string& failure_reason);
// Returns a list of available USB devices. // Returns a list of available USB devices.
void HandleGetCrostiniSharedUsbDevices(const base::ListValue* args); void HandleGetCrostiniSharedUsbDevices(const base::ListValue* args);
// Set the share state of a USB device. // Set the share state of a USB device.
......
...@@ -561,6 +561,12 @@ void AddCrostiniStrings(content::WebUIDataSource* html_source, ...@@ -561,6 +561,12 @@ void AddCrostiniStrings(content::WebUIDataSource* html_source,
IDS_SETTINGS_CROSTINI_SHARED_PATHS_INSTRUCTIONS_REMOVE}, IDS_SETTINGS_CROSTINI_SHARED_PATHS_INSTRUCTIONS_REMOVE},
{"crostiniSharedPathsRemoveSharing", {"crostiniSharedPathsRemoveSharing",
IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_SHARING}, IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_SHARING},
{"crostiniSharedPathsRemoveFailureDialogMessage",
IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_FAILURE_DIALOG_MESSAGE},
{"crostiniSharedPathsRemoveFailureDialogTitle",
IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_FAILURE_DIALOG_TITLE},
{"crostiniSharedPathsRemoveFailureTryAgain",
IDS_SETTINGS_CROSTINI_SHARED_PATHS_REMOVE_FAILURE_TRY_AGAIN},
{"crostiniSharedPathsListEmptyMessage", {"crostiniSharedPathsListEmptyMessage",
IDS_SETTINGS_CROSTINI_SHARED_PATHS_LIST_EMPTY_MESSAGE}, IDS_SETTINGS_CROSTINI_SHARED_PATHS_LIST_EMPTY_MESSAGE},
{"crostiniExportImportTitle", IDS_SETTINGS_CROSTINI_EXPORT_IMPORT_TITLE}, {"crostiniExportImportTitle", IDS_SETTINGS_CROSTINI_EXPORT_IMPORT_TITLE},
......
...@@ -346,6 +346,28 @@ suite('CrostiniPageTests', function() { ...@@ -346,6 +346,28 @@ suite('CrostiniPageTests', function() {
assertFalse(subpage.$.crostiniListEmpty.hidden); assertFalse(subpage.$.crostiniListEmpty.hidden);
}); });
}); });
test('RemoveFailedRetry', function() {
// Remove shared path fails.
crostiniBrowserProxy.removeSharedPathResult = false;
subpage.$$('.list-item cr-icon-button').click();
return crostiniBrowserProxy.whenCalled('removeCrostiniSharedPath')
.then(() => {
Polymer.dom.flush();
assertTrue(subpage.$$('#removeSharedPathFailedDialog').open);
// Click retry and make sure 'removeCrostiniSharedPath' is called
// and dialog is closed/removed.
crostiniBrowserProxy.removeSharedPathResult = true;
subpage.$$('#removeSharedPathFailedDialog')
.querySelector('.action-button')
.click();
return crostiniBrowserProxy.whenCalled('removeCrostiniSharedPath');
})
.then(() => {
assertFalse(!!subpage.$$('#removeSharedPathFailedDialog'));
});
});
}); });
suite('SubPageSharedUsbDevices', function() { suite('SubPageSharedUsbDevices', function() {
......
...@@ -17,6 +17,7 @@ class TestCrostiniBrowserProxy extends TestBrowserProxy { ...@@ -17,6 +17,7 @@ class TestCrostiniBrowserProxy extends TestBrowserProxy {
'requestCrostiniContainerUpgradeView', 'requestCrostiniContainerUpgradeView',
]); ]);
this.sharedUsbDevices = []; this.sharedUsbDevices = [];
this.removeSharedPathResult = true;
} }
/** @override */ /** @override */
...@@ -49,6 +50,7 @@ class TestCrostiniBrowserProxy extends TestBrowserProxy { ...@@ -49,6 +50,7 @@ class TestCrostiniBrowserProxy extends TestBrowserProxy {
/** override */ /** override */
removeCrostiniSharedPath(vmName, path) { removeCrostiniSharedPath(vmName, path) {
this.methodCalled('removeCrostiniSharedPath', [vmName, path]); this.methodCalled('removeCrostiniSharedPath', [vmName, path]);
return Promise.resolve(this.removeSharedPathResult);
} }
/** @override */ /** @override */
......
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