Select Git revision
-
Stepan Salenikovich authored
Return invalid QModelIndex in the case that is a top node but out of range. Issue: #75334 Change-Id: I3f6784b757d504fc8103208fe1ba341c1cfaf5da
Stepan Salenikovich authoredReturn invalid QModelIndex in the case that is a top node but out of range. Issue: #75334 Change-Id: I3f6784b757d504fc8103208fe1ba341c1cfaf5da
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
recentmodel.cpp 19.61 KiB
/************************************************************************************
* Copyright (C) 2015 by Savoir-faire Linux *
* Author : Emmanuel Lepage Vallee <emmanuel.lepage@savoirfairelinux.com> *
* Alexandre Lision <alexandre.lision@savoirfairelinux.com> *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library 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 *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***********************************************************************************/
#include "recentmodel.h"
//std
#include <algorithm>
//Qt
#include <QtCore/QCoreApplication>
//Ring
#include <call.h>
#include <person.h>
#include <personmodel.h>
#include <contactmethod.h>
#include <phonedirectorymodel.h>
#include <callmodel.h>
#include <categorizedhistorymodel.h>
#include <media/recordingmodel.h>
#include <media/textrecording.h>
struct CallGroup
{
QVector<Call*> m_lCalls ;
Call::Direction m_Direction;
bool m_Missed ;
time_t m_LastUsed ;
};
struct RecentViewNode
{
//Types
enum class Type {
PERSON ,
CONTACT_METHOD ,
CALL ,
CALL_GROUP ,
TEXT_MESSAGE ,
TEXT_MESSAGE_GROUP,
};
//Constructor
explicit RecentViewNode();
virtual ~RecentViewNode();
//Attributes
long int m_Index ;
Type m_Type ;
RecentViewNode* m_pParent ;
QList<RecentViewNode*> m_lChildren;
union {
const Person* m_pPerson ;
ContactMethod* m_pContactMethod;
Call* m_pCall ;
CallGroup* m_pCallGroup ;
//ImConversationIterator; //TODO
//ImConversationIterator;
} m_uContent;
//Helpers
inline time_t lastUsed() const;
};
class RecentModelPrivate : public QObject
{
public:
RecentModelPrivate(RecentModel* p);
/*
* m_lTopLevelReverted hold the elements in the reverse order of
* QAbstractItemModel::index. This cause most of the energy to be
* in the bottom half of the vector, preventing std::move every time
* someone is contacted
*/
QList<RecentViewNode*> m_lTopLevelReverted;
QHash<const Person*,RecentViewNode*> m_hPersonsToNodes ;
QHash<ContactMethod*,RecentViewNode*> m_hCMsToNodes ;
QList<Call*> m_lCallBucket ;
//Helper
void insertNode(RecentViewNode* n, time_t t, bool isNew);
void removeNode(RecentViewNode* n );
private:
RecentModel* q_ptr;
public Q_SLOTS:
void slotLastUsedTimeChanged(const Person* p , time_t t );
void slotPersonAdded (const Person* p );
void slotLastUsedChanged (ContactMethod* cm, time_t t );
void slotContactChanged (ContactMethod* cm, Person* np, Person* op);
void slotCallAdded (Call* call , Call* parent );
void slotCallStateChanged (Call* call , Call::State previousState);
void slotUpdate ( );
};
RecentModelPrivate::RecentModelPrivate(RecentModel* p) : q_ptr(p)
{
}
RecentModel::RecentModel(QObject* parent) : QAbstractItemModel(parent), d_ptr(new RecentModelPrivate(this))
{
connect(PersonModel::instance() , &PersonModel::lastUsedTimeChanged , d_ptr, &RecentModelPrivate::slotLastUsedTimeChanged);
connect(PersonModel::instance() , &PersonModel::newPersonAdded , d_ptr, &RecentModelPrivate::slotPersonAdded );
connect(PhoneDirectoryModel::instance(), &PhoneDirectoryModel::lastUsedChanged, d_ptr, &RecentModelPrivate::slotLastUsedChanged );
connect(PhoneDirectoryModel::instance(), &PhoneDirectoryModel::contactChanged , d_ptr, &RecentModelPrivate::slotContactChanged );
connect(CallModel::instance() , &CallModel::callAdded , d_ptr, &RecentModelPrivate::slotCallAdded );
connect(CallModel::instance() , &CallModel::callStateChanged , d_ptr, &RecentModelPrivate::slotCallStateChanged );
//Fill the contacts
for (int i=0; i < PersonModel::instance()->rowCount(); i++) {
auto person = qvariant_cast<Person*>(PersonModel::instance()->data(
PersonModel::instance()->index(i,0),
static_cast<int>(Person::Role::Object)
));
if (person && person->lastUsedTime())
d_ptr->slotLastUsedTimeChanged(person, person->lastUsedTime());
}
//Fill the "orphan" contact methods
for (int i = 0; i < PhoneDirectoryModel::instance()->rowCount(); i++) {
auto cm = qvariant_cast<ContactMethod*>(PhoneDirectoryModel::instance()->data(
PhoneDirectoryModel::instance()->index(i,0),
static_cast<int>(PhoneDirectoryModel::Role::Object)
));
if (cm && cm->lastUsed() && !cm->contact())
d_ptr->slotLastUsedChanged(cm, cm->lastUsed());
}
//Fill node with history data
//const CallMap callMap = CategorizedHistoryModel::instance()->getHistoryCalls();
//Q_FOREACH(auto const &call , callMap) {
// d_ptr->slotCallAdded(call, nullptr);
//}
}
RecentModel::~RecentModel()
{
for (RecentViewNode* n : d_ptr->m_lTopLevelReverted)
delete n;
delete d_ptr;
}
RecentViewNode::RecentViewNode()
{
}
RecentViewNode::~RecentViewNode()
{
for (RecentViewNode* n : m_lChildren) {
delete n;
}
}
time_t RecentViewNode::lastUsed() const
{
switch(m_Type) {
case Type::PERSON :
return m_uContent.m_pPerson->lastUsedTime();
case Type::CONTACT_METHOD :
return m_uContent.m_pContactMethod->lastUsed();
case Type::CALL :
return m_uContent.m_pCall->stopTimeStamp();
case Type::CALL_GROUP :
return m_uContent.m_pCallGroup->m_LastUsed;
case Type::TEXT_MESSAGE :
case Type::TEXT_MESSAGE_GROUP:
//TODO
break;
}
return {};
}
RecentModel* RecentModel::instance()
{
static RecentModel* instance = new RecentModel(QCoreApplication::instance());
return instance;
}
/**
* Check if given index has an ongoing call
* returns true if one of its child is also in the CallModel
*/
bool RecentModel::hasActiveCall(const QModelIndex &idx)
{
if (not idx.isValid())
return false;
auto node = static_cast<RecentViewNode*>(idx.internalPointer());
QListIterator<RecentViewNode*> lIterator(node->m_lChildren);
lIterator.toBack();
while (lIterator.hasPrevious()) {
auto child = lIterator.previous();
if (child->m_Type == RecentViewNode::Type::CALL) {
return CallModel::instance()->getIndex(child->m_uContent.m_pCall).isValid();
}
}
return false;
}
/**
* Return the first found ongoing call of the given index
* Index can be the call itself or the associated parent
*/
Call* RecentModel::getActiveCall(const QModelIndex &idx)
{
if (not idx.isValid())
return nullptr;
RecentViewNode* node = static_cast<RecentViewNode*>(idx.internalPointer());
if (node->m_Type == RecentViewNode::Type::CALL) {
return node->m_uContent.m_pCall;
}
QListIterator<RecentViewNode*> lIterator(node->m_lChildren);
lIterator.toBack();
while (lIterator.hasPrevious()) {
auto child = lIterator.previous();
if (child->m_Type == RecentViewNode::Type::CALL) {
return child->m_uContent.m_pCall;
}
}
return nullptr;
}
QHash<int,QByteArray> RecentModel::roleNames() const
{
static QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
/*static bool initRoles = false;
if (!initRoles) {
initRoles = true;
}*/
return roles;
}
bool RecentModel::setData( const QModelIndex& index, const QVariant &value, int role)
{
Q_UNUSED(index)
Q_UNUSED(value)
Q_UNUSED(role)
return false;
}
QVariant RecentModel::data( const QModelIndex& index, int role ) const
{
if (!index.isValid())
return QVariant();
RecentViewNode* node = static_cast<RecentViewNode*>(index.internalPointer());
switch(node->m_Type) {
case RecentViewNode::Type::PERSON :
return node->m_uContent.m_pPerson->roleData(role);
case RecentViewNode::Type::CONTACT_METHOD :
return node->m_uContent.m_pContactMethod->roleData(role);
case RecentViewNode::Type::CALL :
return node->m_uContent.m_pCall->roleData(role);
case RecentViewNode::Type::CALL_GROUP :
return node->m_uContent.m_pCallGroup->m_lCalls[0]->roleData(role);
case RecentViewNode::Type::TEXT_MESSAGE :
case RecentViewNode::Type::TEXT_MESSAGE_GROUP:
//TODO
break;
}
return QVariant();
}
int RecentModel::rowCount( const QModelIndex& parent ) const
{
if (!parent.isValid())
return d_ptr->m_lTopLevelReverted.size();
RecentViewNode* node = static_cast<RecentViewNode*>(parent.internalPointer());
return node->m_lChildren.size();
}
Qt::ItemFlags RecentModel::flags( const QModelIndex& index ) const
{
return index.isValid() ? Qt::ItemIsEnabled | Qt::ItemIsSelectable : Qt::NoItemFlags;
}
int RecentModel::columnCount( const QModelIndex& parent ) const
{
Q_UNUSED(parent)
return 1;
}
QModelIndex RecentModel::parent( const QModelIndex& index ) const
{
if (!index.isValid())
return QModelIndex();
RecentViewNode* node = static_cast<RecentViewNode*>(index.internalPointer());
if (!node->m_pParent)
return QModelIndex();
return createIndex(node->m_pParent->m_Index, 0, node->m_pParent);
}
QModelIndex RecentModel::index( int row, int column, const QModelIndex& parent) const
{
if (!parent.isValid() && row >= 0 && row < d_ptr->m_lTopLevelReverted.size() && !column)
return createIndex(row, 0, d_ptr->m_lTopLevelReverted[d_ptr->m_lTopLevelReverted.size() - 1 - row]);
if (!parent.isValid())
return QModelIndex();
RecentViewNode* node = static_cast<RecentViewNode*>(parent.internalPointer());
if (row >= 0 && row < node->m_lChildren.size())
return createIndex(row, 0, node->m_lChildren[row]);
return QModelIndex();
}
QVariant RecentModel::headerData( int section, Qt::Orientation orientation, int role) const
{
if (!section && role == Qt::DisplayRole && orientation == Qt::Horizontal)
return tr("Recent persons");
return QVariant();
}
/*
* Move rows around to keep the person/contactmethods ordered
*
* Further optimization:
* * Invert m_Index to avoid the O(N) loop when adding an item
*/
void RecentModelPrivate::insertNode(RecentViewNode* n, time_t t, bool isNew)
{
//Don't bother with the sorted insertion and indexes housekeeping
if (m_lTopLevelReverted.isEmpty()) {
q_ptr->beginInsertRows(QModelIndex(),0,0);
m_lTopLevelReverted << n;
q_ptr->endInsertRows();
return;
}
//Compute the bounds, this is needed to use beginMoveRows
int newPos = 0;
if (m_lTopLevelReverted.last()->lastUsed() > t) {
//NOTE std::lower_bound need the "value" argument to be the same type as the iterator
//this use the m_Index field to hold the time_t
static RecentViewNode fake;
fake.m_Index = static_cast<long int>(t);
//NOTE Using std::lower_bound is officially supported on QList on all platforms
auto lower = std::lower_bound(m_lTopLevelReverted.begin(), m_lTopLevelReverted.end(), &fake,
[t](const RecentViewNode* a, const RecentViewNode* t2) -> bool {
return a->lastUsed() < t2->m_Index;
});
// the value pointed by the iterator returned by this function may also
// be equivalent to val, and not only greater
if (!isNew && n->m_Index == (*lower)->m_Index) {
newPos = (*lower)->m_Index;
} else
newPos = (*lower)->m_Index+1;
}
//Begin the transaction
if (!isNew) {
if (newPos == n->m_Index)
return; //Nothing to do
q_ptr->beginMoveRows(QModelIndex(), n->m_Index, n->m_Index, QModelIndex(), newPos ? newPos+1 : newPos );
m_lTopLevelReverted.removeAt(m_lTopLevelReverted.size()-1-n->m_Index);
}
else
q_ptr->beginInsertRows(QModelIndex(),newPos,newPos);
//Apply the transaction
m_lTopLevelReverted.insert(m_lTopLevelReverted.size() - newPos,n);
for (int i = 0 ; i < m_lTopLevelReverted.size(); ++i) {
m_lTopLevelReverted[i]->m_Index = m_lTopLevelReverted.size() - 1 - i;
}
//Notify that the transaction is complete
if (!isNew)
q_ptr->endMoveRows();
else
q_ptr->endInsertRows();
#if 0
//Uncomment if there is issues
qDebug() << "\n\nList:" << m_lTopLevelReverted.size() << isNew;
for (int i = 0; i<m_lTopLevelReverted.size();i++) {
qDebug() << "|||" << m_lTopLevelReverted[i]->lastUsed() << m_lTopLevelReverted[i]->m_Index << q_ptr->data(q_ptr->index(m_lTopLevelReverted.size()-1-i,0),Qt::DisplayRole);
for (auto child : m_lTopLevelReverted[i]->m_lChildren) {
qDebug() << "|||" << "|||" << child << child->m_uContent.m_pCall->formattedName();
}
}
#endif
}
void RecentModelPrivate::removeNode(RecentViewNode* n)
{
const int idx = n->m_Index;
q_ptr->beginRemoveRows(QModelIndex(), idx, idx);
m_lTopLevelReverted.removeOne(n);
if (idx < m_lTopLevelReverted.size()) {
for (int i = 0; i <= idx; i++) {
m_lTopLevelReverted[i]->m_Index--;
}
}
q_ptr->endRemoveRows();
}
void RecentModelPrivate::slotPersonAdded(const Person* p)
{
if (p)
slotLastUsedTimeChanged(p, p->lastUsedTime());
}
void RecentModelPrivate::slotLastUsedTimeChanged(const Person* p, time_t t)
{
RecentViewNode* n = m_hPersonsToNodes[p];
const bool isNew = !n;
if (isNew) {
n = new RecentViewNode();
n->m_Type = RecentViewNode::Type::PERSON;
n->m_uContent.m_pPerson = p ;
n->m_pParent = nullptr ;
n->m_Index = 0 ;
m_hPersonsToNodes[p] = n ;
Q_FOREACH(auto cm, p->phoneNumbers()) {
if (auto cmNode = m_hCMsToNodes[cm])
n->m_lChildren.append(cmNode->m_lChildren);
}
}
insertNode(n, t, isNew);
slotUpdate();
}
void RecentModelPrivate::slotLastUsedChanged(ContactMethod* cm, time_t t)
{
//ContactMethod with a Person are handled elsewhere
if (!cm->contact()) {
RecentViewNode* n = m_hCMsToNodes[cm];
const bool isNew = !n;
if (isNew) {
n = new RecentViewNode();
n->m_Type = RecentViewNode::Type::CONTACT_METHOD;
n->m_uContent.m_pContactMethod = cm ;
n->m_pParent = nullptr ;
n->m_Index = 0 ;
m_hCMsToNodes[cm] = n ;
}
insertNode(n, t, isNew);
slotUpdate();
}
}
///Remove the contact method once they are associated with a contact
void RecentModelPrivate::slotContactChanged(ContactMethod* cm, Person* np, Person* op)
{
Q_UNUSED(np)
RecentViewNode* n = m_hCMsToNodes[cm];
if (n)
removeNode(n);
}
void RecentModelPrivate::slotCallStateChanged(Call* call, Call::State previousState)
{
//qDebug() << "STATE CHANGED:" << call->peerContactMethod();
RecentViewNode* n;
if (auto p = call->peerContactMethod()->contact()) {
n = m_hPersonsToNodes[p];
} else {
n = m_hCMsToNodes[call->peerContactMethod()];
}
if (!n)
return;
if (call->state() == Call::State::OVER) {
// Find the active call in children of this node and remove it
auto itEnd = n->m_lChildren.end();
auto it = std::find_if (n->m_lChildren.begin(),
itEnd, [call] (RecentViewNode* child) {
return child->m_uContent.m_pCall == call;
});
if (it == itEnd) {
// Call can be in the bucket (after callAdded) but not inserted
// because it failed before lastUsedChanged to be emitted
m_lCallBucket.removeOne(call);
return;
}
// As long as we have more than 2 parallels calls, show them
if (n->m_lChildren.size() > 2) {
q_ptr->beginRemoveRows(q_ptr->index(n->m_Index,0), (*it)->m_Index, (*it)->m_Index);
n->m_lChildren.removeAt((*it)->m_Index);
q_ptr->endRemoveRows();
} else if (n->m_lChildren.size() == 2) {
// We had two calls, now only one, emit removeRows for ALL children
q_ptr->beginRemoveRows(q_ptr->index(n->m_Index,0), 0, n->m_lChildren.size() - 1);
n->m_lChildren.removeAt((*it)->m_Index);
q_ptr->endRemoveRows();
} else { // Only one call, remove it, and emit dataChanged
n->m_lChildren.removeFirst();
emit q_ptr->dataChanged(q_ptr->index(n->m_Index,0),q_ptr->index(n->m_Index,0));
}
} else
emit q_ptr->dataChanged(q_ptr->index(n->m_Index,0),q_ptr->index(n->m_Index,0));
}
void RecentModelPrivate::slotCallAdded(Call* call, Call* parent)
{
Q_UNUSED(parent)
RecentViewNode* n = nullptr;
if (auto p = call->peerContactMethod()->contact()) {
n = m_hPersonsToNodes[p];
} else {
n = m_hCMsToNodes[call->peerContactMethod()];
}
if (!n && !m_lCallBucket.contains(call)) {
m_lCallBucket.append(call);
connect(call, &Call::lifeCycleStateChanged, this, &RecentModelPrivate::slotUpdate);
return;
} else if (n && m_lCallBucket.contains(call)) {
m_lCallBucket.removeOne(call);
//TODO: remove the connection store callbucket as a map key = call value = metaconnection
} else if (!n && m_lCallBucket.contains(call)) {
return;
}
auto callNode = new RecentViewNode();
callNode->m_Type = RecentViewNode::Type::CALL;
callNode->m_uContent.m_pCall = call;
callNode->m_pParent = n;
callNode->m_Index = n->m_lChildren.size();
// Emit insertRows only when there is more than one call
if (n->m_lChildren.size() > 1) {
q_ptr->beginInsertRows(q_ptr->index(n->m_Index,0), n->m_lChildren.size(), n->m_lChildren.size());
n->m_lChildren.append(callNode);
q_ptr->endInsertRows();
} else if (n->m_lChildren.size() == 1) {
// If there is already a call emit insertRows for the two children
q_ptr->beginInsertRows(q_ptr->index(n->m_Index,0), 0, n->m_lChildren.size());
n->m_lChildren.append(callNode);
q_ptr->endInsertRows();
} else { // size == 0, only add the child, emit dataChanged for the top node
n->m_lChildren.append(callNode);
emit q_ptr->dataChanged(q_ptr->index(n->m_Index,0),q_ptr->index(n->m_Index,0));
}
}
void RecentModelPrivate::slotUpdate()
{
Q_FOREACH(auto call, m_lCallBucket) {
qDebug() << "trying to empty bucket call:" << call;
slotCallAdded(call, nullptr);
}
}