Commit 2aa8d2e9 authored by flackr's avatar flackr Committed by Commit bot

Use chromeos accelerometer reader in athena and support only lid accelerometer.

BUG=380831
TEST=Glimmer/clapper still work, manually tested not having a base accelerometer.

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

Cr-Commit-Position: refs/heads/master@{#295258}
parent 3d0e6906
......@@ -88,8 +88,6 @@
'screen/screen_manager_impl.cc',
'system/background_controller.cc',
'system/background_controller.h',
'system/device_socket_listener.cc',
'system/device_socket_listener.h',
'system/network_selector.cc',
'system/network_selector.h',
'system/orientation_controller.cc',
......
......@@ -83,7 +83,7 @@ class VirtualKeyboardObserver : public keyboard::KeyboardControllerObserver {
DISALLOW_COPY_AND_ASSIGN(VirtualKeyboardObserver);
};
void StartAthenaEnv(scoped_refptr<base::TaskRunner> file_runner) {
void StartAthenaEnv(scoped_refptr<base::TaskRunner> blocking_task_runner) {
athena::AthenaEnv::Create();
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
......@@ -109,7 +109,7 @@ void StartAthenaEnv(scoped_refptr<base::TaskRunner> file_runner) {
athena::InputManager::Create()->OnRootWindowCreated(root_window);
athena::ScreenManager::Create(root_window);
athena::SystemUI::Create(file_runner);
athena::SystemUI::Create(blocking_task_runner);
athena::WindowManager::Create();
athena::AppRegistry::Create();
SetupBackgroundImage();
......
......@@ -142,8 +142,9 @@ class AthenaBrowserMainDelegate : public extensions::ShellBrowserMainDelegate {
extension_system->LoadApp(app_absolute_dir);
}
athena::StartAthenaEnv(content::BrowserThread::GetMessageLoopProxyForThread(
content::BrowserThread::FILE));
athena::StartAthenaEnv(content::BrowserThread::GetBlockingPool()->
GetTaskRunnerWithShutdownBehavior(
base::SequencedWorkerPool::SKIP_ON_SHUTDOWN));
athena::ExtensionsDelegate::CreateExtensionsDelegateForShell(context);
athena::CreateVirtualKeyboardWithContext(context);
athena::StartAthenaSessionWithContext(context);
......
This diff is collapsed.
// Copyright 2014 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 ATHENA_SYSTEM_DEVICE_SOCKET_LISTENER_H_
#define ATHENA_SYSTEM_DEVICE_SOCKET_LISTENER_H_
#include <string>
#include "base/basictypes.h"
#include "base/memory/ref_counted.h"
namespace base {
class TaskRunner;
}
namespace athena {
// This class reads device-data from a socket.
class DeviceSocketListener {
public:
DeviceSocketListener(const std::string& socket_parth,
size_t data_size);
virtual ~DeviceSocketListener();
static void CreateSocketManager(
scoped_refptr<base::TaskRunner> io_task_runner);
static void ShutdownSocketManager();
// This is called on the FILE thread when data is available on the socket.
// |data| is guaranteed to be of exactly |data_size| length. Note that the
// implementation must not mutate or free the data.
virtual void OnDataAvailableOnFILE(const void* data) = 0;
protected:
void StartListening();
void StopListening();
private:
const std::string socket_path_;
size_t data_size_;
DISALLOW_COPY_AND_ASSIGN(DeviceSocketListener);
};
} // namespace athena
#endif // ATHENA_SYSTEM_DEVICE_SOCKET_LISTENER_H_
......@@ -14,112 +14,34 @@ namespace athena {
namespace {
// Path of the socket which the sensor daemon creates.
const char kSocketPath[] = "/dev/sensors/orientation";
// Threshold after which to rotate in a given direction.
const int kGravityThreshold = 6.0f;
// Minimum delay before triggering another orientation change.
const int kOrientationChangeDelayNS = 500000000;
enum SensorType {
SENSOR_ACCELEROMETER,
SENSOR_LIGHT,
SENSOR_PROXIMITY
};
// A sensor event from the device.
struct DeviceSensorEvent {
// The type of event from the SensorType enum above.
int32_t type;
// The time in nanoseconds at which the event happened.
int64_t timestamp;
union {
// Accelerometer X,Y,Z values in SI units (m/s^2) including gravity.
// The orientation is described at
// http://www.html5rocks.com/en/tutorials/device/orientation/.
float data[3];
// Ambient (room) temperature in degrees Celcius.
float temperature;
// Proximity sensor distance in centimeters.
float distance;
// Ambient light level in SI lux units.
float light;
};
};
} // namespace
OrientationController::OrientationController()
: DeviceSocketListener(kSocketPath, sizeof(DeviceSensorEvent)),
last_orientation_change_time_(0),
shutdown_(false) {
CHECK(base::MessageLoopForUI::current());
ui_task_runner_ = base::MessageLoopForUI::current()->task_runner();
OrientationController::OrientationController() {
}
void OrientationController::InitWith(
scoped_refptr<base::TaskRunner> file_task_runner) {
file_task_runner_ = file_task_runner;
file_task_runner->PostTask(
FROM_HERE,
base::Bind(&OrientationController::WatchForSocketPathOnFILE, this));
scoped_refptr<base::TaskRunner> blocking_task_runner) {
accelerometer_reader_.reset(
new chromeos::AccelerometerReader(blocking_task_runner, this));
}
OrientationController::~OrientationController() {
}
void OrientationController::Shutdown() {
CHECK(file_task_runner_.get());
StopListening();
file_task_runner_->PostTask(
FROM_HERE,
base::Bind(&OrientationController::ShutdownOnFILE, this));
}
void OrientationController::ShutdownOnFILE() {
shutdown_ = true;
watcher_.reset();
accelerometer_reader_.reset();
}
void OrientationController::WatchForSocketPathOnFILE() {
CHECK(base::MessageLoopForIO::current());
if (base::PathExists(base::FilePath(kSocketPath))) {
ui_task_runner_->PostTask(FROM_HERE,
base::Bind(&OrientationController::StartListening, this));
} else {
watcher_.reset(new base::FilePathWatcher);
watcher_->Watch(
base::FilePath(kSocketPath),
false,
base::Bind(&OrientationController::OnFilePathChangedOnFILE, this));
}
}
void OrientationController::OnFilePathChangedOnFILE(const base::FilePath& path,
bool error) {
watcher_.reset();
if (error || shutdown_)
void OrientationController::HandleAccelerometerUpdate(
const ui::AccelerometerUpdate& update) {
if (!update.has(ui::ACCELEROMETER_SOURCE_SCREEN))
return;
ui_task_runner_->PostTask(FROM_HERE,
base::Bind(&OrientationController::StartListening, this));
}
void OrientationController::OnDataAvailableOnFILE(const void* data) {
const DeviceSensorEvent* event =
static_cast<const DeviceSensorEvent*>(data);
if (event->type != SENSOR_ACCELEROMETER)
return;
float gravity_x = event->data[0];
float gravity_y = event->data[1];
float gravity_x = update.get(ui::ACCELEROMETER_SOURCE_SCREEN).x();
float gravity_y = update.get(ui::ACCELEROMETER_SOURCE_SCREEN).y();
gfx::Display::Rotation rotation;
if (gravity_x < -kGravityThreshold) {
rotation = gfx::Display::ROTATE_270;
......@@ -134,24 +56,11 @@ void OrientationController::OnDataAvailableOnFILE(const void* data) {
return;
}
if (rotation == current_rotation_ ||
event->timestamp - last_orientation_change_time_ <
kOrientationChangeDelayNS) {
if (rotation == current_rotation_)
return;
}
last_orientation_change_time_ = event->timestamp;
current_rotation_ = rotation;
ui_task_runner_->PostTask(FROM_HERE,
base::Bind(&OrientationController::RotateOnUI, this, rotation));
}
void OrientationController::RotateOnUI(gfx::Display::Rotation rotation) {
ScreenManager* screen_manager = ScreenManager::Get();
// Since this is called from the FILE thread, the screen manager may no longer
// exist.
if (screen_manager)
screen_manager->SetRotation(rotation);
ScreenManager::Get()->SetRotation(rotation);
}
} // namespace athena
......@@ -5,16 +5,14 @@
#ifndef ATHENA_SYSTEM_ORIENTATION_CONTROLLER_H_
#define ATHENA_SYSTEM_ORIENTATION_CONTROLLER_H_
#include "athena/system/device_socket_listener.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
#include "chromeos/accelerometer/accelerometer_reader.h"
#include "ui/gfx/display.h"
namespace base {
class FilePath;
class FilePathWatcher;
class TaskRunner;
}
......@@ -23,50 +21,23 @@ namespace athena {
// Monitors accelerometers, detecting orientation changes. When a change is
// detected rotates the root window to match.
class OrientationController
: public DeviceSocketListener,
public base::RefCountedThreadSafe<OrientationController> {
: public chromeos::AccelerometerReader::Delegate {
public:
OrientationController();
void InitWith(scoped_refptr<base::TaskRunner> file_task_runner);
void Shutdown();
private:
friend class base::RefCountedThreadSafe<OrientationController>;
virtual ~OrientationController();
void ShutdownOnFILE();
// Watch for the socket path to be created, called on the FILE thread.
void WatchForSocketPathOnFILE();
void OnFilePathChangedOnFILE(const base::FilePath& path, bool error);
// Overridden from device::DeviceSocketListener:
virtual void OnDataAvailableOnFILE(const void* data) OVERRIDE;
void InitWith(scoped_refptr<base::TaskRunner> blocking_task_runner);
void Shutdown();
// Rotates the display to |rotation|, called on the UI thread.
void RotateOnUI(gfx::Display::Rotation rotation);
// chromeos::AccelerometerReader::Delegate
virtual void HandleAccelerometerUpdate(
const ui::AccelerometerUpdate& update) OVERRIDE;
private:
// The last configured rotation.
gfx::Display::Rotation current_rotation_;
// The timestamp of the last applied orientation change.
int64_t last_orientation_change_time_;
// True if the OrientaionController has already been shutdown.
// This is initialized on UI thread, but must be accessed / modified
// only on FILE thread.
bool shutdown_;
// A task runner for the UI thread.
scoped_refptr<base::TaskRunner> ui_task_runner_;
// A task runner for the FILE thread.
scoped_refptr<base::TaskRunner> file_task_runner_;
// File path watcher used to detect when sensors are present.
scoped_ptr<base::FilePathWatcher> watcher_;
scoped_ptr<chromeos::AccelerometerReader> accelerometer_reader_;
DISALLOW_COPY_AND_ASSIGN(OrientationController);
};
......
......@@ -6,7 +6,6 @@
#include "athena/screen/public/screen_manager.h"
#include "athena/system/background_controller.h"
#include "athena/system/device_socket_listener.h"
#include "athena/system/orientation_controller.h"
#include "athena/system/power_button_controller.h"
#include "athena/system/status_icon_container_view.h"
......@@ -75,12 +74,12 @@ class SystemInfoView : public views::View {
class SystemUIImpl : public SystemUI {
public:
SystemUIImpl(scoped_refptr<base::TaskRunner> file_task_runner)
SystemUIImpl(scoped_refptr<base::TaskRunner> blocking_task_runner)
: orientation_controller_(new OrientationController()),
power_button_controller_(new PowerButtonController),
background_container_(NULL),
system_modal_container_(NULL) {
orientation_controller_->InitWith(file_task_runner);
orientation_controller_->InitWith(blocking_task_runner);
}
virtual ~SystemUIImpl() {
......@@ -113,7 +112,7 @@ class SystemUIImpl : public SystemUI {
}
private:
scoped_refptr<OrientationController> orientation_controller_;
scoped_ptr<OrientationController> orientation_controller_;
scoped_ptr<PowerButtonController> power_button_controller_;
scoped_ptr<BackgroundController> background_controller_;
......@@ -129,9 +128,9 @@ class SystemUIImpl : public SystemUI {
} // namespace
// static
SystemUI* SystemUI::Create(scoped_refptr<base::TaskRunner> file_task_runner) {
DeviceSocketListener::CreateSocketManager(file_task_runner);
SystemUIImpl* system_ui = new SystemUIImpl(file_task_runner);
SystemUI* SystemUI::Create(
scoped_refptr<base::TaskRunner> blocking_task_runner) {
SystemUIImpl* system_ui = new SystemUIImpl(blocking_task_runner);
instance = system_ui;
system_ui->Init();
return instance;
......@@ -148,7 +147,6 @@ void SystemUI::Shutdown() {
CHECK(instance);
delete instance;
instance = NULL;
DeviceSocketListener::ShutdownSocketManager();
}
} // namespace athena
......@@ -60,6 +60,7 @@ void AthenaTestHelper::SetUp(ui::ContextFactory* context_factory) {
aura::test::EnvTestHelper(aura::Env::GetInstance())
.SetInputStateLookup(scoped_ptr<aura::InputStateLookup>());
// TODO(oshima): Use a BlockingPool task runner.
athena::StartAthenaEnv(file_thread_->message_loop_proxy());
athena::ExtensionsDelegate::CreateExtensionsDelegateForTest();
athena::StartAthenaSession(new SampleActivityFactory(),
......
......@@ -38,8 +38,9 @@ class ChromeBrowserMainExtraPartsAthena : public ChromeBrowserMainExtraParts,
private:
// Overridden from ChromeBrowserMainExtraParts:
virtual void PreProfileInit() OVERRIDE {
athena::StartAthenaEnv(content::BrowserThread::GetMessageLoopProxyForThread(
content::BrowserThread::FILE));
athena::StartAthenaEnv(content::BrowserThread::GetBlockingPool()->
GetTaskRunnerWithShutdownBehavior(
base::SequencedWorkerPool::SKIP_ON_SHUTDOWN));
}
virtual void PostProfileInit() OVERRIDE {
if (!CommandLine::ForCurrentProcess()->HasSwitch(
......
......@@ -27,28 +27,29 @@ const base::FilePath::CharType kAccelerometerDevicePath[] =
const base::FilePath::CharType kAccelerometerIioBasePath[] =
FILE_PATH_LITERAL("/sys/bus/iio/devices/");
// Files within the device in kAccelerometerIioBasePath containing the scales of
// File within the device in kAccelerometerIioBasePath containing the scale of
// the accelerometers.
const base::FilePath::CharType kAccelerometerBaseScaleName[] =
FILE_PATH_LITERAL("in_accel_base_scale");
const base::FilePath::CharType kAccelerometerLidScaleName[] =
FILE_PATH_LITERAL("in_accel_lid_scale");
const base::FilePath::CharType kScaleNameFormatString[] = "in_accel_%s_scale";
// The filename giving the path to read the scan index of each accelerometer
// axis.
const char kAccelerometerScanIndexPath[] =
"scan_elements/in_accel_%s_%s_index";
// The names of the accelerometers and axes in the order we want to read them.
const char kAccelerometerNames[][5] = {"base", "lid"};
const char kAccelerometerAxes[][2] = {"x", "y", "z"};
const size_t kTriggerDataValues =
arraysize(kAccelerometerNames) * arraysize(kAccelerometerAxes);
const size_t kTriggerDataLength = kTriggerDataValues * 2;
// The names of the accelerometers. Matches up with the enum AccelerometerSource
// in ui/accelerometer/accelerometer_types.h.
const char kAccelerometerNames[ui::ACCELEROMETER_SOURCE_COUNT][5] = {
"lid", "base"};
// The axes on each accelerometer.
const char kAccelerometerAxes[][2] = {"y", "x", "z"};
// The length required to read uint values from configuration files.
const size_t kMaxAsciiUintLength = 21;
// The size of individual values.
const size_t kDataSize = 2;
// The time to wait between reading the accelerometer.
const int kDelayBetweenReadsMs = 100;
......@@ -57,15 +58,14 @@ const float kMeanGravity = 9.80665f;
// Reads |path| to the unsigned int pointed to by |value|. Returns true on
// success or false on failure.
bool ReadFileToUint(const base::FilePath& path, unsigned int* value) {
bool ReadFileToInt(const base::FilePath& path, int* value) {
std::string s;
DCHECK(value);
if (!base::ReadFileToString(path, &s, kMaxAsciiUintLength)) {
LOG(ERROR) << "Failed to read " << path.value();
return false;
}
base::TrimWhitespaceASCII(s, base::TRIM_ALL, &s);
if (!base::StringToUint(s, value)) {
if (!base::StringToInt(s, value)) {
LOG(ERROR) << "Failed to parse \"" << s << "\" from " << path.value();
return false;
}
......@@ -90,41 +90,62 @@ bool DetectAndReadAccelerometerConfiguration(
base::FilePath iio_path(base::FilePath(kAccelerometerIioBasePath).Append(
device));
// Read accelerometer scales
if (!ReadFileToUint(iio_path.Append(kAccelerometerBaseScaleName),
&(configuration->data.base_scale))) {
return false;
}
if (!ReadFileToUint(iio_path.Append(kAccelerometerLidScaleName),
&(configuration->data.lid_scale))) {
return false;
}
// Read indices of each accelerometer axis from each accelerometer from
// /sys/bus/iio/devices/iio:deviceX/scan_elements/in_accel_{x,y,z}_%s_index
// Read configuration of each accelerometer axis from each accelerometer from
// /sys/bus/iio/devices/iio:deviceX/.
for (size_t i = 0; i < arraysize(kAccelerometerNames); ++i) {
// Read scale of accelerometer.
std::string accelerometer_scale_path = base::StringPrintf(
kScaleNameFormatString, kAccelerometerNames[i]);
int scale_divisor;
if (!ReadFileToInt(iio_path.Append(accelerometer_scale_path.c_str()),
&scale_divisor)) {
configuration->data.has[i] = false;
continue;
}
configuration->data.has[i] = true;
configuration->data.count++;
for (size_t j = 0; j < arraysize(kAccelerometerAxes); ++j) {
configuration->data.scale[i][j] = kMeanGravity / scale_divisor;
std::string accelerometer_index_path = base::StringPrintf(
kAccelerometerScanIndexPath, kAccelerometerAxes[j],
kAccelerometerNames[i]);
unsigned int index = 0;
if (!ReadFileToUint(iio_path.Append(accelerometer_index_path.c_str()),
&index)) {
if (!ReadFileToInt(iio_path.Append(accelerometer_index_path.c_str()),
&(configuration->data.index[i][j]))) {
return false;
}
if (index >= kTriggerDataValues) {
LOG(ERROR) << "Field index from " << accelerometer_index_path
<< " out of bounds: " << index;
}
}
// Adjust the directions of accelerometers to match the AccelerometerUpdate
// type specified in ui/accelerometer/accelerometer_types.h.
configuration->data.scale[ui::ACCELEROMETER_SOURCE_SCREEN][0] *= -1.0f;
for (int i = 0; i < 3; ++i) {
configuration->data.scale[ui::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD][i] *=
-1.0f;
}
// Verify indices are within bounds.
for (int i = 0; i < ui::ACCELEROMETER_SOURCE_COUNT; ++i) {
if (!configuration->data.has[i])
continue;
for (int j = 0; j < 3; ++j) {
if (configuration->data.index[i][j] < 0 ||
configuration->data.index[i][j] >=
3 * static_cast<int>(configuration->data.count)) {
LOG(ERROR) << "Field index for " << kAccelerometerNames[i] << " "
<< kAccelerometerAxes[j] << " axis out of bounds.";
return false;
}
configuration->data.index.push_back(index);
}
}
configuration->data.length = kDataSize * 3 * configuration->data.count;
return true;
}
bool ReadAccelerometer(
scoped_refptr<AccelerometerReader::Reading> reading) {
scoped_refptr<AccelerometerReader::Reading> reading,
size_t length) {
// Initiate the trigger to read accelerometers simultaneously
int bytes_written = base::WriteFile(
base::FilePath(kAccelerometerTriggerPath), "1\n", 2);
......@@ -135,10 +156,10 @@ bool ReadAccelerometer(
// Read resulting sample from /dev/cros-ec-accel.
int bytes_read = base::ReadFile(base::FilePath(kAccelerometerDevicePath),
reading->data, kTriggerDataLength);
if (bytes_read < static_cast<int>(kTriggerDataLength)) {
reading->data, length);
if (bytes_read < static_cast<int>(length)) {
LOG(ERROR) << "Read " << bytes_read << " byte(s), expected "
<< kTriggerDataLength << " bytes from accelerometer";
<< length << " bytes from accelerometer";
return false;
}
return true;
......@@ -146,16 +167,24 @@ bool ReadAccelerometer(
} // namespace
AccelerometerReader::ConfigurationData::ConfigurationData() {
AccelerometerReader::ConfigurationData::ConfigurationData()
: count(0) {
for (int i = 0; i < ui::ACCELEROMETER_SOURCE_COUNT; ++i) {
has[i] = false;
for (int j = 0; j < 3; ++j) {
scale[i][j] = 0;
index[i][j] = -1;
}
}
}
AccelerometerReader::ConfigurationData::~ConfigurationData() {
}
AccelerometerReader::AccelerometerReader(
base::TaskRunner* task_runner,
base::TaskRunner* blocking_task_runner,
AccelerometerReader::Delegate* delegate)
: task_runner_(task_runner),
: task_runner_(blocking_task_runner),
delegate_(delegate),
configuration_(new AccelerometerReader::Configuration()),
weak_factory_(this) {
......@@ -186,7 +215,8 @@ void AccelerometerReader::TriggerRead() {
new AccelerometerReader::Reading());
base::PostTaskAndReplyWithResult(task_runner_.get(),
FROM_HERE,
base::Bind(&ReadAccelerometer, reading),
base::Bind(&ReadAccelerometer, reading,
configuration_->data.length),
base::Bind(&AccelerometerReader::OnDataRead,
weak_factory_.GetWeakPtr(),
reading));
......@@ -198,17 +228,19 @@ void AccelerometerReader::OnDataRead(
DCHECK(!task_runner_->RunsTasksOnCurrentThread());
if (success) {
int16* values = reinterpret_cast<int16*>(reading->data);
float lid_scale = kMeanGravity / configuration_->data.lid_scale;
update_.Set(ui::ACCELEROMETER_SOURCE_SCREEN,
-values[configuration_->data.index[4]] * lid_scale,
values[configuration_->data.index[3]] * lid_scale,
values[configuration_->data.index[5]] * lid_scale);
float base_scale = kMeanGravity / configuration_->data.base_scale;
update_.Set(ui::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD,
-values[configuration_->data.index[1]] * base_scale,
-values[configuration_->data.index[0]] * base_scale,
-values[configuration_->data.index[2]] * base_scale);
for (int i = 0; i < ui::ACCELEROMETER_SOURCE_COUNT; ++i) {
if (!configuration_->data.has[i])
continue;
int16* values = reinterpret_cast<int16*>(reading->data);
update_.Set(static_cast<ui::AccelerometerSource>(i),
values[configuration_->data.index[i][0]] *
configuration_->data.scale[i][0],
values[configuration_->data.index[i][1]] *
configuration_->data.scale[i][1],
values[configuration_->data.index[i][2]] *
configuration_->data.scale[i][2]);
}
delegate_->HandleAccelerometerUpdate(update_);
}
......
......@@ -5,8 +5,6 @@
#ifndef CHROMEOS_ACCELEROMETER_ACCELEROMETER_READER_H_
#define CHROMEOS_ACCELEROMETER_ACCELEROMETER_READER_H_
#include <vector>
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
......@@ -28,12 +26,20 @@ class CHROMEOS_EXPORT AccelerometerReader {
ConfigurationData();
~ConfigurationData();
// Scale of accelerometers (i.e. raw value * 1.0f / scale = G's).
unsigned int base_scale;
unsigned int lid_scale;
// Number of accelerometers on device.
size_t count;
// Length of accelerometer updates.
size_t length;
// Which accelerometers are present on device.
bool has[ui::ACCELEROMETER_SOURCE_COUNT];
// Scale of accelerometers (i.e. raw value * scale = m/s^2).
float scale[ui::ACCELEROMETER_SOURCE_COUNT][3];
// Index of each accelerometer axis in data stream.
std::vector<unsigned int> index;
int index[ui::ACCELEROMETER_SOURCE_COUNT][3];
};
typedef base::RefCountedData<ConfigurationData> Configuration;
typedef base::RefCountedData<char[12]> Reading;
......
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