Commit f14ab187 authored by Alexey Baskakov's avatar Alexey Baskakov Committed by Commit Bot

WebApp: Write icons to disk in WebAppInstallFinalizer.

Output structure:
<ProfileDir>
    WebApps
        Sync Data
            LevelDB
        Temp
            <temp_name>
                Icons
                    <size1>.png
                    <sizeN>.png
        <AppId1>
            Icons
                <size1>.png
                <sizeN>.png
        <AppIdN>
            Icons
                <size1>.png
                <sizeN>.png

When all .png files written, we `mv` Temp/<temp_name> to WebApps/<AppId1> in one final
file system "commit".

TBR=dcheng@chromium.org

Bug: 901226
Change-Id: I2e8840aa3df230d9f78f287d5a637780ef34268b
Reviewed-on: https://chromium-review.googlesource.com/c/1349148Reviewed-by: default avatarAlexey Baskakov <loyso@chromium.org>
Reviewed-by: default avatarAlan Cutter <alancutter@chromium.org>
Commit-Queue: Alexey Baskakov <loyso@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611589}
parent ea7742fa
......@@ -11,6 +11,8 @@ source_set("web_applications") {
"abstract_web_app_database.h",
"external_web_apps.cc",
"external_web_apps.h",
"file_utils_wrapper.cc",
"file_utils_wrapper.h",
"policy/web_app_policy_constants.cc",
"policy/web_app_policy_constants.h",
"policy/web_app_policy_manager.cc",
......@@ -23,6 +25,8 @@ source_set("web_applications") {
"web_app_database.h",
"web_app_database_factory.cc",
"web_app_database_factory.h",
"web_app_icon_manager.cc",
"web_app_icon_manager.h",
"web_app_install_finalizer.cc",
"web_app_install_finalizer.h",
"web_app_install_manager.cc",
......@@ -54,6 +58,8 @@ source_set("web_applications_test_support") {
sources = [
"test/test_data_retriever.cc",
"test/test_data_retriever.h",
"test/test_file_utils.cc",
"test/test_file_utils.h",
"test/test_install_finalizer.cc",
"test/test_install_finalizer.h",
"test/test_system_web_app_manager.cc",
......
......@@ -34,7 +34,8 @@ enum class InstallResultCode {
kGetWebApplicationInfoFailed = 3,
kPreviouslyUninstalled = 4,
kWebContentsDestroyed = 5,
kMaxValue = kWebContentsDestroyed,
kWriteDataFailed = 6,
kMaxValue = kWriteDataFailed,
};
// Where an app was installed from. This affects what flags will be used when
......
// Copyright 2018 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/web_applications/file_utils_wrapper.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
namespace web_app {
std::unique_ptr<FileUtilsWrapper> FileUtilsWrapper::Clone() {
return std::make_unique<FileUtilsWrapper>();
}
bool FileUtilsWrapper::PathExists(const base::FilePath& path) {
return base::PathExists(path);
}
bool FileUtilsWrapper::PathIsWritable(const base::FilePath& path) {
return base::PathIsWritable(path);
}
bool FileUtilsWrapper::DirectoryExists(const base::FilePath& path) {
return base::DirectoryExists(path);
}
bool FileUtilsWrapper::CreateDirectory(const base::FilePath& full_path) {
return base::CreateDirectory(full_path);
}
int FileUtilsWrapper::ReadFile(const base::FilePath& filename,
char* data,
int max_size) {
return base::ReadFile(filename, data, max_size);
}
int FileUtilsWrapper::WriteFile(const base::FilePath& filename,
const char* data,
int size) {
return base::WriteFile(filename, data, size);
}
bool FileUtilsWrapper::Move(const base::FilePath& from_path,
const base::FilePath& to_path) {
return base::Move(from_path, to_path);
}
bool FileUtilsWrapper::IsDirectoryEmpty(const base::FilePath& dir_path) {
return base::IsDirectoryEmpty(dir_path);
}
bool FileUtilsWrapper::ReadFileToString(const base::FilePath& path,
std::string* contents) {
return base::ReadFileToString(path, contents);
}
bool FileUtilsWrapper::DeleteFile(const base::FilePath& path, bool recursive) {
return base::DeleteFile(path, recursive);
}
} // namespace web_app
// Copyright 2018 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.
#ifndef CHROME_BROWSER_WEB_APPLICATIONS_FILE_UTILS_WRAPPER_H_
#define CHROME_BROWSER_WEB_APPLICATIONS_FILE_UTILS_WRAPPER_H_
#include <memory>
#include <string>
#include "base/macros.h"
#include "build/build_config.h"
// Include this to avoid conflicts with CreateDirectory Win macro.
// It converts CreateDirectory into CreateDirectoryW.
#if defined(OS_WIN)
#include "base/win/windows_types.h"
#endif // defined(OS_WIN)
namespace base {
class FilePath;
}
namespace web_app {
// A simple wrapper for base/files/file_util.h utilities.
// See detailed comments for functionality in corresponding
// base/files/file_util.h functions.
// Allows a testing implementation to intercept calls to the file system.
// TODO(loyso): Add more tests and promote mocked methods to |virtual|.
class FileUtilsWrapper {
public:
FileUtilsWrapper() = default;
virtual ~FileUtilsWrapper() = default;
// Create a copy to use in IO task.
virtual std::unique_ptr<FileUtilsWrapper> Clone();
bool PathExists(const base::FilePath& path);
bool PathIsWritable(const base::FilePath& path);
bool DirectoryExists(const base::FilePath& path);
bool CreateDirectory(const base::FilePath& full_path);
int ReadFile(const base::FilePath& filename, char* data, int max_size);
virtual int WriteFile(const base::FilePath& filename,
const char* data,
int size);
bool Move(const base::FilePath& from_path, const base::FilePath& to_path);
bool IsDirectoryEmpty(const base::FilePath& dir_path);
bool ReadFileToString(const base::FilePath& path, std::string* contents);
bool DeleteFile(const base::FilePath& path, bool recursive);
DISALLOW_COPY_AND_ASSIGN(FileUtilsWrapper);
};
} // namespace web_app
#endif // CHROME_BROWSER_WEB_APPLICATIONS_FILE_UTILS_WRAPPER_H_
......@@ -33,6 +33,8 @@ class TestDataRetriever : public WebAppDataRetriever {
// Set icons to respond on |GetIcons|.
void SetIcons(IconsMap icons_map);
WebApplicationInfo& web_app_info() { return *web_app_info_; }
private:
std::unique_ptr<WebApplicationInfo> web_app_info_;
IconsMap icons_map_;
......
// Copyright 2018 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 <utility>
#include "chrome/browser/web_applications/test/test_file_utils.h"
namespace web_app {
std::unique_ptr<FileUtilsWrapper> TestFileUtils::Clone() {
auto clone = std::make_unique<TestFileUtils>();
clone->remaining_disk_space_ = remaining_disk_space_;
return clone;
}
void TestFileUtils::SetRemainingDiskSpaceSize(int remaining_disk_space) {
remaining_disk_space_ = remaining_disk_space;
}
int TestFileUtils::WriteFile(const base::FilePath& filename,
const char* data,
int size) {
if (remaining_disk_space_ != kNoLimit) {
if (size > remaining_disk_space_) {
// Disk full:
const int size_written = remaining_disk_space_;
if (size_written > 0)
FileUtilsWrapper::WriteFile(filename, data, size_written);
remaining_disk_space_ = 0;
return size_written;
}
remaining_disk_space_ -= size;
}
return FileUtilsWrapper::WriteFile(filename, data, size);
}
} // namespace web_app
// Copyright 2018 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.
#ifndef CHROME_BROWSER_WEB_APPLICATIONS_TEST_TEST_FILE_UTILS_H_
#define CHROME_BROWSER_WEB_APPLICATIONS_TEST_TEST_FILE_UTILS_H_
#include <memory>
#include "base/macros.h"
#include "chrome/browser/web_applications/file_utils_wrapper.h"
namespace web_app {
// A testing implementation to intercept calls to the file system.
class TestFileUtils : public FileUtilsWrapper {
public:
TestFileUtils() = default;
~TestFileUtils() override = default;
// FileUtilsWrapper:
std::unique_ptr<FileUtilsWrapper> Clone() override;
int WriteFile(const base::FilePath& filename,
const char* data,
int size) override;
static constexpr int kNoLimit = -1;
// Simulate "disk full" error: limit disk space for |WriteFile| operations.
void SetRemainingDiskSpaceSize(int remaining_disk_space);
private:
int remaining_disk_space_ = kNoLimit;
DISALLOW_COPY_AND_ASSIGN(TestFileUtils);
};
} // namespace web_app
#endif // CHROME_BROWSER_WEB_APPLICATIONS_TEST_TEST_FILE_UTILS_H_
......@@ -4,19 +4,15 @@
#include "chrome/browser/web_applications/web_app_database_factory.h"
#include "base/files/file_path.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "components/sync/model_impl/model_type_store_service_impl.h"
namespace web_app {
constexpr base::FilePath::CharType kWebAppsFolderName[] =
FILE_PATH_LITERAL("WebApps");
WebAppDatabaseFactory::WebAppDatabaseFactory(Profile* profile)
: model_type_store_service_(
std::make_unique<syncer::ModelTypeStoreServiceImpl>(
profile->GetPath().Append(base::FilePath(kWebAppsFolderName)))) {}
GetWebAppsDirectory(profile))) {}
WebAppDatabaseFactory::~WebAppDatabaseFactory() {}
......
// Copyright 2018 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/web_applications/web_app_icon_manager.h"
#include "base/callback.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "chrome/browser/web_applications/file_utils_wrapper.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "content/public/browser/browser_thread.h"
#include "ui/gfx/codec/png_codec.h"
namespace web_app {
namespace {
constexpr base::FilePath::CharType kTempDirectoryName[] =
FILE_PATH_LITERAL("Temp");
constexpr base::FilePath::CharType kIconsDirectoryName[] =
FILE_PATH_LITERAL("Icons");
base::FilePath GetTempDir(FileUtilsWrapper* utils,
const base::FilePath& web_apps_dir) {
// Create the temp directory as a sub-directory of the WebApps directory.
// This guarantees it is on the same file system as the WebApp's eventual
// install target.
base::FilePath temp_path = web_apps_dir.Append(kTempDirectoryName);
if (utils->PathExists(temp_path)) {
if (!utils->DirectoryExists(temp_path)) {
LOG(ERROR) << "Not a directory: " << temp_path.value();
return base::FilePath();
}
if (!utils->PathIsWritable(temp_path)) {
LOG(ERROR) << "Can't write to path: " << temp_path.value();
return base::FilePath();
}
// This is a directory we can write to.
return temp_path;
}
// Directory doesn't exist, so create it.
if (!utils->CreateDirectory(temp_path)) {
LOG(ERROR) << "Could not create directory: " << temp_path.value();
return base::FilePath();
}
return temp_path;
}
bool WriteIcon(FileUtilsWrapper* utils,
const base::FilePath& icons_dir,
const WebApplicationInfo::IconInfo& icon_info) {
base::FilePath icon_file =
icons_dir.AppendASCII(base::StringPrintf("%i.png", icon_info.width));
std::vector<unsigned char> image_data;
const bool discard_transparency = false;
if (!gfx::PNGCodec::EncodeBGRASkBitmap(icon_info.data, discard_transparency,
&image_data)) {
LOG(ERROR) << "Could not encode icon data.";
return false;
}
const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]);
int size = base::checked_cast<int>(image_data.size());
if (utils->WriteFile(icon_file, image_data_ptr, size) != size) {
LOG(ERROR) << "Could not write icon file.";
return false;
}
return true;
}
bool WriteIcons(FileUtilsWrapper* utils,
const base::FilePath& app_dir,
const WebApplicationInfo& web_app_info) {
const base::FilePath icons_dir = app_dir.Append(kIconsDirectoryName);
if (!utils->CreateDirectory(icons_dir)) {
LOG(ERROR) << "Could not create icons directory.";
return false;
}
for (const WebApplicationInfo::IconInfo& icon_info : web_app_info.icons) {
// Skip unfetched bitmaps.
if (icon_info.data.colorType() == kUnknown_SkColorType)
continue;
if (!WriteIcon(utils, icons_dir, icon_info))
return false;
}
return true;
}
// Performs blocking I/O. May be called on another thread.
// Returns true if no errors occured.
bool WriteDataBlocking(std::unique_ptr<FileUtilsWrapper> utils,
base::FilePath web_apps_directory,
AppId app_id,
std::unique_ptr<WebApplicationInfo> web_app_info) {
const base::FilePath temp_dir = GetTempDir(utils.get(), web_apps_directory);
if (temp_dir.empty()) {
LOG(ERROR)
<< "Could not get path to WebApps temporary directory in profile.";
return false;
}
base::ScopedTempDir app_temp_dir;
if (!app_temp_dir.CreateUniqueTempDirUnderPath(temp_dir)) {
LOG(ERROR) << "Could not create temporary WebApp directory.";
return false;
}
if (!WriteIcons(utils.get(), app_temp_dir.GetPath(), *web_app_info))
return false;
// Commit: move whole app data dir to final destination in one mv operation.
const base::FilePath app_id_dir = web_apps_directory.AppendASCII(app_id);
if (!utils->Move(app_temp_dir.GetPath(), app_id_dir)) {
LOG(ERROR) << "Could not move temp WebApp directory to final destination.";
return false;
}
app_temp_dir.Take();
return true;
}
} // namespace
WebAppIconManager::WebAppIconManager(Profile* profile,
std::unique_ptr<FileUtilsWrapper> utils)
: utils_(std::move(utils)) {
web_apps_directory_ = GetWebAppsDirectory(profile);
}
WebAppIconManager::~WebAppIconManager() = default;
void WebAppIconManager::WriteData(
AppId app_id,
std::unique_ptr<WebApplicationInfo> web_app_info,
WriteDataCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
constexpr base::TaskTraits traits = {
base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN};
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, traits,
base::BindOnce(WriteDataBlocking, utils_->Clone(), web_apps_directory_,
std::move(app_id), std::move(web_app_info)),
std::move(callback));
}
} // namespace web_app
// Copyright 2018 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.
#ifndef CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_ICON_MANAGER_H_
#define CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_ICON_MANAGER_H_
#include <memory>
#include "base/callback_forward.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/common/web_application_info.h"
class Profile;
struct WebApplicationInfo;
namespace web_app {
class FileUtilsWrapper;
// Exclusively used from the UI thread.
class WebAppIconManager {
public:
WebAppIconManager(Profile* profile, std::unique_ptr<FileUtilsWrapper> utils);
~WebAppIconManager();
using WriteDataCallback = base::OnceCallback<void(bool success)>;
void WriteData(AppId app_id,
std::unique_ptr<WebApplicationInfo> web_app_info,
WriteDataCallback callback);
private:
base::FilePath web_apps_directory_;
std::unique_ptr<FileUtilsWrapper> utils_;
DISALLOW_COPY_AND_ASSIGN(WebAppIconManager);
};
} // namespace web_app
#endif // CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_ICON_MANAGER_H_
......@@ -12,20 +12,30 @@
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/web_application_info.h"
#include "content/public/browser/browser_thread.h"
namespace web_app {
WebAppInstallFinalizer::WebAppInstallFinalizer(WebAppRegistrar* registrar)
: registrar_(registrar) {}
WebAppInstallFinalizer::WebAppInstallFinalizer(WebAppRegistrar* registrar,
WebAppIconManager* icon_manager)
: registrar_(registrar), icon_manager_(icon_manager) {}
WebAppInstallFinalizer::~WebAppInstallFinalizer() = default;
void WebAppInstallFinalizer::FinalizeInstall(
std::unique_ptr<WebApplicationInfo> web_app_info,
InstallFinalizedCallback callback) {
const AppId app_id = GenerateAppIdFromURL(web_app_info->app_url);
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
AppId app_id = GenerateAppIdFromURL(web_app_info->app_url);
if (registrar_->GetAppById(app_id)) {
std::move(callback).Run(app_id, InstallResultCode::kAlreadyInstalled);
return;
}
auto web_app = std::make_unique<WebApp>(app_id);
web_app->SetName(base::UTF16ToUTF8(web_app_info->title));
......@@ -34,11 +44,30 @@ void WebAppInstallFinalizer::FinalizeInstall(
web_app->SetScope(web_app_info->scope);
web_app->SetThemeColor(web_app_info->theme_color);
// TODO(loyso): Add web_app_info->icons into web_app. Save them on disk.
icon_manager_->WriteData(
std::move(app_id), std::move(web_app_info),
base::BindOnce(&WebAppInstallFinalizer::OnDataWritten,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
std::move(web_app)));
}
void WebAppInstallFinalizer::OnDataWritten(InstallFinalizedCallback callback,
std::unique_ptr<WebApp> web_app,
bool success) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!success) {
std::move(callback).Run(AppId(), InstallResultCode::kWriteDataFailed);
return;
}
// TODO(loyso): Add |Icons| object into WebApp to iterate over icons and to
// load SkBitmap pixels asynchronously (save memory).
AppId app_id = web_app->app_id();
registrar_->RegisterApp(std::move(web_app));
std::move(callback).Run(app_id, InstallResultCode::kSuccess);
std::move(callback).Run(std::move(app_id), InstallResultCode::kSuccess);
}
} // namespace web_app
......@@ -8,17 +8,21 @@
#include <memory>
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/web_applications/components/install_finalizer.h"
struct WebApplicationInfo;
namespace web_app {
class WebApp;
class WebAppIconManager;
class WebAppRegistrar;
class WebAppInstallFinalizer final : public InstallFinalizer {
public:
explicit WebAppInstallFinalizer(WebAppRegistrar* registrar);
WebAppInstallFinalizer(WebAppRegistrar* registrar,
WebAppIconManager* icon_manager);
~WebAppInstallFinalizer() override;
// InstallFinalizer:
......@@ -26,7 +30,14 @@ class WebAppInstallFinalizer final : public InstallFinalizer {
InstallFinalizedCallback callback) override;
private:
void OnDataWritten(InstallFinalizedCallback callback,
std::unique_ptr<WebApp> web_app,
bool success);
WebAppRegistrar* registrar_;
WebAppIconManager* icon_manager_;
base::WeakPtrFactory<WebAppInstallFinalizer> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(WebAppInstallFinalizer);
};
......
......@@ -17,10 +17,12 @@
#include "chrome/browser/web_applications/extensions/pending_bookmark_app_manager.h"
#include "chrome/browser/web_applications/extensions/web_app_extension_ids_map.h"
#include "chrome/browser/web_applications/external_web_apps.h"
#include "chrome/browser/web_applications/file_utils_wrapper.h"
#include "chrome/browser/web_applications/policy/web_app_policy_manager.h"
#include "chrome/browser/web_applications/system_web_app_manager.h"
#include "chrome/browser/web_applications/web_app_database.h"
#include "chrome/browser/web_applications/web_app_database_factory.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_finalizer.h"
#include "chrome/browser/web_applications/web_app_install_manager.h"
#include "chrome/browser/web_applications/web_app_provider_factory.h"
......@@ -62,9 +64,11 @@ void WebAppProvider::CreateWebAppsSubsystems(Profile* profile) {
database_factory_ = std::make_unique<WebAppDatabaseFactory>(profile);
database_ = std::make_unique<WebAppDatabase>(database_factory_.get());
registrar_ = std::make_unique<WebAppRegistrar>(database_.get());
icon_manager_ = std::make_unique<WebAppIconManager>(
profile, std::make_unique<FileUtilsWrapper>());
auto install_finalizer =
std::make_unique<WebAppInstallFinalizer>(registrar_.get());
auto install_finalizer = std::make_unique<WebAppInstallFinalizer>(
registrar_.get(), icon_manager_.get());
install_manager_ = std::make_unique<WebAppInstallManager>(
profile, std::move(install_finalizer));
......@@ -132,6 +136,7 @@ void WebAppProvider::Reset() {
pending_app_manager_.reset();
install_manager_.reset();
icon_manager_.reset();
registrar_.reset();
database_.reset();
database_factory_.reset();
......
......@@ -34,6 +34,7 @@ class InstallManager;
// Forward declarations for new extension-independent subsystems.
class WebAppDatabase;
class WebAppDatabaseFactory;
class WebAppIconManager;
class WebAppRegistrar;
// Forward declarations for legacy extension-based subsystems.
......@@ -87,6 +88,7 @@ class WebAppProvider : public KeyedService,
std::unique_ptr<WebAppDatabaseFactory> database_factory_;
std::unique_ptr<WebAppDatabase> database_;
std::unique_ptr<WebAppRegistrar> registrar_;
std::unique_ptr<WebAppIconManager> icon_manager_;
// New generalized subsystems:
std::unique_ptr<InstallManager> install_manager_;
......
......@@ -4,13 +4,21 @@
#include "chrome/browser/web_applications/web_app_utils.h"
#include "base/files/file_path.h"
#include "chrome/browser/profiles/profile.h"
namespace web_app {
constexpr base::FilePath::CharType kWebAppsDirectoryName[] =
FILE_PATH_LITERAL("WebApps");
bool AllowWebAppInstallation(Profile* profile) {
return !profile->IsGuestSession() && !profile->IsOffTheRecord() &&
!profile->IsSystemProfile();
}
base::FilePath GetWebAppsDirectory(Profile* profile) {
return profile->GetPath().Append(base::FilePath(kWebAppsDirectoryName));
}
} // namespace web_app
......@@ -7,10 +7,16 @@
class Profile;
namespace base {
class FilePath;
}
namespace web_app {
bool AllowWebAppInstallation(Profile* profile);
base::FilePath GetWebAppsDirectory(Profile* profile);
} // namespace web_app
#endif // CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_UTILS_H_
......@@ -52927,6 +52927,7 @@ Full version information for the fingerprint enum values:
<int value="3" label="GetWebApplicationInfoFailed"/>
<int value="4" label="PreviouslyUninstalled"/>
<int value="5" label="WebContentsDestroyed"/>
<int value="6" label="WriteDataFailed"/>
</enum>
<enum name="WebAppInstallSource">
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