Add Calls::Panel and handle incoming calls.

This commit is contained in:
John Preston 2017-04-19 23:25:48 +03:00
parent 0a716036c2
commit 4925af69e2
31 changed files with 1101 additions and 156 deletions

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

View File

@ -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 <openssl/bn.h>
#include <openssl/sha.h>
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<const unsigned char*>(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<gsl::byte> getBytes() const {
if (failed()) {
return std::vector<gsl::byte>();
}
auto length = BN_num_bytes(raw());
auto result = std::vector<gsl::byte>(length, gsl::byte());
auto resultSize = BN_bn2bin(raw(), reinterpret_cast<unsigned char*>(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<gsl::byte, SHA256_DIGEST_LENGTH> Sha256(base::const_byte_span bytes) {
auto result = std::array<gsl::byte, SHA256_DIGEST_LENGTH>();
SHA256(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size(), reinterpret_cast<unsigned char*>(result.data()));
return result;
}
inline std::array<gsl::byte, SHA_DIGEST_LENGTH> Sha1(base::const_byte_span bytes) {
auto result = std::array<gsl::byte, SHA_DIGEST_LENGTH>();
SHA1(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size(), reinterpret_cast<unsigned char*>(result.data()));
return result;
}
} // namespace openssl

View File

@ -24,7 +24,7 @@ namespace base {
class enable_weak_from_this;
template <typename T, typename = std::enable_if_t<std::is_base_of<enable_weak_from_this, T>::value>>
template <typename T>
class weak_unique_ptr;
class enable_weak_from_this {
@ -42,12 +42,12 @@ public:
}
private:
template <typename Child, typename>
template <typename Child>
friend class weak_unique_ptr;
std::shared_ptr<enable_weak_from_this*> getGuarded() {
if (!_guarded) {
_guarded = std::make_shared<enable_weak_from_this*>(this);
_guarded = std::make_shared<enable_weak_from_this*>(static_cast<enable_weak_from_this*>(this));
}
return _guarded;
}
@ -56,7 +56,7 @@ private:
};
template <typename T, typename>
template <typename T>
class weak_unique_ptr {
public:
weak_unique_ptr() = default;

View File

@ -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;

View File

@ -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 <openssl/bn.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
@ -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<tgvoip::Endpoint> &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<gsl::byte> ComputeModExp(const DhConfig &config, const openssl::BigNum &base, const std::array<gsl::byte, Call::kRandomPowerSize> &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<gsl::byte> ComputeModExpFirst(const DhConfig &config, const std::array<gsl::byte, Call::kRandomPowerSize> &randomPower) {
return ComputeModExp(config, openssl::BigNum(config.g), randomPower);
}
std::vector<gsl::byte> ComputeModExpFinal(const DhConfig &config, base::const_byte_span first, const std::array<gsl::byte, Call::kRandomPowerSize> &randomPower) {
return ComputeModExp(config, openssl::BigNum(first), randomPower);
}
constexpr auto kFingerprintDataSize = 256;
uint64 ComputeFingerprint(const std::array<gsl::byte, kFingerprintDataSize> &authKey) {
auto hash = openssl::Sha1(authKey);
return (gsl::to_integer<uint64>(hash[19]) << 56)
| (gsl::to_integer<uint64>(hash[18]) << 48)
| (gsl::to_integer<uint64>(hash[17]) << 40)
| (gsl::to_integer<uint64>(hash[16]) << 32)
| (gsl::to_integer<uint64>(hash[15]) << 24)
| (gsl::to_integer<uint64>(hash[14]) << 16)
| (gsl::to_integer<uint64>(hash[13]) << 8)
| (gsl::to_integer<uint64>(hash[12]));
}
} // namespace
Call::Call(gsl::not_null<Delegate*> delegate, gsl::not_null<UserData*> user)
Call::Call(gsl::not_null<Delegate*> delegate, gsl::not_null<UserData*> 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<const unsigned char*>(_salt.data()), _salt.size(), &saltBN);
BIGNUM pbytesBN;
BN_init(&pbytesBN);
BN_bin2bn(reinterpret_cast<const unsigned char*>(_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<gsl::byte>(g_a_length, gsl::byte());
BN_bn2bin(&tmp, reinterpret_cast<unsigned char*>(_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<gsl::byte>(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<int32>();
auto g_a_hash = std::array<gsl::byte, SHA256_DIGEST_LENGTH>();
SHA256(reinterpret_cast<const unsigned char*>(_g_a.data()), _g_a.size(), reinterpret_cast<unsigned char*>(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<const unsigned char*>(_dhConfig.p.data()), _dhConfig.p.size(), &p);
BN_bin2bn(reinterpret_cast<const unsigned char*>(call.vg_b.v.constData()), call.vg_b.v.length(), &i_authKey);
BN_bin2bn(reinterpret_cast<const unsigned char*>(_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<unsigned char*>(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<const unsigned char*>(_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<tgvoip::VoIPController>();
if (_mute) {
_controller->SetMicMute(_mute);
}
_controller->implData = static_cast<void*>(this);
_controller->SetRemoteEndpoints(endpoints, true);
_controller->SetConfig(&config);
_controller->SetEncryptionKey(reinterpret_cast<char*>(_authKey.data()), true);
_controller->SetEncryptionKey(reinterpret_cast<char*>(_authKey.data()), (_type == Type::Outgoing));
_controller->SetStateCallback([](tgvoip::VoIPController *controller, int state) {
static_cast<Call*>(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<void*>(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 <typename Type>
bool Call::checkCallCommonFields(const Type &call) {
template <typename T>
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<int>((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

View File

@ -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*> call, const MTPPhoneCallDiscardReason &reason) = 0;
virtual void callFinished(gsl::not_null<Call*> call) = 0;
virtual void callFailed(gsl::not_null<Call*> call) = 0;
};
static constexpr auto kSaltSize = 256;
static constexpr auto kRandomPowerSize = 256;
Call(gsl::not_null<Delegate*> instance, gsl::not_null<UserData*> user);
enum class Type {
Incoming,
Outgoing,
};
Call(gsl::not_null<Delegate*> delegate, gsl::not_null<UserData*> user, Type type);
void startOutgoing(base::const_byte_span random);
Type type() const {
return _type;
}
gsl::not_null<UserData*> 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<State> &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 <typename Type>
bool checkCallCommonFields(const Type &call);
template <typename T>
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*> _delegate;
gsl::not_null<UserData*> _user;
std::vector<gsl::byte> _g_a;
std::array<gsl::byte, kSaltSize> _salt;
Type _type = Type::Outgoing;
State _state = State::WaitingInit;
bool _finishAfterRequestingCall = false;
base::Observable<State> _stateChanged;
TimeMs _startTime = 0;
base::DelayedCallTimer _hangupByTimeoutTimer;
bool _mute = false;
DhConfig _dhConfig;
std::vector<gsl::byte> _ga;
std::vector<gsl::byte> _gb;
std::array<gsl::byte, kSha256Size> _gaHash;
std::array<gsl::byte, kRandomPowerSize> _randomPower;
std::array<gsl::byte, kAuthKeySize> _authKey;
MTPPhoneCallProtocol _protocol;
uint64 _id = 0;
uint64 _accessHash = 0;
uint64 _keyFingerprint = 0;

View File

@ -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<UserData*> user) {
if (_currentCall) {
return; // Already in a call.
}
createCall(user, Call::Type::Outgoing);
}
_currentCall = std::make_unique<Call>(getCallDelegate(), user);
request(MTPmessages_GetDhConfig(MTP_int(_dhConfig.version), MTP_int(Call::kSaltSize))).done([this, call = base::weak_unique_ptr<Call>(_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*> call) {
if (_currentCall.get() == call) {
_currentCallPanel.reset();
_currentCall.reset();
}
}
void Instance::callFailed(gsl::not_null<Call*> call) {
if (_currentCall.get() == call) {
_currentCallPanel.reset();
_currentCall.reset();
}
}
void Instance::createCall(gsl::not_null<UserData*> user, Call::Type type) {
_currentCall = std::make_unique<Call>(getCallDelegate(), user, type);
_currentCallPanel = std::make_unique<Panel>(_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<Call>(_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<UserData*> 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<Call>(_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*> call, const MTPPhoneCallDiscardReason &reason) {
if (_currentCall.get() == call) {
_currentCall.reset();
}
}
void Instance::callFailed(gsl::not_null<Call*> 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()));

View File

@ -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*> call, const MTPPhoneCallDiscardReason &reason) override;
void callFinished(gsl::not_null<Call*> call) override;
void callFailed(gsl::not_null<Call*> call) override;
void createCall(gsl::not_null<UserData*> user, Call::Type type);
void refreshDhConfig();
void handleCallUpdate(const MTPPhoneCall &call);
DhConfig _dhConfig;
std::unique_ptr<Call> _currentCall;
std::unique_ptr<Panel> _currentCallPanel;
};

View File

@ -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<bool>(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(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

View File

@ -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*> 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> _call;
gsl::not_null<UserData*> _user;
bool _useTransparency = true;
style::margins _padding;
int _contentTop = 0;
bool _dragging = false;
QPoint _dragStartMousePosition;
QPoint _dragStartMyPosition;
class Button;
object_ptr<Ui::IconButton> _close = { nullptr };
object_ptr<Button> _answer = { nullptr };
object_ptr<Button> _hangup;
object_ptr<Ui::IconButton> _mute;
object_ptr<Ui::FlatLabel> _name;
object_ptr<Ui::FlatLabel> _status;
std::vector<EmojiPtr> _fingerprint;
QPixmap _userPhoto;
PhotoId _userPhotoId = 0;
bool _userPhotoFull = false;
QPixmap _bottomCache;
QPixmap _cache;
};
} // namespace Calls

View File

@ -210,6 +210,10 @@ inline void copy_bytes(byte_span destination, const_byte_span source) {
memcpy(destination.data(), source.data(), source.size());
}
inline void set_bytes(byte_span destination, gsl::byte value) {
memset(destination.data(), gsl::to_integer<unsigned char>(value), destination.size());
}
} // namespace base
// using for_const instead of plain range-based for loop to ensure usage of const_iterator

View File

@ -777,3 +777,12 @@ Messenger::~Messenger() {
MainWindow *Messenger::mainWindow() {
return _window.get();
}
QPoint Messenger::getPointForCallPanelCenter() const {
Expects(_window != nullptr);
Expects(_window->windowHandle() != nullptr);
if (_window->isActive()) {
return _window->geometry().center();
}
return _window->windowHandle()->screen()->geometry().center();
}

View File

@ -50,6 +50,7 @@ public:
~Messenger();
MainWindow *mainWindow();
QPoint getPointForCallPanelCenter() const;
static Messenger *InstancePointer();
static Messenger &Instance() {

View File

@ -377,11 +377,12 @@ public:
return _openLink;
}
ImagePtr currentUserpic() const;
protected:
void updateNameDelayed(const QString &newName, const QString &newNameOrPhone, const QString &newUsername);
ImagePtr _userpic;
ImagePtr currentUserpic() const;
mutable EmptyUserpic _userpicEmpty;
private:

View File

@ -352,6 +352,11 @@ MultiSelect {
fieldCancelSkip: pixels;
}
CallButton {
button: IconButton;
bg: color;
}
Menu {
skip: pixels;

View File

@ -37,6 +37,7 @@
'<(res_loc)/colors.palette',
'<(res_loc)/basic.style',
'<(src_loc)/boxes/boxes.style',
'<(src_loc)/calls/calls.style',
'<(src_loc)/dialogs/dialogs.style',
'<(src_loc)/history/history.style',
'<(src_loc)/intro/intro.style',

View File

@ -3,6 +3,7 @@
<(src_loc)/base/observer.cpp
<(src_loc)/base/observer.h
<(src_loc)/base/ordered_set.h
<(src_loc)/base/openssl_help.h
<(src_loc)/base/parse_helper.cpp
<(src_loc)/base/parse_helper.h
<(src_loc)/base/qthelp_regex.h
@ -83,6 +84,8 @@
<(src_loc)/calls/calls_call.h
<(src_loc)/calls/calls_instance.cpp
<(src_loc)/calls/calls_instance.h
<(src_loc)/calls/calls_panel.cpp
<(src_loc)/calls/calls_panel.h
<(src_loc)/chat_helpers/bot_keyboard.cpp
<(src_loc)/chat_helpers/bot_keyboard.h
<(src_loc)/chat_helpers/emoji_list_widget.cpp