// Copyright 2013 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 "remoting/client/jni/chromoting_jni_instance.h" #include <android/log.h> #include "base/bind.h" #include "base/logging.h" #include "net/socket/client_socket_factory.h" #include "remoting/client/audio_player.h" #include "remoting/client/jni/android_keymap.h" #include "remoting/client/jni/chromoting_jni_runtime.h" #include "remoting/jingle_glue/chromium_port_allocator.h" #include "remoting/jingle_glue/chromium_socket_factory.h" #include "remoting/jingle_glue/network_settings.h" #include "remoting/protocol/host_stub.h" #include "remoting/protocol/libjingle_transport_factory.h" namespace remoting { namespace { // TODO(solb) Move into location shared with client plugin. const char* const kXmppServer = "talk.google.com"; const int kXmppPort = 5222; const bool kXmppUseTls = true; // Interval at which to log performance statistics, if enabled. const int kPerfStatsIntervalMs = 10000; } ChromotingJniInstance::ChromotingJniInstance(ChromotingJniRuntime* jni_runtime, const char* username, const char* auth_token, const char* host_jid, const char* host_id, const char* host_pubkey, const char* pairing_id, const char* pairing_secret) : jni_runtime_(jni_runtime), host_id_(host_id), create_pairing_(false), stats_logging_enabled_(false) { DCHECK(jni_runtime_->ui_task_runner()->BelongsToCurrentThread()); // Intialize XMPP config. xmpp_config_.host = kXmppServer; xmpp_config_.port = kXmppPort; xmpp_config_.use_tls = kXmppUseTls; xmpp_config_.username = username; xmpp_config_.auth_token = auth_token; xmpp_config_.auth_service = "oauth2"; // Initialize ClientConfig. client_config_.host_jid = host_jid; client_config_.host_public_key = host_pubkey; client_config_.fetch_secret_callback = base::Bind(&ChromotingJniInstance::FetchSecret, this); client_config_.authentication_tag = host_id_; client_config_.client_pairing_id = pairing_id; client_config_.client_paired_secret = pairing_secret; client_config_.authentication_methods.push_back( protocol::AuthenticationMethod::FromString("spake2_pair")); client_config_.authentication_methods.push_back( protocol::AuthenticationMethod::FromString("spake2_hmac")); client_config_.authentication_methods.push_back( protocol::AuthenticationMethod::FromString("spake2_plain")); // Post a task to start connection jni_runtime_->display_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::ConnectToHostOnDisplayThread, this)); } ChromotingJniInstance::~ChromotingJniInstance() {} void ChromotingJniInstance::Cleanup() { if (!jni_runtime_->display_task_runner()->BelongsToCurrentThread()) { jni_runtime_->display_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::Cleanup, this)); return; } // This must be destroyed on the display thread before the producer is gone. view_.reset(); // The weak pointers must be invalidated on the same thread they were used. view_weak_factory_->InvalidateWeakPtrs(); jni_runtime_->network_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::DisconnectFromHostOnNetworkThread, this)); } void ChromotingJniInstance::ProvideSecret(const std::string& pin, bool create_pairing) { DCHECK(jni_runtime_->ui_task_runner()->BelongsToCurrentThread()); DCHECK(!pin_callback_.is_null()); create_pairing_ = create_pairing; jni_runtime_->network_task_runner()->PostTask(FROM_HERE, base::Bind(pin_callback_, pin)); } void ChromotingJniInstance::RedrawDesktop() { if (!jni_runtime_->display_task_runner()->BelongsToCurrentThread()) { jni_runtime_->display_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::RedrawDesktop, this)); return; } jni_runtime_->RedrawCanvas(); } void ChromotingJniInstance::PerformMouseAction( int x, int y, protocol::MouseEvent_MouseButton button, bool button_down) { if (!jni_runtime_->network_task_runner()->BelongsToCurrentThread()) { jni_runtime_->network_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::PerformMouseAction, this, x, y, button, button_down)); return; } protocol::MouseEvent action; action.set_x(x); action.set_y(y); action.set_button(button); if (button != protocol::MouseEvent::BUTTON_UNDEFINED) action.set_button_down(button_down); connection_->input_stub()->InjectMouseEvent(action); } void ChromotingJniInstance::PerformMouseWheelDeltaAction(int delta_x, int delta_y) { if (!jni_runtime_->network_task_runner()->BelongsToCurrentThread()) { jni_runtime_->network_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::PerformMouseWheelDeltaAction, this, delta_x, delta_y)); return; } protocol::MouseEvent action; action.set_wheel_delta_x(delta_x); action.set_wheel_delta_y(delta_y); connection_->input_stub()->InjectMouseEvent(action); } void ChromotingJniInstance::PerformKeyboardAction(int key_code, bool key_down) { if (!jni_runtime_->network_task_runner()->BelongsToCurrentThread()) { jni_runtime_->network_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::PerformKeyboardAction, this, key_code, key_down)); return; } uint32 usb_code = AndroidKeycodeToUsbKeycode(key_code); if (usb_code) { protocol::KeyEvent action; action.set_usb_keycode(usb_code); action.set_pressed(key_down); connection_->input_stub()->InjectKeyEvent(action); } else { LOG(WARNING) << "Ignoring unknown keycode: " << key_code; } } void ChromotingJniInstance::RecordPaintTime(int64 paint_time_ms) { if (!jni_runtime_->network_task_runner()->BelongsToCurrentThread()) { jni_runtime_->network_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::RecordPaintTime, this, paint_time_ms)); return; } if (stats_logging_enabled_) client_->GetStats()->video_paint_ms()->Record(paint_time_ms); } void ChromotingJniInstance::OnConnectionState( protocol::ConnectionToHost::State state, protocol::ErrorCode error) { DCHECK(jni_runtime_->network_task_runner()->BelongsToCurrentThread()); EnableStatsLogging(state == protocol::ConnectionToHost::CONNECTED); if (create_pairing_ && state == protocol::ConnectionToHost::CONNECTED) { protocol::PairingRequest request; request.set_client_name("Android"); connection_->host_stub()->RequestPairing(request); } jni_runtime_->ui_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniRuntime::ReportConnectionStatus, base::Unretained(jni_runtime_), state, error)); } void ChromotingJniInstance::OnConnectionReady(bool ready) { // We ignore this message, since OnConnectionState tells us the same thing. } void ChromotingJniInstance::SetCapabilities(const std::string& capabilities) { NOTIMPLEMENTED(); } void ChromotingJniInstance::SetPairingResponse( const protocol::PairingResponse& response) { jni_runtime_->ui_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniRuntime::CommitPairingCredentials, base::Unretained(jni_runtime_), host_id_, response.client_id(), response.shared_secret())); } void ChromotingJniInstance::DeliverHostMessage( const protocol::ExtensionMessage& message) { NOTIMPLEMENTED(); } protocol::ClipboardStub* ChromotingJniInstance::GetClipboardStub() { return this; } protocol::CursorShapeStub* ChromotingJniInstance::GetCursorShapeStub() { return this; } scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher> ChromotingJniInstance::GetTokenFetcher(const std::string& host_public_key) { // Return null to indicate that third-party authentication is unsupported. return scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>(); } void ChromotingJniInstance::InjectClipboardEvent( const protocol::ClipboardEvent& event) { NOTIMPLEMENTED(); } void ChromotingJniInstance::SetCursorShape( const protocol::CursorShapeInfo& shape) { if (!jni_runtime_->display_task_runner()->BelongsToCurrentThread()) { jni_runtime_->display_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::SetCursorShape, this, shape)); return; } jni_runtime_->UpdateCursorShape(shape); } void ChromotingJniInstance::ConnectToHostOnDisplayThread() { DCHECK(jni_runtime_->display_task_runner()->BelongsToCurrentThread()); view_.reset(new JniFrameConsumer(jni_runtime_, this)); view_weak_factory_.reset(new base::WeakPtrFactory<JniFrameConsumer>( view_.get())); frame_consumer_ = new FrameConsumerProxy(jni_runtime_->display_task_runner(), view_weak_factory_->GetWeakPtr()); jni_runtime_->network_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::ConnectToHostOnNetworkThread, this)); } void ChromotingJniInstance::ConnectToHostOnNetworkThread() { DCHECK(jni_runtime_->network_task_runner()->BelongsToCurrentThread()); client_context_.reset(new ClientContext( jni_runtime_->network_task_runner().get())); client_context_->Start(); connection_.reset(new protocol::ConnectionToHost(true)); client_.reset(new ChromotingClient( client_config_, client_context_.get(), connection_.get(), this, frame_consumer_, scoped_ptr<AudioPlayer>())); view_->set_frame_producer(client_->GetFrameProducer()); signaling_.reset(new XmppSignalStrategy( net::ClientSocketFactory::GetDefaultFactory(), jni_runtime_->url_requester(), xmpp_config_)); NetworkSettings network_settings(NetworkSettings::NAT_TRAVERSAL_ENABLED); // Use Chrome's network stack to allocate ports for peer-to-peer channels. scoped_ptr<ChromiumPortAllocator> port_allocator( ChromiumPortAllocator::Create(jni_runtime_->url_requester(), network_settings)); scoped_ptr<protocol::TransportFactory> transport_factory( new protocol::LibjingleTransportFactory( signaling_.get(), port_allocator.PassAs<cricket::HttpPortAllocatorBase>(), network_settings)); client_->Start(signaling_.get(), transport_factory.Pass()); } void ChromotingJniInstance::DisconnectFromHostOnNetworkThread() { DCHECK(jni_runtime_->network_task_runner()->BelongsToCurrentThread()); host_id_.clear(); stats_logging_enabled_ = false; // |client_| must be torn down before |signaling_|. connection_.reset(); client_.reset(); } void ChromotingJniInstance::FetchSecret( bool pairable, const protocol::SecretFetchedCallback& callback) { if (!jni_runtime_->ui_task_runner()->BelongsToCurrentThread()) { jni_runtime_->ui_task_runner()->PostTask( FROM_HERE, base::Bind(&ChromotingJniInstance::FetchSecret, this, pairable, callback)); return; } if (!client_config_.client_pairing_id.empty()) { // We attempted to connect using an existing pairing that was rejected. // Unless we forget about the stale credentials, we'll continue trying them. jni_runtime_->CommitPairingCredentials(host_id_, "", ""); } pin_callback_ = callback; jni_runtime_->DisplayAuthenticationPrompt(pairable); } void ChromotingJniInstance::EnableStatsLogging(bool enabled) { DCHECK(jni_runtime_->network_task_runner()->BelongsToCurrentThread()); if (enabled && !stats_logging_enabled_) { jni_runtime_->network_task_runner()->PostDelayedTask( FROM_HERE, base::Bind(&ChromotingJniInstance::LogPerfStats, this), base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs)); } stats_logging_enabled_ = enabled; } void ChromotingJniInstance::LogPerfStats() { DCHECK(jni_runtime_->network_task_runner()->BelongsToCurrentThread()); if (!stats_logging_enabled_) return; ChromotingStats* stats = client_->GetStats(); __android_log_print(ANDROID_LOG_INFO, "stats", "Bandwidth:%.0f FrameRate:%.1f Capture:%.1f Encode:%.1f " "Decode:%.1f Render:%.1f Latency:%.0f", stats->video_bandwidth()->Rate(), stats->video_frame_rate()->Rate(), stats->video_capture_ms()->Average(), stats->video_encode_ms()->Average(), stats->video_decode_ms()->Average(), stats->video_paint_ms()->Average(), stats->round_trip_ms()->Average()); jni_runtime_->network_task_runner()->PostDelayedTask( FROM_HERE, base::Bind(&ChromotingJniInstance::LogPerfStats, this), base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs)); } } // namespace remoting