diff --git a/Telegram/Resources/colors.palette b/Telegram/Resources/colors.palette index 787617cba..370c2fbb2 100644 --- a/Telegram/Resources/colors.palette +++ b/Telegram/Resources/colors.palette @@ -507,3 +507,15 @@ mediaviewTransparentFg: #cccccc; // another transparent filling part // notification notificationBg: windowBg; // custom notification window background + +// calls +callBg: #26282cf2; +callNameFg: #ffffff; +callFingerprintBg: #00000066; +callStatusFg: #aaabac; +callIconFg: #ffffff; +callAnswerBg: #64c15b; +callAnswerRipple: #52b149; +callHangupBg: #d75a5a; +callHangupRipple: #c04646; +callMuteRipple: #ffffff12; diff --git a/Telegram/Resources/icons/call_answer.png b/Telegram/Resources/icons/call_answer.png new file mode 100644 index 000000000..f5a9ceb73 Binary files /dev/null and b/Telegram/Resources/icons/call_answer.png differ diff --git a/Telegram/Resources/icons/call_answer@2x.png b/Telegram/Resources/icons/call_answer@2x.png new file mode 100644 index 000000000..d6eb7c010 Binary files /dev/null and b/Telegram/Resources/icons/call_answer@2x.png differ diff --git a/Telegram/Resources/icons/call_discard.png b/Telegram/Resources/icons/call_discard.png new file mode 100644 index 000000000..312a8be71 Binary files /dev/null and b/Telegram/Resources/icons/call_discard.png differ diff --git a/Telegram/Resources/icons/call_discard@2x.png b/Telegram/Resources/icons/call_discard@2x.png new file mode 100644 index 000000000..8fffeb412 Binary files /dev/null and b/Telegram/Resources/icons/call_discard@2x.png differ diff --git a/Telegram/Resources/icons/call_record_active.png b/Telegram/Resources/icons/call_record_active.png new file mode 100644 index 000000000..4659bbb6c Binary files /dev/null and b/Telegram/Resources/icons/call_record_active.png differ diff --git a/Telegram/Resources/icons/call_record_active@2x.png b/Telegram/Resources/icons/call_record_active@2x.png new file mode 100644 index 000000000..8ce748f9c Binary files /dev/null and b/Telegram/Resources/icons/call_record_active@2x.png differ diff --git a/Telegram/Resources/icons/call_record_muted.png b/Telegram/Resources/icons/call_record_muted.png new file mode 100644 index 000000000..0747c8afa Binary files /dev/null and b/Telegram/Resources/icons/call_record_muted.png differ diff --git a/Telegram/Resources/icons/call_record_muted@2x.png b/Telegram/Resources/icons/call_record_muted@2x.png new file mode 100644 index 000000000..4d7d690e9 Binary files /dev/null and b/Telegram/Resources/icons/call_record_muted@2x.png differ diff --git a/Telegram/Resources/icons/call_shadow_left.png b/Telegram/Resources/icons/call_shadow_left.png new file mode 100644 index 000000000..74864ad4b Binary files /dev/null and b/Telegram/Resources/icons/call_shadow_left.png differ diff --git a/Telegram/Resources/icons/call_shadow_left@2x.png b/Telegram/Resources/icons/call_shadow_left@2x.png new file mode 100644 index 000000000..6a0e6e4c5 Binary files /dev/null and b/Telegram/Resources/icons/call_shadow_left@2x.png differ diff --git a/Telegram/Resources/icons/call_shadow_top.png b/Telegram/Resources/icons/call_shadow_top.png new file mode 100644 index 000000000..653e0afa9 Binary files /dev/null and b/Telegram/Resources/icons/call_shadow_top.png differ diff --git a/Telegram/Resources/icons/call_shadow_top@2x.png b/Telegram/Resources/icons/call_shadow_top@2x.png new file mode 100644 index 000000000..47c672ca3 Binary files /dev/null and b/Telegram/Resources/icons/call_shadow_top@2x.png differ diff --git a/Telegram/Resources/icons/call_shadow_top_left.png b/Telegram/Resources/icons/call_shadow_top_left.png new file mode 100644 index 000000000..baba49364 Binary files /dev/null and b/Telegram/Resources/icons/call_shadow_top_left.png differ diff --git a/Telegram/Resources/icons/call_shadow_top_left@2x.png b/Telegram/Resources/icons/call_shadow_top_left@2x.png new file mode 100644 index 000000000..0d9672fb5 Binary files /dev/null and b/Telegram/Resources/icons/call_shadow_top_left@2x.png differ diff --git a/Telegram/SourceFiles/base/openssl_help.h b/Telegram/SourceFiles/base/openssl_help.h new file mode 100644 index 000000000..90639e241 --- /dev/null +++ b/Telegram/SourceFiles/base/openssl_help.h @@ -0,0 +1,146 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include +#include + +namespace openssl { + +class Context { +public: + Context() : _data(BN_CTX_new()) { + } + Context(const Context &other) = delete; + Context(Context &&other) : _data(base::take(other._data)) { + } + Context &operator=(const Context &other) = delete; + Context &operator=(Context &&other) { + _data = base::take(other._data); + return *this; + } + ~Context() { + if (_data) { + BN_CTX_free(_data); + } + } + + BN_CTX *raw() const { + return _data; + } + +private: + BN_CTX *_data = nullptr; + +}; + +class BigNum { +public: + BigNum() { + BN_init(raw()); + } + BigNum(const BigNum &other) : BigNum() { + *this = other; + } + BigNum &operator=(const BigNum &other) { + if (other.failed() || !BN_copy(raw(), other.raw())) { + _failed = true; + } + return *this; + } + ~BigNum() { + BN_clear_free(raw()); + } + + explicit BigNum(unsigned int word) : BigNum() { + setWord(word); + } + explicit BigNum(base::const_byte_span bytes) : BigNum() { + setBytes(bytes); + } + + void setWord(unsigned int word) { + if (!BN_set_word(raw(), word)) { + _failed = true; + } + } + void setBytes(base::const_byte_span bytes) { + if (!BN_bin2bn(reinterpret_cast(bytes.data()), bytes.size(), raw())) { + _failed = true; + } + } + void setModExp(const BigNum &a, const BigNum &p, const BigNum &m, const Context &context = Context()) { + if (a.failed() || p.failed() || m.failed()) { + _failed = true; + } else if (a.isNegative() || p.isNegative() || m.isNegative()) { + _failed = true; + } else if (!BN_mod_exp(raw(), a.raw(), p.raw(), m.raw(), context.raw())) { + _failed = true; + } else if (isNegative()) { + _failed = true; + } + } + + bool isNegative() const { + return BN_is_negative(raw()); + } + + std::vector getBytes() const { + if (failed()) { + return std::vector(); + } + auto length = BN_num_bytes(raw()); + auto result = std::vector(length, gsl::byte()); + auto resultSize = BN_bn2bin(raw(), reinterpret_cast(result.data())); + t_assert(resultSize == length); + return result; + } + + BIGNUM *raw() { + return &_data; + } + const BIGNUM *raw() const { + return &_data; + } + + bool failed() const { + return _failed; + } + +private: + BIGNUM _data; + bool _failed = false; + +}; + +inline std::array Sha256(base::const_byte_span bytes) { + auto result = std::array(); + SHA256(reinterpret_cast(bytes.data()), bytes.size(), reinterpret_cast(result.data())); + return result; +} + +inline std::array Sha1(base::const_byte_span bytes) { + auto result = std::array(); + SHA1(reinterpret_cast(bytes.data()), bytes.size(), reinterpret_cast(result.data())); + return result; +} + +} // namespace openssl diff --git a/Telegram/SourceFiles/base/weak_unique_ptr.h b/Telegram/SourceFiles/base/weak_unique_ptr.h index ce0b7b8ce..b4dbac92d 100644 --- a/Telegram/SourceFiles/base/weak_unique_ptr.h +++ b/Telegram/SourceFiles/base/weak_unique_ptr.h @@ -24,7 +24,7 @@ namespace base { class enable_weak_from_this; -template ::value>> +template class weak_unique_ptr; class enable_weak_from_this { @@ -42,12 +42,12 @@ public: } private: - template + template friend class weak_unique_ptr; std::shared_ptr getGuarded() { if (!_guarded) { - _guarded = std::make_shared(this); + _guarded = std::make_shared(static_cast(this)); } return _guarded; } @@ -56,7 +56,7 @@ private: }; -template +template class weak_unique_ptr { public: weak_unique_ptr() = default; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style new file mode 100644 index 000000000..46f389802 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls.style @@ -0,0 +1,80 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +using "basic.style"; + +using "ui/widgets/widgets.style"; + +callWidth: 300px; +callHeight: 470px; +callShadow: Shadow { + left: icon {{ "call_shadow_left", windowShadowFg }}; + topLeft: icon {{ "call_shadow_top_left", windowShadowFg }}; + top: icon {{ "call_shadow_top", windowShadowFg }}; + topRight: icon {{ "call_shadow_top_left-flip_horizontal", windowShadowFg }}; + right: icon {{ "call_shadow_left-flip_horizontal", windowShadowFg }}; + bottomRight: icon {{ "call_shadow_top_left-flip_vertical-flip_horizontal", windowShadowFg }}; + bottom: icon {{ "call_shadow_top-flip_vertical", windowShadowFg }}; + bottomLeft: icon {{ "call_shadow_top_left-flip_vertical", windowShadowFg }}; + extend: margins(9px, 8px, 9px, 10px); + fallback: windowShadowFgFallback; +} + +callButton: IconButton { + width: 64px; + height: 64px; + + iconPosition: point(20px, 20px); + + rippleAreaPosition: point(8px, 8px); + rippleAreaSize: 48px; + ripple: defaultRippleAnimation; +} + +callAnswer: CallButton { + button: IconButton(callButton) { + icon: icon {{ "call_answer", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callAnswerRipple; + } + } + bg: callAnswerBg; +} +callHangup: CallButton { + button: IconButton(callButton) { + icon: icon {{ "call_discard", callIconFg }}; + iconPosition: point(20px, 24px); + ripple: RippleAnimation(defaultRippleAnimation) { + color: callHangupRipple; + } + } + bg: callHangupBg; +} +callMuteToggle: IconButton(callButton) { + icon: icon {{ "call_record_active", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callMuteRipple; + } +} +callUnmuteIcon: icon {{ "call_record_muted", callIconFg }}; + +callControlsTop: 84px; +callControlsSkip: 8px; +callMuteRight: 12px; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 745ddfbc0..42a5e719f 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -21,9 +21,10 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "calls/calls_call.h" #include "auth_session.h" +#include "mainwidget.h" #include "calls/calls_instance.h" +#include "base/openssl_help.h" -#include #include #include @@ -45,86 +46,195 @@ namespace { constexpr auto kMinLayer = 65; constexpr auto kMaxLayer = 65; // MTP::CurrentLayer? +constexpr auto kHangupTimeoutMs = 5000; // TODO read from server config using tgvoip::Endpoint; void ConvertEndpoint(std::vector &ep, const MTPDphoneConnection &mtc) { - if (mtc.vpeer_tag.v.length() != 16) + if (mtc.vpeer_tag.v.length() != 16) { return; + } auto ipv4 = tgvoip::IPv4Address(std::string(mtc.vip.v.constData(), mtc.vip.v.size())); auto ipv6 = tgvoip::IPv6Address(std::string(mtc.vipv6.v.constData(), mtc.vipv6.v.size())); ep.push_back(Endpoint((int64_t)mtc.vid.v, (uint16_t)mtc.vport.v, ipv4, ipv6, EP_TYPE_UDP_RELAY, (unsigned char*)mtc.vpeer_tag.v.data())); } +std::vector ComputeModExp(const DhConfig &config, const openssl::BigNum &base, const std::array &randomPower) { + using namespace openssl; + + BigNum resultBN; + resultBN.setModExp(base, BigNum(randomPower), BigNum(config.p)); + auto result = resultBN.getBytes(); + constexpr auto kMaxModExpSize = 256; + t_assert(result.size() <= kMaxModExpSize); + return result; +} + +std::vector ComputeModExpFirst(const DhConfig &config, const std::array &randomPower) { + return ComputeModExp(config, openssl::BigNum(config.g), randomPower); +} + +std::vector ComputeModExpFinal(const DhConfig &config, base::const_byte_span first, const std::array &randomPower) { + return ComputeModExp(config, openssl::BigNum(first), randomPower); +} + +constexpr auto kFingerprintDataSize = 256; +uint64 ComputeFingerprint(const std::array &authKey) { + auto hash = openssl::Sha1(authKey); + return (gsl::to_integer(hash[19]) << 56) + | (gsl::to_integer(hash[18]) << 48) + | (gsl::to_integer(hash[17]) << 40) + | (gsl::to_integer(hash[16]) << 32) + | (gsl::to_integer(hash[15]) << 24) + | (gsl::to_integer(hash[14]) << 16) + | (gsl::to_integer(hash[13]) << 8) + | (gsl::to_integer(hash[12])); +} + } // namespace -Call::Call(gsl::not_null delegate, gsl::not_null user) +Call::Call(gsl::not_null delegate, gsl::not_null user, Type type) : _delegate(delegate) -, _user(user) { +, _user(user) +, _type(type) { + if (_type == Type::Outgoing) { + setState(State::Requesting); + } +} + +void Call::generateRandomPower(base::const_byte_span random) { + Expects(random.size() == _randomPower.size()); + memset_rand(_randomPower.data(), _randomPower.size()); + for (auto i = 0, count = int(_randomPower.size()); i != count; i++) { + _randomPower[i] ^= random[i]; + } +} + +void Call::start(base::const_byte_span random) { // Save config here, because it is possible that it changes between // different usages inside the same call. _dhConfig = _delegate->getDhConfig(); -} + t_assert(_dhConfig.g != 0); + t_assert(!_dhConfig.p.empty()); -void Call::generateSalt(base::const_byte_span random) { - Expects(random.size() == _salt.size()); - memset_rand(_salt.data(), _salt.size()); - for (auto i = 0, count = int(_salt.size()); i != count; i++) { - _salt[i] ^= random[i]; + generateRandomPower(random); + + if (_type == Type::Outgoing) { + startOutgoing(); + } else { + startIncoming(); } } -void Call::startOutgoing(base::const_byte_span random) { - generateSalt(random); - - BN_CTX* ctx = BN_CTX_new(); - BN_CTX_init(ctx); - BIGNUM i_g_a; - BN_init(&i_g_a); - BN_set_word(&i_g_a, _dhConfig.g); - BIGNUM tmp; - BN_init(&tmp); - BIGNUM saltBN; - BN_init(&saltBN); - BN_bin2bn(reinterpret_cast(_salt.data()), _salt.size(), &saltBN); - BIGNUM pbytesBN; - BN_init(&pbytesBN); - BN_bin2bn(reinterpret_cast(_dhConfig.p.data()), _dhConfig.p.size(), &pbytesBN); - BN_mod_exp(&tmp, &i_g_a, &saltBN, &pbytesBN, ctx); - auto g_a_length = BN_num_bytes(&tmp); - _g_a = std::vector(g_a_length, gsl::byte()); - BN_bn2bin(&tmp, reinterpret_cast(_g_a.data())); - constexpr auto kMaxGASize = 256; - if (_g_a.size() > kMaxGASize) { - auto slice = gsl::make_span(_g_a).subspan(1, kMaxGASize); - _g_a = std::vector(slice.begin(), slice.end()); +void Call::startOutgoing() { + _ga = ComputeModExpFirst(_dhConfig, _randomPower); + if (_ga.empty()) { + LOG(("Call Error: Could not compute mod-exp first.")); + setState(State::Failed); + return; } - BN_CTX_free(ctx); - + _gaHash = openssl::Sha256(_ga); auto randomID = rand_value(); - auto g_a_hash = std::array(); - SHA256(reinterpret_cast(_g_a.data()), _g_a.size(), reinterpret_cast(g_a_hash.data())); - request(MTPphone_RequestCall(_user->inputUser, MTP_int(randomID), MTP_bytes(g_a_hash), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { + setState(State::Requesting); + request(MTPphone_RequestCall(_user->inputUser, MTP_int(randomID), MTP_bytes(_gaHash), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { Expects(result.type() == mtpc_phone_phoneCall); auto &call = result.c_phone_phoneCall(); App::feedUsers(call.vusers); if (call.vphone_call.type() != mtpc_phoneCallWaiting) { - LOG(("API Error: Expected phoneCallWaiting in response to phone.requestCall()")); - failed(); + LOG(("Call Error: Expected phoneCallWaiting in response to phone.requestCall()")); + setState(State::Failed); return; } + + setState(State::Waiting); + if (_finishAfterRequestingCall) { + hangup(); + return; + } + auto &phoneCall = call.vphone_call.c_phoneCallWaiting(); _id = phoneCall.vid.v; _accessHash = phoneCall.vaccess_hash.v; + handleUpdate(call.vphone_call); }).fail([this](const RPCError &error) { - failed(); + setState(State::Failed); }).send(); } +void Call::startIncoming() { + setState(State::Ringing); +} + +void Call::answer() { + Expects(_type == Type::Incoming); + _gb = ComputeModExpFirst(_dhConfig, _randomPower); + if (_gb.empty()) { + LOG(("Call Error: Could not compute mod-exp first.")); + setState(State::Failed); + return; + } + + setState(State::ExchangingKeys); + request(MTPphone_AcceptCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_bytes(_gb), _protocol)).done([this](const MTPphone_PhoneCall &result) { + Expects(result.type() == mtpc_phone_phoneCall); + auto &call = result.c_phone_phoneCall(); + App::feedUsers(call.vusers); + if (call.vphone_call.type() != mtpc_phoneCallWaiting) { + LOG(("Call Error: Expected phoneCallWaiting in response to phone.acceptCall()")); + setState(State::Failed); + return; + } + + handleUpdate(call.vphone_call); + }).fail([this](const RPCError &error) { + setState(State::Failed); + }).send(); +} + +void Call::setMute(bool mute) { + _mute = mute; + if (_controller) { + _controller->SetMicMute(_mute); + } +} + +void Call::hangup() { + auto missed = (_state == State::Ringing || (_state == State::Waiting && _type == Type::Outgoing)); + auto reason = missed ? MTP_phoneCallDiscardReasonMissed() : MTP_phoneCallDiscardReasonHangup(); + finish(reason); +} + +void Call::decline() { + finish(MTP_phoneCallDiscardReasonBusy()); +} + bool Call::handleUpdate(const MTPPhoneCall &call) { switch (call.type()) { - case mtpc_phoneCallRequested: Unexpected("phoneCallRequested call inside an existing call handleUpdate()"); + case mtpc_phoneCallRequested: { + auto &data = call.c_phoneCallRequested(); + if (_type != Type::Incoming + || _id != 0 + || peerToUser(_user->id) != data.vadmin_id.v) { + Unexpected("phoneCallRequested call inside an existing call handleUpdate()"); + } + if (AuthSession::CurrentUserId() != data.vparticipant_id.v) { + LOG(("Call Error: Wrong call participant_id %1, expected %2.").arg(data.vparticipant_id.v).arg(AuthSession::CurrentUserId())); + setState(State::Failed); + return true; + + } + _id = data.vid.v; + _accessHash = data.vaccess_hash.v; + _protocol = data.vprotocol; + auto gaHashBytes = bytesFromMTP(data.vg_a_hash); + if (gaHashBytes.size() != _gaHash.size()) { + LOG(("Call Error: Wrong g_a_hash size %1, expected %2.").arg(gaHashBytes.size()).arg(_gaHash.size())); + setState(State::Failed); + return true; + } + base::copy_bytes(gsl::make_span(_gaHash), gaHashBytes); + } return true; case mtpc_phoneCallEmpty: { auto &data = call.c_phoneCallEmpty(); @@ -132,7 +242,7 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { return false; } LOG(("Call Error: phoneCallEmpty received.")); - failed(); + setState(State::Failed); } return true; case mtpc_phoneCallWaiting: { @@ -147,6 +257,9 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { if (data.vid.v != _id) { return false; } + if (_type == Type::Incoming && _state == State::ExchangingKeys) { + startConfirmedCall(data); + } } return true; case mtpc_phoneCallDiscarded: { @@ -154,7 +267,17 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { if (data.vid.v != _id) { return false; } - _delegate->callFinished(this, data.vreason); + if (data.is_need_debug()) { + auto debugLog = _controller ? _controller->GetDebugLog() : std::string(); + if (!debugLog.empty()) { + MTP::send(MTPphone_SaveCallDebug(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_dataJSON(MTP_string(debugLog)))); + } + } + if (data.has_reason() && data.vreason.type() == mtpc_phoneCallDiscardReasonBusy) { + setState(State::Busy); + } else { + setState(State::Ended); + } } return true; case mtpc_phoneCallAccepted: { @@ -162,7 +285,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { if (data.vid.v != _id) { return false; } - if (checkCallFields(data)) { + if (_type != Type::Outgoing) { + LOG(("Call Error: Unexpected phoneCallAccepted for an incoming call.")); + setState(State::Failed); + } else if (checkCallFields(data)) { confirmAcceptedCall(data); } } return true; @@ -172,69 +298,83 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { } void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) { + Expects(_type == Type::Outgoing); + // TODO check isGoodGaAndGb - - BN_CTX *ctx = BN_CTX_new(); - BN_CTX_init(ctx); - BIGNUM p; - BIGNUM i_authKey; - BIGNUM res; - BIGNUM salt; - BN_init(&p); - BN_init(&i_authKey); - BN_init(&res); - BN_init(&salt); - BN_bin2bn(reinterpret_cast(_dhConfig.p.data()), _dhConfig.p.size(), &p); - BN_bin2bn(reinterpret_cast(call.vg_b.v.constData()), call.vg_b.v.length(), &i_authKey); - BN_bin2bn(reinterpret_cast(_salt.data()), _salt.size(), &salt); - - BN_mod_exp(&res, &i_authKey, &salt, &p, ctx); - BN_CTX_free(ctx); - auto realAuthKeyLength = BN_num_bytes(&res); - auto realAuthKeyBytes = QByteArray(realAuthKeyLength, Qt::Uninitialized); - BN_bn2bin(&res, reinterpret_cast(realAuthKeyBytes.data())); - - if (realAuthKeyLength > kAuthKeySize) { - memcpy(_authKey.data(), realAuthKeyBytes.constData() + (realAuthKeyLength - kAuthKeySize), kAuthKeySize); - } else if (realAuthKeyLength < kAuthKeySize) { - memset(_authKey.data(), 0, kAuthKeySize - realAuthKeyLength); - memcpy(_authKey.data() + (kAuthKeySize - realAuthKeyLength), realAuthKeyBytes.constData(), realAuthKeyLength); - } else { - memcpy(_authKey.data(), realAuthKeyBytes.constData(), kAuthKeySize); + auto computedAuthKey = ComputeModExpFinal(_dhConfig, byteVectorFromMTP(call.vg_b), _randomPower); + if (computedAuthKey.empty()) { + LOG(("Call Error: Could not compute mod-exp final.")); + setState(State::Failed); + return; } - unsigned char authKeyHash[SHA_DIGEST_LENGTH]; - SHA1(reinterpret_cast(_authKey.data()), _authKey.size(), authKeyHash); + auto computedAuthKeySize = computedAuthKey.size(); + t_assert(computedAuthKeySize <= kAuthKeySize); + auto authKeyBytes = gsl::make_span(_authKey); + if (computedAuthKeySize < kAuthKeySize) { + base::set_bytes(authKeyBytes.subspan(0, kAuthKeySize - computedAuthKeySize), gsl::byte()); + base::copy_bytes(authKeyBytes.subspan(kAuthKeySize - computedAuthKeySize), computedAuthKey); + } else { + base::copy_bytes(authKeyBytes, computedAuthKey); + } + _keyFingerprint = ComputeFingerprint(_authKey); - _keyFingerprint = ((uint64)authKeyHash[19] << 56) - | ((uint64)authKeyHash[18] << 48) - | ((uint64)authKeyHash[17] << 40) - | ((uint64)authKeyHash[16] << 32) - | ((uint64)authKeyHash[15] << 24) - | ((uint64)authKeyHash[14] << 16) - | ((uint64)authKeyHash[13] << 8) - | ((uint64)authKeyHash[12]); - - request(MTPphone_ConfirmCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_bytes(_g_a), MTP_long(_keyFingerprint), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { + setState(State::ExchangingKeys); + request(MTPphone_ConfirmCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_bytes(_ga), MTP_long(_keyFingerprint), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { Expects(result.type() == mtpc_phone_phoneCall); auto &call = result.c_phone_phoneCall(); App::feedUsers(call.vusers); if (call.vphone_call.type() != mtpc_phoneCall) { - LOG(("API Error: Expected phoneCall in response to phone.confirmCall()")); - failed(); + LOG(("Call Error: Expected phoneCall in response to phone.confirmCall()")); + setState(State::Failed); return; } + createAndStartController(call.vphone_call.c_phoneCall()); }).fail([this](const RPCError &error) { - failed(); + setState(State::Failed); }).send(); } +void Call::startConfirmedCall(const MTPDphoneCall &call) { + Expects(_type == Type::Incoming); + + auto firstBytes = bytesFromMTP(call.vg_a_or_b); + if (_gaHash != openssl::Sha256(firstBytes)) { + LOG(("Call Error: Wrong g_a hash received.")); + setState(State::Failed); + return; + } + + // TODO check isGoodGaAndGb + auto computedAuthKey = ComputeModExpFinal(_dhConfig, firstBytes, _randomPower); + if (computedAuthKey.empty()) { + LOG(("Call Error: Could not compute mod-exp final.")); + setState(State::Failed); + return; + } + + auto computedAuthKeySize = computedAuthKey.size(); + t_assert(computedAuthKeySize <= kAuthKeySize); + auto authKeyBytes = gsl::make_span(_authKey); + if (computedAuthKeySize < kAuthKeySize) { + base::set_bytes(authKeyBytes.subspan(0, kAuthKeySize - computedAuthKeySize), gsl::byte()); + base::copy_bytes(authKeyBytes.subspan(kAuthKeySize - computedAuthKeySize), computedAuthKey); + } else { + base::copy_bytes(authKeyBytes, computedAuthKey); + } + _keyFingerprint = ComputeFingerprint(_authKey); + + createAndStartController(call); +} + void Call::createAndStartController(const MTPDphoneCall &call) { if (!checkCallFields(call)) { return; } + setState(State::Established); + voip_config_t config; config.data_saving = DATA_SAVING_NEVER; config.enableAEC = true; @@ -250,10 +390,13 @@ void Call::createAndStartController(const MTPDphoneCall &call) { } _controller = std::make_unique(); + if (_mute) { + _controller->SetMicMute(_mute); + } _controller->implData = static_cast(this); _controller->SetRemoteEndpoints(endpoints, true); _controller->SetConfig(&config); - _controller->SetEncryptionKey(reinterpret_cast(_authKey.data()), true); + _controller->SetEncryptionKey(reinterpret_cast(_authKey.data()), (_type == Type::Outgoing)); _controller->SetStateCallback([](tgvoip::VoIPController *controller, int state) { static_cast(controller->implData)->handleControllerStateChange(controller, state); }); @@ -263,47 +406,52 @@ void Call::createAndStartController(const MTPDphoneCall &call) { void Call::handleControllerStateChange(tgvoip::VoIPController *controller, int state) { // NB! Can be called from an arbitrary thread! - Expects(controller == _controller.get()); + // Expects(controller == _controller.get()); This can be called from ~VoIPController()! Expects(controller->implData == static_cast(this)); switch (state) { case STATE_WAIT_INIT: { - DEBUG_LOG(("Call Info: State changed to Established.")); + DEBUG_LOG(("Call Info: State changed to WaitingInit.")); + setStateQueued(State::WaitingInit); } break; case STATE_WAIT_INIT_ACK: { - DEBUG_LOG(("Call Info: State changed to Established.")); + DEBUG_LOG(("Call Info: State changed to WaitingInitAck.")); + setStateQueued(State::WaitingInitAck); } break; case STATE_ESTABLISHED: { DEBUG_LOG(("Call Info: State changed to Established.")); + setStateQueued(State::Established); } break; case STATE_FAILED: { DEBUG_LOG(("Call Info: State changed to Failed.")); - failed(); + setStateQueued(State::Failed); } break; default: LOG(("Call Error: Unexpected state in handleStateChange: %1").arg(state)); } } -template -bool Call::checkCallCommonFields(const Type &call) { +template +bool Call::checkCallCommonFields(const T &call) { auto checkFailed = [this] { - failed(); + setState(State::Failed); return false; }; if (call.vaccess_hash.v != _accessHash) { - LOG(("API Error: Wrong call access_hash.")); + LOG(("Call Error: Wrong call access_hash.")); return checkFailed(); } - if (call.vadmin_id.v != AuthSession::CurrentUserId()) { - LOG(("API Error: Wrong call admin_id %1, expected %2.").arg(call.vadmin_id.v).arg(AuthSession::CurrentUserId())); + auto adminId = (_type == Type::Outgoing) ? AuthSession::CurrentUserId() : peerToUser(_user->id); + auto participantId = (_type == Type::Outgoing) ? peerToUser(_user->id) : AuthSession::CurrentUserId(); + if (call.vadmin_id.v != adminId) { + LOG(("Call Error: Wrong call admin_id %1, expected %2.").arg(call.vadmin_id.v).arg(adminId)); return checkFailed(); } - if (call.vparticipant_id.v != peerToUser(_user->id)) { - LOG(("API Error: Wrong call participant_id %1, expected %2.").arg(call.vparticipant_id.v).arg(peerToUser(_user->id))); + if (call.vparticipant_id.v != participantId) { + LOG(("Call Error: Wrong call participant_id %1, expected %2.").arg(call.vparticipant_id.v).arg(participantId)); return checkFailed(); } return true; @@ -314,8 +462,8 @@ bool Call::checkCallFields(const MTPDphoneCall &call) { return false; } if (call.vkey_fingerprint.v != _keyFingerprint) { - LOG(("API Error: Wrong call fingerprint.")); - failed(); + LOG(("Call Error: Wrong call fingerprint.")); + setState(State::Failed); return false; } return true; @@ -325,7 +473,64 @@ bool Call::checkCallFields(const MTPDphoneCallAccepted &call) { return checkCallCommonFields(call); } -void Call::destroyController() { +void Call::setState(State state) { + if (_state != state) { + _state = state; + _stateChanged.notify(state, true); + + switch (_state) { + case State::WaitingInit: + case State::WaitingInitAck: + case State::Established: + _startTime = getms(true); + break; + case State::Ended: + _delegate->callFinished(this); + break; + case State::Failed: + _delegate->callFailed(this); + break; + case State::Busy: + _hangupByTimeoutTimer.call(kHangupTimeoutMs, [this] { setState(State::Ended); }); + // TODO play sound + break; + } + } +} + +void Call::finish(const MTPPhoneCallDiscardReason &reason) { + if (_state == State::Requesting) { + _hangupByTimeoutTimer.call(kHangupTimeoutMs, [this] { setState(State::Ended); }); + _finishAfterRequestingCall = true; + return; + } + if (_state == State::HangingUp || _state == State::Ended) { + return; + } + if (!_id) { + setState(State::Ended); + return; + } + + setState(State::HangingUp); + auto duration = _startTime ? static_cast((getms(true) - _startTime) / 1000) : 0; + auto connectionId = _controller ? _controller->GetPreferredRelayID() : 0; + _hangupByTimeoutTimer.call(kHangupTimeoutMs, [this] { setState(State::Ended); }); + request(MTPphone_DiscardCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_int(duration), reason, MTP_long(connectionId))).done([this](const MTPUpdates &result) { + // This could be destroyed by updates, so we set Ended after + // updates being handled, but in a guarded way. + InvokeQueued(this, [this] { setState(State::Ended); }); + App::main()->sentUpdatesReceived(result); + }).fail([this](const RPCError &error) { + setState(State::Ended); + }).send(); +} + +void Call::setStateQueued(State state) { + InvokeQueued(this, [this, state] { setState(state); }); +} + +Call::~Call() { if (_controller) { DEBUG_LOG(("Call Info: Destroying call controller..")); _controller.reset(); @@ -333,12 +538,4 @@ void Call::destroyController() { } } -void Call::failed() { - InvokeQueued(this, [this] { _delegate->callFailed(this); }); -} - -Call::~Call() { - destroyController(); -} - } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 350e255d2..bb6e274b7 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -22,6 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "base/weak_unique_ptr.h" #include "mtproto/sender.h" +#include "base/timer.h" namespace tgvoip { class VoIPController; @@ -40,43 +41,97 @@ public: class Delegate { public: virtual DhConfig getDhConfig() const = 0; - virtual void callFinished(gsl::not_null call, const MTPPhoneCallDiscardReason &reason) = 0; + virtual void callFinished(gsl::not_null call) = 0; virtual void callFailed(gsl::not_null call) = 0; }; - static constexpr auto kSaltSize = 256; + static constexpr auto kRandomPowerSize = 256; - Call(gsl::not_null instance, gsl::not_null user); + enum class Type { + Incoming, + Outgoing, + }; + Call(gsl::not_null delegate, gsl::not_null user, Type type); - void startOutgoing(base::const_byte_span random); + Type type() const { + return _type; + } + gsl::not_null user() const { + return _user; + } + + void start(base::const_byte_span random); bool handleUpdate(const MTPPhoneCall &call); + enum State { + WaitingInit, + WaitingInitAck, + Established, + Failed, + HangingUp, + Ended, + ExchangingKeys, + Waiting, + Requesting, + WaitingIncoming, + Ringing, + Busy, + }; + State state() const { + return _state; + } + base::Observable &stateChanged() { + return _stateChanged; + } + void setMute(bool mute); + + void answer(); + void hangup(); + void decline(); + ~Call(); private: static constexpr auto kAuthKeySize = 256; + static constexpr auto kSha256Size = 32; - void generateSalt(base::const_byte_span random); + void finish(const MTPPhoneCallDiscardReason &reason); + void startOutgoing(); + void startIncoming(); + + void generateRandomPower(base::const_byte_span random); void handleControllerStateChange(tgvoip::VoIPController *controller, int state); void createAndStartController(const MTPDphoneCall &call); - void destroyController(); - template - bool checkCallCommonFields(const Type &call); + template + bool checkCallCommonFields(const T &call); bool checkCallFields(const MTPDphoneCall &call); bool checkCallFields(const MTPDphoneCallAccepted &call); void confirmAcceptedCall(const MTPDphoneCallAccepted &call); + void startConfirmedCall(const MTPDphoneCall &call); + void setState(State state); + void setStateQueued(State state); - void failed(); - - DhConfig _dhConfig; gsl::not_null _delegate; gsl::not_null _user; - std::vector _g_a; - std::array _salt; + Type _type = Type::Outgoing; + State _state = State::WaitingInit; + bool _finishAfterRequestingCall = false; + base::Observable _stateChanged; + TimeMs _startTime = 0; + base::DelayedCallTimer _hangupByTimeoutTimer; + bool _mute = false; + + DhConfig _dhConfig; + std::vector _ga; + std::vector _gb; + std::array _gaHash; + std::array _randomPower; std::array _authKey; + MTPPhoneCallProtocol _protocol; + uint64 _id = 0; uint64 _accessHash = 0; uint64 _keyFingerprint = 0; diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 6a98c96f3..ce222b154 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -23,6 +23,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "mtproto/connection.h" #include "auth_session.h" #include "calls/calls_call.h" +#include "calls/calls_panel.h" namespace Calls { @@ -32,14 +33,32 @@ void Instance::startOutgoingCall(gsl::not_null user) { if (_currentCall) { return; // Already in a call. } + createCall(user, Call::Type::Outgoing); +} - _currentCall = std::make_unique(getCallDelegate(), user); - request(MTPmessages_GetDhConfig(MTP_int(_dhConfig.version), MTP_int(Call::kSaltSize))).done([this, call = base::weak_unique_ptr(_currentCall)](const MTPmessages_DhConfig &result) { - if (!call) { - DEBUG_LOG(("API Warning: call was destroyed before got dhConfig.")); - return; - } +void Instance::callFinished(gsl::not_null call) { + if (_currentCall.get() == call) { + _currentCallPanel.reset(); + _currentCall.reset(); + } +} +void Instance::callFailed(gsl::not_null call) { + if (_currentCall.get() == call) { + _currentCallPanel.reset(); + _currentCall.reset(); + } +} + +void Instance::createCall(gsl::not_null user, Call::Type type) { + _currentCall = std::make_unique(getCallDelegate(), user, type); + _currentCallPanel = std::make_unique(_currentCall.get()); + refreshDhConfig(); +} + +void Instance::refreshDhConfig() { + Expects(_currentCall != nullptr); + request(MTPmessages_GetDhConfig(MTP_int(_dhConfig.version), MTP_int(Call::kRandomPowerSize))).done([this, call = base::weak_unique_ptr(_currentCall)](const MTPmessages_DhConfig &result) { auto random = base::const_byte_span(); switch (result.type()) { case mtpc_messages_dhConfig: { @@ -67,32 +86,22 @@ void Instance::startOutgoingCall(gsl::not_null user) { default: Unexpected("Type in messages.getDhConfig"); } - if (random.size() != Call::kSaltSize) { + if (random.size() != Call::kRandomPowerSize) { LOG(("API Error: dhConfig random bytes wrong size: %1").arg(random.size())); callFailed(call.get()); return; } - call->startOutgoing(random); + if (call) { + call->start(random); + } }).fail([this, call = base::weak_unique_ptr(_currentCall)](const RPCError &error) { if (!call) { DEBUG_LOG(("API Warning: call was destroyed before got dhConfig.")); return; } - callFailed(call.get()); }).send(); -} -void Instance::callFinished(gsl::not_null call, const MTPPhoneCallDiscardReason &reason) { - if (_currentCall.get() == call) { - _currentCall.reset(); - } -} - -void Instance::callFailed(gsl::not_null call) { - if (_currentCall.get() == call) { - _currentCall.reset(); - } } void Instance::handleUpdate(const MTPDupdatePhoneCall& update) { @@ -101,10 +110,18 @@ void Instance::handleUpdate(const MTPDupdatePhoneCall& update) { void Instance::handleCallUpdate(const MTPPhoneCall &call) { if (call.type() == mtpc_phoneCallRequested) { - if (_currentCall) { - // discard ? + auto &phoneCall = call.c_phoneCallRequested(); + auto user = App::userLoaded(phoneCall.vadmin_id.v); + if (!user) { + LOG(("API Error: User not loaded for phoneCallRequested.")); + } else if (user->isSelf()) { + LOG(("API Error: Self found in phoneCallRequested.")); + } + if (_currentCall || !user || user->isSelf()) { + request(MTPphone_DiscardCall(MTP_inputPhoneCall(phoneCall.vid, phoneCall.vaccess_hash), MTP_int(0), MTP_phoneCallDiscardReasonBusy(), MTP_long(0))).send(); } else { - // show call + createCall(user, Call::Type::Incoming); + _currentCall->handleUpdate(call); } } else if (!_currentCall || !_currentCall->handleUpdate(call)) { DEBUG_LOG(("API Warning: unexpected phone call update %1").arg(call.type())); diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index ed3846ffe..406fbecef 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -25,7 +25,9 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org namespace Calls { -class Instance : private MTP::Sender, private Call::Delegate { +class Panel; + +class Instance : private MTP::Sender, private Call::Delegate, private base::Subscriber { public: Instance(); @@ -42,14 +44,17 @@ private: DhConfig getDhConfig() const override { return _dhConfig; } - void callFinished(gsl::not_null call, const MTPPhoneCallDiscardReason &reason) override; + void callFinished(gsl::not_null call) override; void callFailed(gsl::not_null call) override; + void createCall(gsl::not_null user, Call::Type type); + void refreshDhConfig(); void handleCallUpdate(const MTPPhoneCall &call); DhConfig _dhConfig; std::unique_ptr _currentCall; + std::unique_ptr _currentCallPanel; }; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp new file mode 100644 index 000000000..3d1cf7fa7 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -0,0 +1,320 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "calls/calls_panel.h" + +#include "calls/calls_call.h" +#include "styles/style_calls.h" +#include "styles/style_history.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/shadow.h" +#include "messenger.h" +#include "auth_session.h" +#include "apiwrap.h" +#include "platform/platform_specific.h" + +namespace Calls { + +class Panel::Button : public Ui::RippleButton { +public: + Button(QWidget *parent, const style::CallButton &st); + +protected: + void paintEvent(QPaintEvent *e) override; + + void onStateChanged(State was, StateChangeSource source) override; + + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + const style::CallButton &_st; + QPixmap _bg; + +}; + +Panel::Button::Button(QWidget *parent, const style::CallButton &st) : Ui::RippleButton(parent, st.button.ripple) +, _st(st) { + resize(_st.button.width, _st.button.height); + _bg = App::pixmapFromImageInPlace(style::colorizeImage(prepareRippleMask(), _st.bg)); +} + +void Panel::Button::paintEvent(QPaintEvent *e) { + Painter p(this); + + p.drawPixmap(myrtlpoint(_st.button.rippleAreaPosition), _bg); + + auto ms = getms(); + + paintRipple(p, _st.button.rippleAreaPosition.x(), _st.button.rippleAreaPosition.y(), ms); + + auto down = isDown(); + auto position = _st.button.iconPosition; + _st.button.icon.paint(p, position, width()); +} + +void Panel::Button::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + + auto over = isOver(); + auto wasOver = static_cast(was & StateFlag::Over); + if (over != wasOver) { + update(); + } +} + +QPoint Panel::Button::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()) - _st.button.rippleAreaPosition; +} + +QImage Panel::Button::prepareRippleMask() const { + return Ui::RippleAnimation::ellipseMask(QSize(_st.button.rippleAreaSize, _st.button.rippleAreaSize)); +} + +Panel::Panel(gsl::not_null call) +: _call(call) +, _user(call->user()) +, _hangup(this, st::callHangup) +, _mute(this, st::callMuteToggle) +, _name(this) +, _status(this) { + initControls(); + initLayout(); + show(); +} + +void Panel::initControls() { + subscribe(_call->stateChanged(), [this](Call::State state) { + if (state == Call::State::Failed || state == Call::State::Ended) { + callDestroyed(); + } + }); + _hangup->setClickedCallback([this] { + if (_call) { + _call->hangup(); + } + }); + if (_call->type() == Call::Type::Incoming) { + _answer.create(this, st::callAnswer); + _answer->setClickedCallback([this] { + if (_call) { + _call->answer(); + } + }); + } +} + +void Panel::initLayout() { + hide(); + + setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | /*Qt::WindowStaysOnTopHint | */Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::Tool); + setAttribute(Qt::WA_MacAlwaysShowToolWindow); + setAttribute(Qt::WA_NoSystemBackground, true); + setAttribute(Qt::WA_TranslucentBackground, true); + + initGeometry(); + + processUserPhoto(); + subscribe(AuthSession::Current().api().fullPeerUpdated(), [this](PeerData *peer) { + if (peer == _user) { + processUserPhoto(); + } + }); + subscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] { + refreshUserPhoto(); + }); + createDefaultCacheImage(); +} + +void Panel::processUserPhoto() { + if (!_user->userpicLoaded()) { + _user->loadUserpic(true); + } + auto photo = (_user->photoId && _user->photoId != UnknownPeerPhotoId) ? App::photo(_user->photoId) : nullptr; + if (isGoodUserPhoto(photo)) { + photo->full->load(true); + } else { + if ((_user->photoId == UnknownPeerPhotoId) || (_user->photoId && (!photo || !photo->date))) { + App::api()->requestFullPeer(_user); + } + } + refreshUserPhoto(); +} + +void Panel::refreshUserPhoto() { + auto photo = (_user->photoId && _user->photoId != UnknownPeerPhotoId) ? App::photo(_user->photoId) : nullptr; + if (isGoodUserPhoto(photo) && photo->full->loaded() && (photo->id != _userPhotoId || !_userPhotoFull)) { + _userPhotoId = photo->id; + _userPhotoFull = true; + createUserpicCache(photo->full); + } else if (_userPhoto.isNull()) { + if (auto userpic = _user->currentUserpic()) { + createUserpicCache(userpic); + } + } +} + +void Panel::createUserpicCache(ImagePtr image) { + auto size = st::callWidth * cIntRetinaFactor(); + auto options = _useTransparency ? (Images::Option::RoundedLarge | Images::Option::RoundedTopLeft | Images::Option::RoundedTopRight | Images::Option::Smooth) : 0; + auto width = image->width(); + auto height = image->height(); + if (width > height) { + width = qMax((width * size) / height, 1); + height = size; + } else { + height = qMax((height * size) / width, 1); + width = size; + } + _userPhoto = image->pixNoCache(width, height, options, size, size); + if (cRetina()) _userPhoto.setDevicePixelRatio(cRetinaFactor()); + + refreshCacheImageUserPhoto(); + + update(); +} + +bool Panel::isGoodUserPhoto(PhotoData *photo) { + if (!photo || !photo->date) { + return false; + } + auto badAspect = [](int a, int b) { + return a > 10 * b; + }; + auto width = photo->full->width(); + auto height = photo->full->height(); + return !badAspect(width, height) && !badAspect(height, width); +} + +void Panel::initGeometry() { + auto center = Messenger::Instance().getPointForCallPanelCenter(); + _useTransparency = Platform::TransparentWindowsSupported(center); + _padding = _useTransparency ? st::callShadow.extend : style::margins(); + _contentTop = _padding.top() + st::callWidth; + auto screen = QApplication::desktop()->screenGeometry(center); + auto rect = QRect(0, 0, st::callWidth, st::callHeight); + setGeometry(rect.translated(center - rect.center()).marginsAdded(_padding)); + createBottomImage(); +} + +void Panel::createBottomImage() { + if (!_useTransparency) { + return; + } + auto bottomWidth = width(); + auto bottomHeight = height() - _padding.top() - st::callWidth; + auto image = QImage(QSize(bottomWidth, bottomHeight) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + { + Painter p(&image); + Ui::Shadow::paint(p, QRect(_padding.left(), 0, st::callWidth, bottomHeight - _padding.bottom()), width(), st::callShadow, Ui::Shadow::Side::Left | Ui::Shadow::Side::Right | Ui::Shadow::Side::Bottom); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setBrush(st::callBg); + p.setPen(Qt::NoPen); + PainterHighQualityEnabler hq(p); + p.drawRoundedRect(myrtlrect(_padding.left(), -st::historyMessageRadius, st::callWidth, bottomHeight - _padding.bottom() + st::historyMessageRadius), st::historyMessageRadius, st::historyMessageRadius); + } + _bottomCache = App::pixmapFromImageInPlace(std::move(image)); +} + +void Panel::createDefaultCacheImage() { + if (!_useTransparency || !_cache.isNull()) { + return; + } + auto cache = QImage(size(), QImage::Format_ARGB32_Premultiplied); + cache.fill(Qt::transparent); + { + Painter p(&cache); + auto inner = rect().marginsRemoved(_padding); + Ui::Shadow::paint(p, inner, width(), st::callShadow); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setBrush(st::callBg); + p.setPen(Qt::NoPen); + PainterHighQualityEnabler hq(p); + p.drawRoundedRect(myrtlrect(inner), st::historyMessageRadius, st::historyMessageRadius); + } + _cache = App::pixmapFromImageInPlace(std::move(cache)); +} + +void Panel::refreshCacheImageUserPhoto() { + auto cache = QImage(size(), QImage::Format_ARGB32_Premultiplied); + cache.fill(Qt::transparent); + { + Painter p(&cache); + Ui::Shadow::paint(p, QRect(_padding.left(), _padding.top(), st::callWidth, st::callWidth), width(), st::callShadow, Ui::Shadow::Side::Top | Ui::Shadow::Side::Left | Ui::Shadow::Side::Right); + p.drawPixmapLeft(_padding.left(), _padding.top(), width(), _userPhoto); + p.drawPixmapLeft(0, _padding.top() + st::callWidth, width(), _bottomCache); + } + _cache = App::pixmapFromImageInPlace(std::move(cache)); +} + +void Panel::resizeEvent(QResizeEvent *e) { + auto controlsTop = _contentTop + st::callControlsTop; + if (_answer) { + auto bothWidth = _answer->width() + st::callControlsSkip + _hangup->width(); + _hangup->moveToLeft((width() - bothWidth) / 2, controlsTop); + _answer->moveToRight((width() - bothWidth) / 2, controlsTop); + } else { + _hangup->moveToLeft((width() - _hangup->width()) / 2, controlsTop); + } + _mute->moveToRight(_padding.right() + st::callMuteRight, controlsTop); +} + +void Panel::paintEvent(QPaintEvent *e) { + Painter p(this); + if (_useTransparency) { + p.drawPixmapLeft(0, 0, width(), _cache); + } else { + p.drawPixmapLeft(0, 0, width(), _userPhoto); + p.fillRect(myrtlrect(0, st::callWidth, width(), height() - st::callWidth), st::callBg); + } +} + +void Panel::mousePressEvent(QMouseEvent *e) { + auto dragArea = myrtlrect(_padding.left(), _padding.top(), st::callWidth, st::callWidth); + if (e->button() == Qt::LeftButton && dragArea.contains(e->pos())) { + _dragging = true; + _dragStartMousePosition = e->globalPos(); + _dragStartMyPosition = QPoint(x(), y()); + } +} + +void Panel::mouseMoveEvent(QMouseEvent *e) { + if (_dragging) { + if (!(e->buttons() & Qt::LeftButton)) { + _dragging = false; + } else { + move(_dragStartMyPosition + (e->globalPos() - _dragStartMousePosition)); + } + } +} + +void Panel::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() == Qt::LeftButton) { + _dragging = false; + } +} + +void Panel::callDestroyed() { +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h new file mode 100644 index 000000000..bd1235ed0 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -0,0 +1,89 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include "base/weak_unique_ptr.h" + +namespace Ui { +class IconButton; +class FlatLabel; +} // namespace Ui + +namespace Calls { + +class Call; + +class Panel : public TWidget, private base::Subscriber { +public: + Panel(gsl::not_null call); + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + +private: + void initControls(); + void initLayout(); + void initGeometry(); + void createBottomImage(); + void createDefaultCacheImage(); + void refreshCacheImageUserPhoto(); + + void processUserPhoto(); + void refreshUserPhoto(); + bool isGoodUserPhoto(PhotoData *photo); + void createUserpicCache(ImagePtr image); + + void callDestroyed(); + + base::weak_unique_ptr _call; + gsl::not_null _user; + + bool _useTransparency = true; + style::margins _padding; + int _contentTop = 0; + + bool _dragging = false; + QPoint _dragStartMousePosition; + QPoint _dragStartMyPosition; + + class Button; + object_ptr _close = { nullptr }; + object_ptr