blob: d852f4f14d7c1b03e6327cfa29adc4ad5c3f5f8f [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/crostini/crostini_export_import.h"
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager_factory.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/grit/generated_resources.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
namespace crostini {
class CrostiniExportImportFactory : public BrowserContextKeyedServiceFactory {
public:
static CrostiniExportImport* GetForProfile(Profile* profile) {
return static_cast<CrostiniExportImport*>(
GetInstance()->GetServiceForBrowserContext(profile, true));
}
static CrostiniExportImportFactory* GetInstance() {
static base::NoDestructor<CrostiniExportImportFactory> factory;
return factory.get();
}
private:
friend class base::NoDestructor<CrostiniExportImportFactory>;
CrostiniExportImportFactory()
: BrowserContextKeyedServiceFactory(
"CrostiniExportImportService",
BrowserContextDependencyManager::GetInstance()) {
DependsOn(guest_os::GuestOsSharePathFactory::GetInstance());
DependsOn(CrostiniManagerFactory::GetInstance());
}
~CrostiniExportImportFactory() override = default;
// BrowserContextKeyedServiceFactory:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override {
Profile* profile = Profile::FromBrowserContext(context);
return new CrostiniExportImport(profile);
}
};
CrostiniExportImport* CrostiniExportImport::GetForProfile(Profile* profile) {
return CrostiniExportImportFactory::GetForProfile(profile);
}
CrostiniExportImport::CrostiniExportImport(Profile* profile)
: profile_(profile) {
CrostiniManager* manager = CrostiniManager::GetForProfile(profile_);
manager->AddExportContainerProgressObserver(this);
manager->AddImportContainerProgressObserver(this);
}
CrostiniExportImport::~CrostiniExportImport() = default;
void CrostiniExportImport::Shutdown() {
CrostiniManager* manager = CrostiniManager::GetForProfile(profile_);
manager->RemoveExportContainerProgressObserver(this);
manager->RemoveImportContainerProgressObserver(this);
}
CrostiniExportImport::OperationData::OperationData(
ExportImportType type,
ContainerId container_id,
OnceTrackerFactory tracker_factory)
: type(type),
container_id(std::move(container_id)),
tracker_factory(std::move(tracker_factory)) {}
CrostiniExportImport::OperationData::~OperationData() = default;
CrostiniExportImport::OperationData* CrostiniExportImport::NewOperationData(
ExportImportType type,
ContainerId container_id,
OnceTrackerFactory factory) {
auto operation_data = std::make_unique<OperationData>(
type, std::move(container_id), std::move(factory));
OperationData* operation_data_ptr = operation_data.get();
// |operation_data_storage_| takes ownership.
operation_data_storage_[operation_data_ptr] = std::move(operation_data);
return operation_data_ptr;
}
CrostiniExportImport::OperationData* CrostiniExportImport::NewOperationData(
ExportImportType type,
ContainerId container_id) {
OnceTrackerFactory factory = base::BindOnce(
[](Profile* profile, ContainerId container_id,
std::string notification_id, ExportImportType type,
base::FilePath path)
-> std::unique_ptr<CrostiniExportImportStatusTracker> {
return std::make_unique<CrostiniExportImportNotificationController>(
profile, type, std::move(notification_id), std::move(path),
std::move(container_id));
},
profile_, container_id, GetUniqueNotificationId());
return NewOperationData(type, std::move(container_id), std::move(factory));
}
CrostiniExportImport::OperationData* CrostiniExportImport::NewOperationData(
ExportImportType type) {
return NewOperationData(type, ContainerId::GetDefault());
}
void CrostiniExportImport::ExportContainer(content::WebContents* web_contents) {
OpenFileDialog(NewOperationData(ExportImportType::EXPORT), web_contents);
}
void CrostiniExportImport::ImportContainer(content::WebContents* web_contents) {
OpenFileDialog(NewOperationData(ExportImportType::IMPORT), web_contents);
}
void CrostiniExportImport::ExportContainer(ContainerId container_id,
content::WebContents* web_contents,
OnceTrackerFactory tracker_factory) {
OpenFileDialog(
NewOperationData(ExportImportType::EXPORT, std::move(container_id),
std::move(tracker_factory)),
web_contents);
}
void CrostiniExportImport::ImportContainer(ContainerId container_id,
content::WebContents* web_contents,
OnceTrackerFactory tracker_factory) {
OpenFileDialog(
NewOperationData(ExportImportType::IMPORT, std::move(container_id),
std::move(tracker_factory)),
web_contents);
}
base::FilePath CrostiniExportImport::GetDefaultBackupPath() const {
base::Time::Exploded exploded;
base::Time::Now().LocalExplode(&exploded);
return file_manager::util::GetMyFilesFolderForProfile(profile_).Append(
base::StringPrintf("chromeos-linux-%04d-%02d-%02d.tini", exploded.year,
exploded.month, exploded.day_of_month));
}
void CrostiniExportImport::OpenFileDialog(OperationData* operation_data,
content::WebContents* web_contents) {
if (!crostini::CrostiniFeatures::Get()->IsExportImportUIAllowed(profile_)) {
return;
}
ui::SelectFileDialog::Type file_selector_mode;
unsigned title = 0;
base::FilePath default_path;
ui::SelectFileDialog::FileTypeInfo file_types;
file_types.allowed_paths = ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH;
file_types.extensions = {{"tini", "tar.gz", "tgz"}};
switch (operation_data->type) {
case ExportImportType::EXPORT:
file_selector_mode = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
title = IDS_SETTINGS_CROSTINI_EXPORT;
default_path = GetDefaultBackupPath();
break;
case ExportImportType::IMPORT:
file_selector_mode = ui::SelectFileDialog::SELECT_OPEN_FILE,
title = IDS_SETTINGS_CROSTINI_IMPORT;
default_path = file_manager::util::GetMyFilesFolderForProfile(profile_);
break;
}
select_folder_dialog_ = ui::SelectFileDialog::Create(
this, std::make_unique<ChromeSelectFilePolicy>(web_contents));
select_folder_dialog_->SelectFile(
file_selector_mode, l10n_util::GetStringUTF16(title), default_path,
&file_types, 0, base::FilePath::StringType(),
web_contents->GetTopLevelNativeWindow(),
static_cast<void*>(operation_data));
}
void CrostiniExportImport::FileSelected(const base::FilePath& path,
int index,
void* params) {
Start(static_cast<OperationData*>(params), path, base::DoNothing());
}
void CrostiniExportImport::FileSelectionCanceled(void* params) {
auto* operation_data = static_cast<OperationData*>(params);
if (operation_data->tracker_factory) {
// Create the status tracker so we can let it know the operation was
// canceled.
auto status_tracker = std::move(operation_data->tracker_factory)
.Run(operation_data->type, base::FilePath());
status_tracker->SetStatusCancelled();
}
operation_data_storage_.erase(operation_data);
}
void CrostiniExportImport::ExportContainer(
ContainerId container_id,
base::FilePath path,
CrostiniManager::CrostiniResultCallback callback) {
Start(NewOperationData(ExportImportType::EXPORT, std::move(container_id)),
path, std::move(callback));
}
void CrostiniExportImport::ImportContainer(
ContainerId container_id,
base::FilePath path,
CrostiniManager::CrostiniResultCallback callback) {
Start(NewOperationData(ExportImportType::IMPORT, std::move(container_id)),
path, std::move(callback));
}
void CrostiniExportImport::ExportContainer(ContainerId container_id,
base::FilePath path,
OnceTrackerFactory tracker_factory) {
Start(NewOperationData(ExportImportType::EXPORT, std::move(container_id),
std::move(tracker_factory)),
path, base::DoNothing());
}
void CrostiniExportImport::ImportContainer(ContainerId container_id,
base::FilePath path,
OnceTrackerFactory tracker_factory) {
Start(NewOperationData(ExportImportType::IMPORT, std::move(container_id),
std::move(tracker_factory)),
path, base::DoNothing());
}
void CrostiniExportImport::Start(
OperationData* operation_data,
base::FilePath path,
CrostiniManager::CrostiniResultCallback callback) {
std::unique_ptr<OperationData> operation_data_storage(
std::move(operation_data_storage_[operation_data]));
operation_data_storage_.erase(operation_data);
if (!crostini::CrostiniFeatures::Get()->IsExportImportUIAllowed(profile_)) {
return std::move(callback).Run(CrostiniResult::NOT_ALLOWED);
}
auto status_tracker = std::move(operation_data->tracker_factory)
.Run(operation_data->type, path);
status_tracker->SetStatusRunning(0);
auto it = status_trackers_.find(operation_data->container_id);
if (it != status_trackers_.end()) {
// There is already an operation in progress. Ensure the existing
// status_tracker is (re)displayed so the user knows why this new concurrent
// operation failed, and show a failure status_tracker for the new request.
it->second->ForceRedisplay();
status_tracker->SetStatusFailedConcurrentOperation(it->second->type());
return;
} else {
status_trackers_.emplace_hint(it, operation_data->container_id,
std::move(status_tracker));
for (auto& observer : observers_) {
observer.OnCrostiniExportImportOperationStatusChanged(true);
}
}
switch (operation_data->type) {
case ExportImportType::EXPORT:
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::MayBlock()},
// Ensure file exists so that it can be shared.
base::BindOnce(
[](const base::FilePath& path) {
base::File file(path, base::File::FLAG_CREATE_ALWAYS |
base::File::FLAG_WRITE);
DCHECK(file.IsValid()) << path << " is invalid";
},
path),
base::BindOnce(
&guest_os::GuestOsSharePath::SharePath,
base::Unretained(
guest_os::GuestOsSharePath::GetForProfile(profile_)),
kCrostiniDefaultVmName, path, false,
base::BindOnce(&CrostiniExportImport::ExportAfterSharing,
weak_ptr_factory_.GetWeakPtr(),
operation_data->container_id, path,
std::move(callback))));
break;
case ExportImportType::IMPORT:
guest_os::GuestOsSharePath::GetForProfile(profile_)->SharePath(
kCrostiniDefaultVmName, path, false,
base::BindOnce(&CrostiniExportImport::ImportAfterSharing,
weak_ptr_factory_.GetWeakPtr(),
operation_data->container_id, path,
std::move(callback)));
break;
}
}
void CrostiniExportImport::ExportAfterSharing(
const ContainerId& container_id,
const base::FilePath& path,
CrostiniManager::CrostiniResultCallback callback,
const base::FilePath& container_path,
bool result,
const std::string& failure_reason) {
if (!result) {
LOG(ERROR) << "Error sharing for export host path=" << path.value()
<< ", container path=" << container_path.value() << ": "
<< failure_reason;
auto it = status_trackers_.find(container_id);
if (it != status_trackers_.end()) {
RemoveTracker(it)->SetStatusFailed();
} else {
NOTREACHED() << container_id << " has no status_tracker to update";
}
return;
}
CrostiniManager::GetForProfile(profile_)->ExportLxdContainer(
ContainerId::GetDefault(), container_path,
base::BindOnce(&CrostiniExportImport::OnExportComplete,
weak_ptr_factory_.GetWeakPtr(), base::Time::Now(),
container_id, std::move(callback)));
}
void CrostiniExportImport::OnExportComplete(
const base::Time& start,
const ContainerId& container_id,
CrostiniManager::CrostiniResultCallback callback,
CrostiniResult result,
uint64_t container_size,
uint64_t compressed_size) {
auto it = status_trackers_.find(container_id);
if (it == status_trackers_.end()) {
NOTREACHED() << container_id << " has no status_tracker to update";
return;
}
ExportContainerResult enum_hist_result = ExportContainerResult::kSuccess;
if (result == CrostiniResult::SUCCESS) {
switch (it->second->status()) {
case CrostiniExportImportStatusTracker::Status::CANCELLING: {
// If a user requests to cancel, but the export completes before the
// cancel can happen (|result| == SUCCESS), then removing the exported
// file is functionally the same as a successful cancel.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(base::GetDeleteFileCallback(), it->second->path()));
RemoveTracker(it)->SetStatusCancelled();
break;
}
case CrostiniExportImportStatusTracker::Status::RUNNING:
UMA_HISTOGRAM_LONG_TIMES("Crostini.BackupTimeSuccess",
base::Time::Now() - start);
// Log backup size statistics.
if (container_size && compressed_size) {
base::UmaHistogramCustomCounts("Crostini.BackupContainerSizeLog2",
std::round(std::log2(container_size)),
0, 50, 50);
base::UmaHistogramCustomCounts("Crostini.BackupCompressedSizeLog2",
std::round(std::log2(compressed_size)),
0, 50, 50);
base::UmaHistogramPercentageObsoleteDoNotUse(
"Crostini.BackupSizeRatio",
std::round(compressed_size * 100.0 / container_size));
}
RemoveTracker(it)->SetStatusDone();
break;
default:
NOTREACHED();
}
} else if (result == CrostiniResult::CONTAINER_EXPORT_IMPORT_CANCELLED) {
switch (it->second->status()) {
case CrostiniExportImportStatusTracker::Status::CANCELLING: {
// If a user requests to cancel, and the export is cancelled (|result|
// == CONTAINER_EXPORT_IMPORT_CANCELLED), then the partially exported
// file needs to be cleaned up.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(base::GetDeleteFileCallback(), it->second->path()));
RemoveTracker(it)->SetStatusCancelled();
break;
}
default:
NOTREACHED();
}
} else {
LOG(ERROR) << "Error exporting " << int(result);
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(base::GetDeleteFileCallback(), it->second->path()));
switch (result) {
case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STOPPED:
enum_hist_result = ExportContainerResult::kFailedVmStopped;
break;
case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STARTED:
enum_hist_result = ExportContainerResult::kFailedVmStarted;
break;
default:
DCHECK(result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED);
enum_hist_result = ExportContainerResult::kFailed;
}
UMA_HISTOGRAM_LONG_TIMES("Crostini.BackupTimeFailed",
base::Time::Now() - start);
DCHECK(it->second->status() ==
CrostiniExportImportStatusTracker::Status::RUNNING ||
it->second->status() ==
CrostiniExportImportStatusTracker::Status::CANCELLING);
RemoveTracker(it)->SetStatusFailed();
}
UMA_HISTOGRAM_ENUMERATION("Crostini.Backup", enum_hist_result);
std::move(callback).Run(result);
}
void CrostiniExportImport::OnExportContainerProgress(
const ContainerId& container_id,
ExportContainerProgressStatus status,
int progress_percent,
uint64_t progress_speed) {
auto it = status_trackers_.find(container_id);
if (it == status_trackers_.end()) {
NOTREACHED() << container_id << " has no status_tracker to update";
return;
}
switch (status) {
// Rescale PACK:1-100 => 0-50.
case ExportContainerProgressStatus::PACK:
it->second->SetStatusRunning(progress_percent / 2);
break;
// Rescale DOWNLOAD:1-100 => 50-100.
case ExportContainerProgressStatus::DOWNLOAD:
it->second->SetStatusRunning(50 + progress_percent / 2);
break;
default:
LOG(WARNING) << "Unknown Export progress status " << int(status);
}
}
void CrostiniExportImport::OnExportContainerProgress(
const ContainerId& container_id,
const StreamingExportStatus& status) {
auto it = status_trackers_.find(container_id);
if (it == status_trackers_.end()) {
NOTREACHED() << container_id << " has no status_tracker to update";
return;
}
const auto files_percent = 100.0 * status.exported_files / status.total_files;
const auto bytes_percent = 100.0 * status.exported_bytes / status.total_bytes;
// Averaging the two percentages gives a more accurate estimation.
// TODO(juwa): investigate more accurate approximations of percent.
const auto percent = (files_percent + bytes_percent) / 2.0;
it->second->SetStatusRunning(static_cast<int>(percent));
}
void CrostiniExportImport::ImportAfterSharing(
const ContainerId& container_id,
const base::FilePath& path,
CrostiniManager::CrostiniResultCallback callback,
const base::FilePath& container_path,
bool result,
const std::string& failure_reason) {
if (!result) {
LOG(ERROR) << "Error sharing for import path=" << path
<< ", container path=" << container_path.value() << ": "
<< failure_reason;
auto it = status_trackers_.find(container_id);
if (it != status_trackers_.end()) {
RemoveTracker(it)->SetStatusFailed();
} else {
NOTREACHED() << container_id << " has no status_tracker to update";
}
return;
}
CrostiniManager::GetForProfile(profile_)->ImportLxdContainer(
ContainerId::GetDefault(), container_path,
base::BindOnce(&CrostiniExportImport::OnImportComplete,
weak_ptr_factory_.GetWeakPtr(), base::Time::Now(),
container_id, std::move(callback)));
}
void CrostiniExportImport::OnImportComplete(
const base::Time& start,
const ContainerId& container_id,
CrostiniManager::CrostiniResultCallback callback,
CrostiniResult result) {
auto it = status_trackers_.find(container_id);
ImportContainerResult enum_hist_result = ImportContainerResult::kSuccess;
if (result == CrostiniResult::SUCCESS) {
UMA_HISTOGRAM_LONG_TIMES("Crostini.RestoreTimeSuccess",
base::Time::Now() - start);
if (it != status_trackers_.end()) {
switch (it->second->status()) {
case CrostiniExportImportStatusTracker::Status::RUNNING:
// If a user requests to cancel, but the import completes before the
// cancel can happen, then the container will have been imported over
// and the cancel will have failed. However the period of time in
// which this can happen is very small (<5s), so it feels quite
// natural to pretend the cancel did not happen, and instead display
// success.
case CrostiniExportImportStatusTracker::Status::CANCELLING:
RemoveTracker(it)->SetStatusDone();
break;
default:
NOTREACHED();
}
} else {
NOTREACHED() << container_id << " has no status_tracker to update";
}
} else if (result ==
crostini::CrostiniResult::CONTAINER_EXPORT_IMPORT_CANCELLED) {
if (it != status_trackers_.end()) {
switch (it->second->status()) {
case CrostiniExportImportStatusTracker::Status::CANCELLING:
RemoveTracker(it)->SetStatusCancelled();
break;
default:
NOTREACHED();
}
} else {
NOTREACHED() << container_id << " has no status_tracker to update";
}
} else {
LOG(ERROR) << "Error importing " << int(result);
switch (result) {
case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STOPPED:
enum_hist_result = ImportContainerResult::kFailedVmStopped;
break;
case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STARTED:
enum_hist_result = ImportContainerResult::kFailedVmStarted;
break;
case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_ARCHITECTURE:
enum_hist_result = ImportContainerResult::kFailedArchitecture;
break;
case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_SPACE:
enum_hist_result = ImportContainerResult::kFailedSpace;
break;
default:
DCHECK(result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED);
enum_hist_result = ImportContainerResult::kFailed;
}
// If the operation didn't start successfully or the vm stops during the
// import, then the status_tracker status will not have been set in
// OnImportContainerProgress, so it needs to be updated.
if (result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED ||
result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STOPPED ||
result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STARTED) {
if (it != status_trackers_.end()) {
DCHECK(it->second->status() ==
CrostiniExportImportStatusTracker::Status::RUNNING);
RemoveTracker(it)->SetStatusFailed();
} else {
NOTREACHED() << container_id << " has no status_tracker to update";
}
} else {
DCHECK(it == status_trackers_.end())
<< container_id << " has unexpected status_tracker";
}
UMA_HISTOGRAM_LONG_TIMES("Crostini.RestoreTimeFailed",
base::Time::Now() - start);
}
UMA_HISTOGRAM_ENUMERATION("Crostini.Restore", enum_hist_result);
// Restart from CrostiniManager.
CrostiniManager::GetForProfile(profile_)->RestartCrostini(
container_id, std::move(callback));
}
void CrostiniExportImport::OnImportContainerProgress(
const ContainerId& container_id,
ImportContainerProgressStatus status,
int progress_percent,
uint64_t progress_speed,
const std::string& architecture_device,
const std::string& architecture_container,
uint64_t available_space,
uint64_t minimum_required_space) {
auto it = status_trackers_.find(container_id);
if (it == status_trackers_.end()) {
NOTREACHED() << container_id << " has no status_tracker to update";
return;
}
switch (status) {
// Rescale UPLOAD:1-100 => 0-50.
case ImportContainerProgressStatus::UPLOAD:
it->second->SetStatusRunning(progress_percent / 2);
break;
// Rescale UNPACK:1-100 => 50-100.
case ImportContainerProgressStatus::UNPACK:
it->second->SetStatusRunning(50 + progress_percent / 2);
break;
// Failure, set error message.
case ImportContainerProgressStatus::FAILURE_ARCHITECTURE:
RemoveTracker(it)->SetStatusFailedArchitectureMismatch(
architecture_container, architecture_device);
break;
case ImportContainerProgressStatus::FAILURE_SPACE:
DCHECK_GE(minimum_required_space, available_space);
RemoveTracker(it)->SetStatusFailedInsufficientSpace(
minimum_required_space - available_space);
break;
default:
LOG(WARNING) << "Unknown Export progress status " << int(status);
}
}
std::string CrostiniExportImport::GetUniqueNotificationId() {
return base::StringPrintf("crostini_export_import_%d",
next_status_tracker_id_++);
}
std::unique_ptr<CrostiniExportImportStatusTracker>
CrostiniExportImport::RemoveTracker(TrackerMap::iterator it) {
DCHECK(it != status_trackers_.end());
auto status_tracker = std::move(it->second);
status_trackers_.erase(it);
for (auto& observer : observers_) {
observer.OnCrostiniExportImportOperationStatusChanged(false);
}
return status_tracker;
}
void CrostiniExportImport::CancelOperation(ExportImportType type,
ContainerId container_id) {
auto it = status_trackers_.find(container_id);
if (it == status_trackers_.end()) {
NOTREACHED() << container_id << " has no status_tracker to cancel";
return;
}
it->second->SetStatusCancelling();
auto& manager = *CrostiniManager::GetForProfile(profile_);
switch (type) {
case ExportImportType::EXPORT:
manager.CancelExportLxdContainer(std::move(container_id));
return;
case ExportImportType::IMPORT:
manager.CancelImportLxdContainer(std::move(container_id));
return;
default:
NOTREACHED();
}
}
bool CrostiniExportImport::GetExportImportOperationStatus() const {
ContainerId id(kCrostiniDefaultVmName, kCrostiniDefaultContainerName);
return status_trackers_.find(id) != status_trackers_.end();
}
base::WeakPtr<CrostiniExportImportNotificationController>
CrostiniExportImport::GetNotificationControllerForTesting(
ContainerId container_id) {
auto it = status_trackers_.find(container_id);
if (it == status_trackers_.end()) {
return nullptr;
}
return static_cast<CrostiniExportImportNotificationController*>(
it->second.get())
->GetWeakPtr();
}
} // namespace crostini