Commit cbce79c1 authored by Wei Lee's avatar Wei Lee Committed by Commit Bot

Supporting recording long-length video via camera intents

This CL supports the data chunk mechanism so that we could record
long-length video via camera intents.

Bug: 980812, 967611, b/131809655, b/134635999
Test: am start -a android.media.action.VIDEO_CAPTURE
Change-Id: Ieb18c614d942fb1f59743a37eb5cfa9cbab40128
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1722167
Commit-Queue: Wei Lee <wtlee@chromium.org>
Reviewed-by: default avatarYusuke Sato <yusukes@chromium.org>
Reviewed-by: default avatarDavid Jacobo <djacobo@chromium.org>
Reviewed-by: default avatarKen Rockot <rockot@google.com>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarShik Chen <shik@chromium.org>
Cr-Commit-Position: refs/heads/master@{#698819}
parent bb91dc23
...@@ -78,13 +78,16 @@ void TranslateVideoDeviceId( ...@@ -78,13 +78,16 @@ void TranslateVideoDeviceId(
std::move(callback_on_io_thread)); std::move(callback_on_io_thread));
} }
void TriggerCameraIntent(content::BrowserContext* context, void HandleCameraResult(
uint32_t intent_id, content::BrowserContext* context,
bool is_success, uint32_t intent_id,
const std::vector<uint8_t>& captured_data) { arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& data,
cros::mojom::CameraAppHelper::HandleCameraResultCallback callback) {
auto* intent_helper = auto* intent_helper =
arc::ArcIntentHelperBridge::GetForBrowserContext(context); arc::ArcIntentHelperBridge::GetForBrowserContext(context);
intent_helper->OnCameraIntentHandled(intent_id, is_success, captured_data); intent_helper->HandleCameraResult(intent_id, action, data,
std::move(callback));
} }
// Connects to CameraAppDeviceProvider which could be used to get // Connects to CameraAppDeviceProvider which could be used to get
...@@ -117,11 +120,11 @@ void ConnectToCameraAppDeviceProvider( ...@@ -117,11 +120,11 @@ void ConnectToCameraAppDeviceProvider(
// Connects to CameraAppHelper that could handle camera intents. // Connects to CameraAppHelper that could handle camera intents.
void ConnectToCameraAppHelper(cros::mojom::CameraAppHelperRequest request, void ConnectToCameraAppHelper(cros::mojom::CameraAppHelperRequest request,
content::RenderFrameHost* source) { content::RenderFrameHost* source) {
auto intent_callback = base::BindRepeating( auto handle_result_callback = base::BindRepeating(
&TriggerCameraIntent, source->GetProcess()->GetBrowserContext()); &HandleCameraResult, source->GetProcess()->GetBrowserContext());
auto camera_app_helper = auto camera_app_helper =
std::make_unique<chromeos_camera::CameraAppHelperImpl>( std::make_unique<chromeos_camera::CameraAppHelperImpl>(
std::move(intent_callback)); std::move(handle_result_callback));
mojo::MakeStrongBinding(std::move(camera_app_helper), std::move(request)); mojo::MakeStrongBinding(std::move(camera_app_helper), std::move(request));
} }
#endif #endif
......
...@@ -115,7 +115,6 @@ ArcIntentHelperBridge::ArcIntentHelperBridge(content::BrowserContext* context, ...@@ -115,7 +115,6 @@ ArcIntentHelperBridge::ArcIntentHelperBridge(content::BrowserContext* context,
ArcBridgeService* bridge_service) ArcBridgeService* bridge_service)
: context_(context), : context_(context),
arc_bridge_service_(bridge_service), arc_bridge_service_(bridge_service),
camera_intent_id_(0),
allowed_arc_schemes_(std::cbegin(kArcSchemes), std::cend(kArcSchemes)) { allowed_arc_schemes_(std::cbegin(kArcSchemes), std::cend(kArcSchemes)) {
arc_bridge_service_->intent_helper()->SetHost(this); arc_bridge_service_->intent_helper()->SetHost(this);
} }
...@@ -222,28 +221,44 @@ void ArcIntentHelperBridge::RecordShareFilesMetrics(mojom::ShareFiles flag) { ...@@ -222,28 +221,44 @@ void ArcIntentHelperBridge::RecordShareFilesMetrics(mojom::ShareFiles flag) {
UMA_HISTOGRAM_ENUMERATION("Arc.ShareFilesOnExit", flag); UMA_HISTOGRAM_ENUMERATION("Arc.ShareFilesOnExit", flag);
} }
void ArcIntentHelperBridge::LaunchCameraApp(arc::mojom::CameraIntentMode mode, void ArcIntentHelperBridge::LaunchCameraApp(uint32_t intent_id,
arc::mojom::CameraIntentMode mode,
bool should_handle_result, bool should_handle_result,
bool should_down_scale, bool should_down_scale,
bool is_secure, bool is_secure) {
LaunchCameraAppCallback callback) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
launch_camera_app_callback_map_.emplace(camera_intent_id_,
std::move(callback));
base::DictionaryValue intent_info; base::DictionaryValue intent_info;
std::string mode_str = std::string mode_str =
mode == arc::mojom::CameraIntentMode::PHOTO ? "photo" : "video"; mode == arc::mojom::CameraIntentMode::PHOTO ? "photo" : "video";
std::stringstream queries; std::stringstream queries;
queries << "?intentId=" << camera_intent_id_ << "&mode=" << mode_str queries << "?intentId=" << intent_id << "&mode=" << mode_str
<< "&shouldHandleResult=" << should_handle_result << "&shouldHandleResult=" << should_handle_result
<< "&shouldDownScale=" << should_down_scale << "&shouldDownScale=" << should_down_scale
<< "&isSecure=" << is_secure; << "&isSecure=" << is_secure;
ash::NewWindowDelegate::GetInstance()->LaunchCameraApp(queries.str()); ash::NewWindowDelegate::GetInstance()->LaunchCameraApp(queries.str());
}
void ArcIntentHelperBridge::HandleCameraResult(
uint32_t intent_id,
arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& data,
arc::mojom::IntentHelperInstance::HandleCameraResultCallback callback) {
auto* arc_service_manager = arc::ArcServiceManager::Get();
arc::mojom::IntentHelperInstance* instance = nullptr;
if (arc_service_manager) {
instance = ARC_GET_INSTANCE_FOR_METHOD(
arc_service_manager->arc_bridge_service()->intent_helper(),
HandleCameraResult);
}
if (!instance) {
LOG(ERROR) << "Failed to get instance for HandleCameraResult().";
std::move(callback).Run(false);
return;
}
camera_intent_id_++; instance->HandleCameraResult(intent_id, action, data, std::move(callback));
} }
ArcIntentHelperBridge::GetResult ArcIntentHelperBridge::GetActivityIcons( ArcIntentHelperBridge::GetResult ArcIntentHelperBridge::GetActivityIcons(
...@@ -283,16 +298,6 @@ bool ArcIntentHelperBridge::HasObserver( ...@@ -283,16 +298,6 @@ bool ArcIntentHelperBridge::HasObserver(
return observer_list_.HasObserver(observer); return observer_list_.HasObserver(observer);
} }
void ArcIntentHelperBridge::OnCameraIntentHandled(
uint32_t intent_id,
bool is_success,
const std::vector<uint8_t>& captured_data) {
CHECK(launch_camera_app_callback_map_.find(intent_id) !=
launch_camera_app_callback_map_.end());
std::move(launch_camera_app_callback_map_[intent_id])
.Run(is_success, captured_data);
}
// static // static
bool ArcIntentHelperBridge::IsIntentHelperPackage( bool ArcIntentHelperBridge::IsIntentHelperPackage(
const std::string& package_name) { const std::string& package_name) {
......
...@@ -61,10 +61,6 @@ class ArcIntentHelperBridge : public KeyedService, ...@@ -61,10 +61,6 @@ class ArcIntentHelperBridge : public KeyedService,
void RemoveObserver(ArcIntentHelperObserver* observer); void RemoveObserver(ArcIntentHelperObserver* observer);
bool HasObserver(ArcIntentHelperObserver* observer) const; bool HasObserver(ArcIntentHelperObserver* observer) const;
void OnCameraIntentHandled(uint32_t intent_id,
bool is_success,
const std::vector<uint8_t>& captured_data);
// mojom::IntentHelperHost // mojom::IntentHelperHost
void OnIconInvalidated(const std::string& package_name) override; void OnIconInvalidated(const std::string& package_name) override;
void OnIntentFiltersUpdated( void OnIntentFiltersUpdated(
...@@ -83,11 +79,16 @@ class ArcIntentHelperBridge : public KeyedService, ...@@ -83,11 +79,16 @@ class ArcIntentHelperBridge : public KeyedService,
void FactoryResetArc() override; void FactoryResetArc() override;
void OnOpenWebApp(const std::string& url) override; void OnOpenWebApp(const std::string& url) override;
void RecordShareFilesMetrics(mojom::ShareFiles flag) override; void RecordShareFilesMetrics(mojom::ShareFiles flag) override;
void LaunchCameraApp(arc::mojom::CameraIntentMode mode, void LaunchCameraApp(uint32_t intent_id,
arc::mojom::CameraIntentMode mode,
bool should_handle_result, bool should_handle_result,
bool should_down_scale, bool should_down_scale,
bool is_secure, bool is_secure) override;
LaunchCameraAppCallback callback) override; void HandleCameraResult(
uint32_t intent_id,
arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& data,
arc::mojom::IntentHelperInstance::HandleCameraResultCallback callback);
// Retrieves icons for the |activities| and calls |callback|. // Retrieves icons for the |activities| and calls |callback|.
// See ActivityIconLoader::GetActivityIcons() for more details. // See ActivityIconLoader::GetActivityIcons() for more details.
...@@ -132,11 +133,6 @@ class ArcIntentHelperBridge : public KeyedService, ...@@ -132,11 +133,6 @@ class ArcIntentHelperBridge : public KeyedService,
base::ObserverList<ArcIntentHelperObserver>::Unchecked observer_list_; base::ObserverList<ArcIntentHelperObserver>::Unchecked observer_list_;
base::flat_map<uint32_t, LaunchCameraAppCallback>
launch_camera_app_callback_map_;
uint32_t camera_intent_id_;
// Schemes that ARC is known to send via OnOpenUrl. // Schemes that ARC is known to send via OnOpenUrl.
const std::set<std::string> allowed_arc_schemes_; const std::set<std::string> allowed_arc_schemes_;
......
...@@ -67,6 +67,7 @@ if (is_chromeos) { ...@@ -67,6 +67,7 @@ if (is_chromeos) {
] ]
public_deps = [ public_deps = [
":camera_intent",
":media", ":media",
":notifications", ":notifications",
"//media/capture/video/chromeos/mojom:cros_camera", "//media/capture/video/chromeos/mojom:cros_camera",
...@@ -80,6 +81,12 @@ if (is_chromeos) { ...@@ -80,6 +81,12 @@ if (is_chromeos) {
] ]
} }
mojom("camera_intent") {
sources = [
"camera_intent.mojom",
]
}
mojom("notifications") { mojom("notifications") {
sources = [ sources = [
"bitmap.mojom", "bitmap.mojom",
......
// 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.
module arc.mojom;
enum CameraIntentMode {
PHOTO,
VIDEO,
};
// The status of camera intent handling process. Details are as following:
//
// FINISH: Camera intent is finished and the intent helper should save the
// data and notify the caller that the intent is handled
// successfully.
// CANCEL: Camera intent is aborted and the intent helper should withdraw all
// the data it saved and notify the caller that the intent is canceled.
// APPEND_DATA: Camera intent is sending data and not finished yet. The intent
// helper should save the data accordingly.
// CLEAR_DATA: Camera intent discards all the data it already saved and might
// send new data to save later.
enum CameraIntentAction {
FINISH,
CANCEL,
APPEND_DATA,
CLEAR_DATA,
};
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
module arc.mojom; module arc.mojom;
import "components/arc/mojom/bitmap.mojom"; import "components/arc/mojom/bitmap.mojom";
import "components/arc/mojom/camera_intent.mojom";
import "components/arc/mojom/intent_common.mojom"; import "components/arc/mojom/intent_common.mojom";
import "components/arc/mojom/scale_factor.mojom"; import "components/arc/mojom/scale_factor.mojom";
...@@ -171,11 +172,6 @@ interface CustomTabSession { ...@@ -171,11 +172,6 @@ interface CustomTabSession {
[MinVersion=26] OnOpenInChromeClicked@0(); [MinVersion=26] OnOpenInChromeClicked@0();
}; };
enum CameraIntentMode {
PHOTO,
VIDEO,
};
// Handles intents from ARC in Chrome. // Handles intents from ARC in Chrome.
// Deprecated method ID: 4 // Deprecated method ID: 4
// Next method ID: 13 // Next method ID: 13
...@@ -231,23 +227,23 @@ interface IntentHelperHost { ...@@ -231,23 +227,23 @@ interface IntentHelperHost {
[MinVersion=27] FactoryResetArc@11(); [MinVersion=27] FactoryResetArc@11();
// Launches camera app from the camera intent. // Launches camera app from the camera intent.
// |intent_id| is an unique id that used to recognize different intents.
// |mode| indicates which mode should camera app land on. If // |mode| indicates which mode should camera app land on. If
// |should_handle_result| is true, the intent expects the captured result // |should_handle_result| is true, the intent expects the captured result
// will be returned after capturing. If |should_down_scale| is true, the // will be returned after capturing. If |should_down_scale| is true, the
// intent expects the captured image would be down-scaled to a small enough // intent expects the captured image would be down-scaled to a small enough
// size. If |is_secure| is true, the intent is fired when the device is // size. If |is_secure| is true, the intent is fired when the device is
// secured, which means the camera app should not show any user-sensitive // secured, which means the camera app should not show any user-sensitive
// data. |is_success| indicates that the capture is done successfully. If it // data.
// succeed, the result should be filled in |captured_data| as a byte array. [MinVersion=28] LaunchCameraApp@12(uint32 intent_id,
[MinVersion=28] LaunchCameraApp@12(CameraIntentMode mode, CameraIntentMode mode,
bool should_handle_result, bool should_handle_result,
bool should_down_scale, bool should_down_scale,
bool is_secure) bool is_secure);
=> (bool is_success, array<uint8> captured_data);
}; };
// Sends intents to ARC on behalf of Chrome. // Sends intents to ARC on behalf of Chrome.
// Next method ID: 16 // Next method ID: 17
interface IntentHelperInstance { interface IntentHelperInstance {
// Sets the given package as a preferred package. The next time an ACTION_VIEW // Sets the given package as a preferred package. The next time an ACTION_VIEW
// intent is sent with a URL that requires disambiguation, instead of opening // intent is sent with a URL that requires disambiguation, instead of opening
...@@ -320,4 +316,14 @@ interface IntentHelperInstance { ...@@ -320,4 +316,14 @@ interface IntentHelperInstance {
[MinVersion=22] RequestTextSelectionActions@15(string text, [MinVersion=22] RequestTextSelectionActions@15(string text,
ScaleFactor scale_factor) ScaleFactor scale_factor)
=> (array<TextSelectionAction> actions); => (array<TextSelectionAction> actions);
// Sends the captured result |data| for corresponding intent recognized by
// |intent_id| back to ARC. The handler should handle |data| and maybe notify
// the intent caller according to the intention of the |action|. |is_success|
// will be set to true if the ARC received the result and set to false for
// invalid input.
[MinVersion=28] HandleCameraResult@16(uint32 intent_id,
CameraIntentAction action,
array<uint8> data)
=> (bool is_success);
}; };
...@@ -133,6 +133,12 @@ void FakeIntentHelperInstance::RequestTextSelectionActions( ...@@ -133,6 +133,12 @@ void FakeIntentHelperInstance::RequestTextSelectionActions(
::arc::mojom::ScaleFactor scale_factor, ::arc::mojom::ScaleFactor scale_factor,
RequestTextSelectionActionsCallback callback) {} RequestTextSelectionActionsCallback callback) {}
void FakeIntentHelperInstance::HandleCameraResult(
uint32_t intent_id,
arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& data,
HandleCameraResultCallback callback) {}
std::vector<FakeIntentHelperInstance::Broadcast> std::vector<FakeIntentHelperInstance::Broadcast>
FakeIntentHelperInstance::GetBroadcastsForAction( FakeIntentHelperInstance::GetBroadcastsForAction(
const std::string& action) const { const std::string& action) const {
......
...@@ -37,8 +37,7 @@ class FakeIntentHelperInstance : public mojom::IntentHelperInstance { ...@@ -37,8 +37,7 @@ class FakeIntentHelperInstance : public mojom::IntentHelperInstance {
// Parameters passed to HandleIntent(). // Parameters passed to HandleIntent().
struct HandledIntent { struct HandledIntent {
HandledIntent(mojom::IntentInfoPtr intent, HandledIntent(mojom::IntentInfoPtr intent, mojom::ActivityNamePtr activity);
mojom::ActivityNamePtr activity);
HandledIntent(HandledIntent&& other); HandledIntent(HandledIntent&& other);
HandledIntent& operator=(HandledIntent&& other); HandledIntent& operator=(HandledIntent&& other);
~HandledIntent(); ~HandledIntent();
...@@ -60,9 +59,8 @@ class FakeIntentHelperInstance : public mojom::IntentHelperInstance { ...@@ -60,9 +59,8 @@ class FakeIntentHelperInstance : public mojom::IntentHelperInstance {
// Sets a list of intent handlers to be returned in response to // Sets a list of intent handlers to be returned in response to
// RequestIntentHandlerList() calls with intents containing |action|. // RequestIntentHandlerList() calls with intents containing |action|.
void SetIntentHandlers( void SetIntentHandlers(const std::string& action,
const std::string& action, std::vector<mojom::IntentHandlerInfoPtr> handlers);
std::vector<mojom::IntentHandlerInfoPtr> handlers);
// mojom::IntentHelperInstance: // mojom::IntentHelperInstance:
~FakeIntentHelperInstance() override; ~FakeIntentHelperInstance() override;
...@@ -121,6 +119,11 @@ class FakeIntentHelperInstance : public mojom::IntentHelperInstance { ...@@ -121,6 +119,11 @@ class FakeIntentHelperInstance : public mojom::IntentHelperInstance {
::arc::mojom::ScaleFactor scale_factor, ::arc::mojom::ScaleFactor scale_factor,
RequestTextSelectionActionsCallback callback) override; RequestTextSelectionActionsCallback callback) override;
void HandleCameraResult(uint32_t intent_id,
arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& data,
HandleCameraResultCallback callback) override;
private: private:
std::vector<Broadcast> broadcasts_; std::vector<Broadcast> broadcasts_;
......
...@@ -8,16 +8,18 @@ ...@@ -8,16 +8,18 @@
namespace chromeos_camera { namespace chromeos_camera {
CameraAppHelperImpl::CameraAppHelperImpl(IntentCallback intent_callback) CameraAppHelperImpl::CameraAppHelperImpl(
: intent_callback_(std::move(intent_callback)) {} CameraResultCallback camera_result_callback)
: camera_result_callback_(std::move(camera_result_callback)) {}
CameraAppHelperImpl::~CameraAppHelperImpl() = default; CameraAppHelperImpl::~CameraAppHelperImpl() = default;
void CameraAppHelperImpl::OnIntentHandled( void CameraAppHelperImpl::HandleCameraResult(
uint32_t intent_id, uint32_t intent_id,
bool is_success, arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& captured_data) { const std::vector<uint8_t>& data,
intent_callback_.Run(intent_id, is_success, captured_data); HandleCameraResultCallback callback) {
camera_result_callback_.Run(intent_id, action, data, std::move(callback));
} }
void CameraAppHelperImpl::IsTabletMode(IsTabletModeCallback callback) { void CameraAppHelperImpl::IsTabletMode(IsTabletModeCallback callback) {
......
...@@ -13,20 +13,24 @@ namespace chromeos_camera { ...@@ -13,20 +13,24 @@ namespace chromeos_camera {
class CameraAppHelperImpl : public cros::mojom::CameraAppHelper { class CameraAppHelperImpl : public cros::mojom::CameraAppHelper {
public: public:
using IntentCallback = base::RepeatingCallback< using CameraResultCallback =
void(uint32_t, bool, const std::vector<uint8_t>&)>; base::RepeatingCallback<void(uint32_t,
arc::mojom::CameraIntentAction,
const std::vector<uint8_t>&,
HandleCameraResultCallback)>;
explicit CameraAppHelperImpl(IntentCallback intent_callback); explicit CameraAppHelperImpl(CameraResultCallback camera_result_callback);
~CameraAppHelperImpl() override; ~CameraAppHelperImpl() override;
// cros::mojom::CameraAppHelper implementations. // cros::mojom::CameraAppHelper implementations.
void OnIntentHandled(uint32_t intent_id, void HandleCameraResult(uint32_t intent_id,
bool is_success, arc::mojom::CameraIntentAction action,
const std::vector<uint8_t>& captured_data) override; const std::vector<uint8_t>& data,
HandleCameraResultCallback callback) override;
void IsTabletMode(IsTabletModeCallback callback) override; void IsTabletMode(IsTabletModeCallback callback) override;
private: private:
IntentCallback intent_callback_; CameraResultCallback camera_result_callback_;
DISALLOW_COPY_AND_ASSIGN(CameraAppHelperImpl); DISALLOW_COPY_AND_ASSIGN(CameraAppHelperImpl);
}; };
......
...@@ -15,6 +15,7 @@ mojom("cros_camera") { ...@@ -15,6 +15,7 @@ mojom("cros_camera") {
] ]
deps = [ deps = [
"//components/arc/mojom:camera_intent",
"//components/chromeos_camera/common", "//components/chromeos_camera/common",
"//media/capture/mojom:image_capture", "//media/capture/mojom:image_capture",
"//media/mojo/mojom", "//media/mojo/mojom",
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
module cros.mojom; module cros.mojom;
import "components/arc/mojom/camera_intent.mojom";
import "media/capture/mojom/image_capture.mojom"; import "media/capture/mojom/image_capture.mojom";
import "media/capture/video/chromeos/mojom/camera_common.mojom"; import "media/capture/video/chromeos/mojom/camera_common.mojom";
import "media/capture/video/chromeos/mojom/camera_metadata.mojom"; import "media/capture/video/chromeos/mojom/camera_metadata.mojom";
...@@ -58,17 +59,14 @@ interface CameraAppDeviceProvider { ...@@ -58,17 +59,14 @@ interface CameraAppDeviceProvider {
// Interface for communication between Chrome Camera App (Remote) and Chrome // Interface for communication between Chrome Camera App (Remote) and Chrome
// (Receiver). // (Receiver).
interface CameraAppHelper { interface CameraAppHelper {
// Invoked when the Android intent from ARC++ is fulfilled or is failed. // Sends the captured result |data| for corresponding intent recognized by
// For the intent which expects to have result, it is fulfilled when the // |intent_id| back to ARC. The handler should handle |data| and may notify
// captured is done and is failed if the session ends without finishing the // the intent caller according to the intention of the |action|. |is_success|
// capture. For the intent which don't expect any result, it is fulfilled when // will be set to true if the ARC received the result and set to false for
// the camera app is successfully launched and is failed when the camera fails // invalid input.
// to launch. |intent_id| should be the same id that was specified in the HandleCameraResult(uint32 intent_id, arc.mojom.CameraIntentAction action,
// query when launching the camera app. |is_success| indicates the result array<uint8> data)
// status of the intent. The |captured_data| will be delivered to the handler => (bool is_success);
// as a byte array.
OnIntentHandled(uint32 intent_id, bool is_success,
array<uint8> captured_data);
// Checks if device is under tablet mode currently. // Checks if device is under tablet mode currently.
IsTabletMode() => (bool is_tablet_mode); IsTabletMode() => (bool is_tablet_mode);
......
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