1367 lines
54 KiB
C++
1367 lines
54 KiB
C++
/*
|
|
* Copyright (C) 2007-2009 Patrick Ohly <patrick.ohly@gmx.de>
|
|
* Copyright (C) 2009 Intel Corporation
|
|
* Copyright (C) 2012 BMW Car IT GmbH. All rights reserved.
|
|
*
|
|
* 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) version 3.
|
|
*
|
|
* 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 "config.h"
|
|
|
|
#ifdef ENABLE_PBAP
|
|
|
|
#include "PbapSyncSource.h"
|
|
|
|
#include <boost/assign/list_of.hpp>
|
|
#include <boost/algorithm/string/case_conv.hpp>
|
|
#include <boost/tokenizer.hpp>
|
|
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <stdint.h>
|
|
|
|
#include <pcrecpp.h>
|
|
#include <algorithm>
|
|
|
|
#include <syncevo/GLibSupport.h> // PBAP backend does not compile without GLib.
|
|
#include <syncevo/util.h>
|
|
#include <syncevo/BoostHelper.h>
|
|
#include <src/syncevo/SynthesisEngine.h>
|
|
#include <syncevo/SuspendFlags.h>
|
|
|
|
#include "gdbus-cxx-bridge.h"
|
|
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <boost/bind.hpp>
|
|
|
|
#include <synthesis/SDK_util.h>
|
|
|
|
#include <syncevo/declarations.h>
|
|
SE_BEGIN_CXX
|
|
|
|
#define OBC_SERVICE "org.openobex.client" // obexd < 0.47
|
|
#define OBC_SERVICE_NEW "org.bluez.obex.client" // obexd >= 0.47, including 0.48 (with yet another slight API change!)
|
|
#define OBC_SERVICE_NEW5 "org.bluez.obex" // obexd in Bluez 5.0
|
|
#define OBC_CLIENT_INTERFACE "org.openobex.Client"
|
|
#define OBC_CLIENT_INTERFACE_NEW "org.bluez.obex.Client"
|
|
#define OBC_CLIENT_INTERFACE_NEW5 "org.bluez.obex.Client1"
|
|
#define OBC_PBAP_INTERFACE "org.openobex.PhonebookAccess"
|
|
#define OBC_PBAP_INTERFACE_NEW "org.bluez.obex.PhonebookAccess"
|
|
#define OBC_PBAP_INTERFACE_NEW5 "org.bluez.obex.PhonebookAccess1"
|
|
#define OBC_TRANSFER_INTERFACE_NEW "org.bluez.obex.Transfer"
|
|
#define OBC_TRANSFER_INTERFACE_NEW5 "org.bluez.obex.Transfer1"
|
|
|
|
typedef std::map<int, pcrecpp::StringPiece> Content;
|
|
typedef std::list<std::string> ContactQueue;
|
|
typedef std::list<std::string> Properties;
|
|
typedef boost::variant< std::string, Properties, uint16_t > Bluez5Values;
|
|
typedef std::map<std::string, Bluez5Values> Bluez5Filter;
|
|
typedef std::map<std::string, boost::variant<std::string> > Params;
|
|
typedef std::pair<GDBusCXX::DBusObject_t, Params> Bluez5PullAllResult;
|
|
|
|
enum PullData
|
|
{
|
|
PULL_AS_CONFIGURED,
|
|
PULL_WITHOUT_PHOTOS
|
|
};
|
|
|
|
struct PullParams
|
|
{
|
|
/** Which data to pull. */
|
|
PullData m_pullData;
|
|
|
|
/**
|
|
* How much time is meant to be used per chunk.
|
|
*/
|
|
double m_timePerChunk;
|
|
|
|
/**
|
|
* The lambda factor used in exponential smoothing of the max
|
|
* count per transfer to achieve the desired time per chunk.
|
|
* 0 means "use latest observation only", 1 means "keep using
|
|
* initial chunk size".
|
|
*/
|
|
double m_timeLambda;
|
|
|
|
/** Initial chunk size in number of contacts, without and with photo data. */
|
|
uint16_t m_startMaxCount[2];
|
|
|
|
/** Initial chunk offset, again in contacts. */
|
|
uint16_t m_startOffset;
|
|
|
|
// cppcheck-suppress memsetClassFloat
|
|
PullParams() { memset(this, 0, sizeof(*this)); m_timePerChunk = m_timeLambda = 0; }
|
|
};
|
|
|
|
/**
|
|
* This class is responsible for a) generating unique IDs for each
|
|
* contact in a certain order (returned one-by-one via getNextID())
|
|
* and b) returning the contact one-by-one in that order
|
|
* (getContact()). This is done to match the way how the sync engine
|
|
* handles items.
|
|
*
|
|
* Although the API of getContact() allows random access, we don't need
|
|
* to support that for syncing.
|
|
*
|
|
* IDs are #0 to #n-1 where n = GetSize() at the time when the session
|
|
* starts.
|
|
*
|
|
* A simple transfer then just does a PullAll() and returns the
|
|
* incoming data one at a time. The downsides are a) if the transfer
|
|
* always gets interrupted in the middle, we never cache contacts at
|
|
* the end and b) the entire data must be stored temporarily, either in RAM
|
|
* or on disk.
|
|
*
|
|
* Transfers have been reported to take half an hour for slow peers and
|
|
* large address books. This is perhaps unusual, but it happens. More common
|
|
* is the second downside.
|
|
*
|
|
* Transferring in chunks addresses both. Here's a potential (and not
|
|
* 100% correct!) algorithm for transferring a complete address book in chunks:
|
|
*
|
|
* uint16 used = GetSize() # not the same as maximum offset!
|
|
* uint16 start = choose_start()
|
|
* uint16 chunksize = choose_chunk_size()
|
|
*
|
|
* uint16 i
|
|
* for (i = start; i < used; i += chunksize) {
|
|
* PullAll( Offset = i, MaxCount = chunksize)
|
|
* }
|
|
* for (i = 0; i < start; i += chunksize) {
|
|
* PullAll( Offset = i, MaxCount = min(chunksize, start - 1)
|
|
* }
|
|
*
|
|
* Note that GetSize() is specified as returning the number of entries in
|
|
* the selected phonebook object that are actually used (i.e. indexes that
|
|
* correspond to non-NULL entries). This is relevant if contacts get
|
|
* deleted after starting the session. In that case, the algorithm above
|
|
* will not necessarily read all contacts. Here's an example:
|
|
* offsets #0 till #99, with contacts #10 till #19 deleted
|
|
* chunksize = 10
|
|
* GetSize() = 90
|
|
*
|
|
* => this will request offsets #0 till #89, missing contacts #90 till #99
|
|
*
|
|
* This could be fixed with an additional PullAll, leading to:
|
|
*
|
|
* for (i = start; i < used; i += chunksize) {
|
|
* PullAll( Offset = i, MaxCount = chunksize)
|
|
* }
|
|
* PullAll(Offset = i) # no MaxCount!
|
|
* for (i = 0; i < start; i += chunksize) {
|
|
* PullAll( Offset = i, MaxCount = min(chunksize, start - 1)
|
|
* }
|
|
*
|
|
* The additional PullAll() is meant to read all contacts at the end which
|
|
* would not be covered otherwise.
|
|
*
|
|
* Now the other problem: MaxCount means "read chunksize contacts
|
|
* starting at #i". Therefore the algorithm above will end up reading contacts
|
|
* multiple times occasionally. Example:
|
|
*
|
|
* offsets #0 till #99, with contact #0 deleted
|
|
* chunksize = 10
|
|
* GetSize() = 98
|
|
*
|
|
* PullAll(Offset = 0, MaxCount = 10) => returns 10 contacts #1 till #10 (inclusive)
|
|
* PullAll(Offset = 10, MaxCount = 10) => returns 10 contacts #10 till #19
|
|
* => contact #10 appears twice in the result
|
|
*
|
|
* The duplicate cannot be filtered out easily because the UID is not
|
|
* reliable. This could be addressed by keeping a hash of each contact and
|
|
* discarding those who are exact matches for already seen contacts. It's easier
|
|
* to accept the duplicate and remove it during the next sync.
|
|
*
|
|
* When combining these two problems (some contacts read twice, plus
|
|
* the additional PullAll() at the end), we can get more contacts than
|
|
* originally anticipated based on GetSize(). The sync engine will not
|
|
* ask for more contacts than we originally announced. Therefore the
|
|
* current implementation does *not* do the additional PullAll(); this
|
|
* is unlikely to cause any real problems because it should be rare
|
|
* that the number of contacts changes in the short period of time
|
|
* between establishing the session and asking for the size.
|
|
*
|
|
* There are two more aspects that I chose to ignore above: how to
|
|
* implement the choice of start offset and chunk size.
|
|
*
|
|
* Start offset could be random (no persistent state needed) or could
|
|
* continue where the last sync left off. The latter will require a write
|
|
* after each PullAll() (in case of unexpected shutdowns), even if nothing
|
|
* ever changes. Is that acceptable? Probably not. The current implementation
|
|
* chooses randomly by default.
|
|
*
|
|
* The chunk size in bytes depends on the size of the average contact,
|
|
* which is unknown. Make it too small, and we end up generating lots
|
|
* of individual transfers. Make it too large, and we still have
|
|
* chunks that never transfer completely. The current implementation
|
|
* uses self-tuning to achieve a certain desired transfer time per
|
|
* chunk.
|
|
*
|
|
* This algorithm can be tuned by env variables. See the README for
|
|
* details.
|
|
*/
|
|
class PullAll
|
|
{
|
|
PullParams m_pullParams;
|
|
|
|
std::string m_buffer; // vCards kept in memory when using old obexd.
|
|
TmpFile m_tmpFile; // Stored in temporary file and mmapped with more recent obexd.
|
|
|
|
// Maps contact number to chunks of m_buffer or m_tmpFile.
|
|
Content m_content;
|
|
int m_contentStartIndex;
|
|
|
|
uint16_t m_numContacts; // Number of existing contacts, according to GetSize() or after downloading.
|
|
uint16_t m_currentContact; // Numbered starting with zero according to discovery in addVCards.
|
|
boost::shared_ptr<PbapSession> m_session; // Only set when there is a transfer ongoing.
|
|
size_t m_tmpFileOffset; // Number of bytes already parsed.
|
|
uint16_t m_transferOffset; // First contact requested as part of current transfer.
|
|
uint16_t m_initialOffset; // First contact request by first transfer.
|
|
uint16_t m_transferMaxCount; // Number of contacts requested as part of current transfer, 0 when not doing chunked transfers.
|
|
uint16_t m_desiredMaxCount; // Number of contacts supposed to be transfered, may be more than m_transferMaxCount when reading at the end of the enumerated contacts.
|
|
Bluez5Filter m_filter; // Current filter for a Bluez5-like transfer (includes obexd 0.48 case).
|
|
Timespec m_transferStart; // Start time of current transfer.
|
|
|
|
// Observed results from the last transfer.
|
|
double m_lastTransferRate;
|
|
double m_lastContactSizeAverage;
|
|
bool m_wasSuspended;
|
|
|
|
friend class PbapSession;
|
|
friend class PbapSyncSource;
|
|
public:
|
|
PullAll();
|
|
~PullAll();
|
|
|
|
std::string getNextID();
|
|
bool getContact(const char *id, pcrecpp::StringPiece &vcard);
|
|
const char *addVCards(int startIndex, const pcrecpp::StringPiece &content);
|
|
};
|
|
|
|
PullAll::PullAll() :
|
|
m_contentStartIndex(0),
|
|
m_numContacts(0),
|
|
m_currentContact(0),
|
|
m_tmpFileOffset(0),
|
|
m_transferOffset(0),
|
|
m_initialOffset(0),
|
|
m_transferMaxCount(0),
|
|
m_desiredMaxCount(0),
|
|
m_lastTransferRate(0),
|
|
m_lastContactSizeAverage(0),
|
|
m_wasSuspended(false)
|
|
{}
|
|
|
|
PullAll::~PullAll()
|
|
{
|
|
}
|
|
|
|
class PbapSession : private boost::noncopyable {
|
|
public:
|
|
static boost::shared_ptr<PbapSession> create(PbapSyncSource &parent);
|
|
|
|
void initSession(const std::string &address, const std::string &format);
|
|
|
|
typedef std::map<std::string, pcrecpp::StringPiece> Content;
|
|
|
|
boost::shared_ptr<PullAll> startPullAll(const PullParams &pullParams);
|
|
void continuePullAll(PullAll &state);
|
|
void checkForError(); // Throws exception if transfer failed.
|
|
Timespec transferComplete() const;
|
|
void resetTransfer();
|
|
void shutdown(void);
|
|
void setFreeze(bool freeze);
|
|
void blockOnFreeze();
|
|
|
|
private:
|
|
PbapSession(PbapSyncSource &parent);
|
|
|
|
PbapSyncSource &m_parent;
|
|
boost::weak_ptr<PbapSession> m_self;
|
|
std::unique_ptr<GDBusCXX::DBusRemoteObject> m_client;
|
|
bool m_frozen;
|
|
enum {
|
|
OBEXD_OLD, // obexd < 0.47
|
|
OBEXD_NEW, // obexd == 0.47, file-based transfer
|
|
// OBEXD_048 // obexd == 0.48, file-based transfer without SetFilter and with filter parameter to PullAll()
|
|
BLUEZ5 // obexd in Bluez >= 5.0
|
|
} m_obexAPI;
|
|
|
|
Bluez5Filter m_filter5;
|
|
Properties m_filterFields;
|
|
Properties supportedProperties() const;
|
|
|
|
/**
|
|
* m_transferComplete will be set to the current monotonic time when observing a
|
|
* "Complete" signal on a transfer object path which has the
|
|
* current session as prefix. There may be more than one such transfer,
|
|
* so record all completions that we see and then pick the right one.
|
|
*
|
|
* It also gets set when an error occurred for such a transfer,
|
|
* in which case m_error will also be set.
|
|
*
|
|
* This only works as long as the session is only used for a
|
|
* single transfer. Otherwise a more complex tracking of
|
|
* completion, for example per transfer object path, is needed.
|
|
*/
|
|
class Completion {
|
|
public:
|
|
Timespec m_transferComplete;
|
|
std::string m_transferErrorCode;
|
|
std::string m_transferErrorMsg;
|
|
|
|
static Completion now() {
|
|
Completion res;
|
|
res.m_transferComplete = Timespec::monotonic();
|
|
return res;
|
|
}
|
|
};
|
|
typedef std::map<std::string, Completion> Transfers;
|
|
Transfers m_transfers;
|
|
std::string m_currentTransfer;
|
|
|
|
std::unique_ptr<GDBusCXX::SignalWatch3<GDBusCXX::Path_t, std::string, std::string> >
|
|
m_errorSignal;
|
|
void errorCb(const GDBusCXX::Path_t &path, const std::string &error,
|
|
const std::string &msg);
|
|
|
|
// Bluez 5
|
|
typedef GDBusCXX::SignalWatch4<GDBusCXX::Path_t, std::string, Params, std::vector<std::string> > PropChangedSignal_t;
|
|
std::unique_ptr<PropChangedSignal_t> m_propChangedSignal;
|
|
void propChangedCb(const GDBusCXX::Path_t &path,
|
|
const std::string &interface,
|
|
const Params &changed,
|
|
const std::vector<std::string> &invalidated);
|
|
|
|
// new obexd API
|
|
typedef GDBusCXX::SignalWatch1<GDBusCXX::Path_t> CompleteSignal_t;
|
|
std::unique_ptr<CompleteSignal_t> m_completeSignal;
|
|
void completeCb(const GDBusCXX::Path_t &path);
|
|
typedef GDBusCXX::SignalWatch3<GDBusCXX::Path_t, std::string, boost::variant<int64_t> > PropertyChangedSignal_t;
|
|
std::unique_ptr<PropertyChangedSignal_t> m_propertyChangedSignal;
|
|
void propertyChangedCb(const GDBusCXX::Path_t &path, const std::string &name, const boost::variant<int64_t> &value);
|
|
|
|
std::unique_ptr<GDBusCXX::DBusRemoteObject> m_session;
|
|
};
|
|
|
|
PbapSession::PbapSession(PbapSyncSource &parent) :
|
|
m_parent(parent),
|
|
m_frozen(false)
|
|
{
|
|
}
|
|
|
|
boost::shared_ptr<PbapSession> PbapSession::create(PbapSyncSource &parent)
|
|
{
|
|
boost::shared_ptr<PbapSession> session(new PbapSession(parent));
|
|
session->m_self = session;
|
|
return session;
|
|
}
|
|
|
|
void PbapSession::propChangedCb(const GDBusCXX::Path_t &path,
|
|
const std::string &interface,
|
|
const Params &changed,
|
|
const std::vector<std::string> &invalidated)
|
|
{
|
|
// Called for a path which matches the current session, so we know
|
|
// that the signal is for our transfer. Only need to check the status.
|
|
Params::const_iterator it = changed.find("Status");
|
|
if (it != changed.end()) {
|
|
std::string status = boost::get<std::string>(it->second);
|
|
SE_LOG_DEBUG(NULL, "OBEXD transfer %s: %s",
|
|
path.c_str(), status.c_str());
|
|
if (status == "complete" || status == "error") {
|
|
Completion completion = Completion::now();
|
|
if (status == "error") {
|
|
// We have to make up some error descriptions. The Bluez
|
|
// 5 API no longer seems to provide that.
|
|
completion.m_transferErrorCode = "transfer failed";
|
|
completion.m_transferErrorMsg = "reason unknown";
|
|
}
|
|
m_transfers[path] = completion;
|
|
} else if (status == "active" && m_currentTransfer == path && m_frozen) {
|
|
// Retry Suspend() which must have failed earlier.
|
|
try {
|
|
GDBusCXX::DBusRemoteObject transfer(m_client->getConnection(),
|
|
m_currentTransfer,
|
|
OBC_TRANSFER_INTERFACE_NEW5,
|
|
OBC_SERVICE_NEW5,
|
|
true);
|
|
GDBusCXX::DBusClientCall0(transfer, "Suspend")();
|
|
SE_LOG_DEBUG(NULL, "successfully suspended transfer when it became active");
|
|
} catch (...) {
|
|
// Ignore all errors here. The worst that can happen is that
|
|
// the transfer continues to run. Once Bluez supports suspending
|
|
// queued transfers we shouldn't get here at all.
|
|
std::string explanation;
|
|
Exception::handle(explanation, HANDLE_EXCEPTION_NO_ERROR);
|
|
SE_LOG_DEBUG(NULL, "ignoring failure of delayed suspend: %s", explanation.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PbapSession::completeCb(const GDBusCXX::Path_t &path)
|
|
{
|
|
SE_LOG_DEBUG(NULL, "obexd transfer %s completed", path.c_str());
|
|
m_transfers[path] = Completion::now();
|
|
}
|
|
|
|
void PbapSession::errorCb(const GDBusCXX::Path_t &path,
|
|
const std::string &error,
|
|
const std::string &msg)
|
|
{
|
|
SE_LOG_DEBUG(NULL, "obexd transfer %s failed: %s %s",
|
|
path.c_str(), error.c_str(), msg.c_str());
|
|
Completion &completion = m_transfers[path];
|
|
completion.m_transferComplete = Timespec::monotonic();
|
|
completion.m_transferErrorCode = error;
|
|
completion.m_transferErrorMsg = msg;
|
|
}
|
|
|
|
void PbapSession::propertyChangedCb(const GDBusCXX::Path_t &path,
|
|
const std::string &name,
|
|
const boost::variant<int64_t> &value)
|
|
{
|
|
const int64_t *tmp = boost::get<int64_t>(&value);
|
|
if (tmp) {
|
|
SE_LOG_DEBUG(NULL, "obexd transfer %s property change: %s = %ld",
|
|
path.c_str(), name.c_str(), (long signed)*tmp);
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, "obexd transfer %s property change: %s",
|
|
path.c_str(), name.c_str());
|
|
}
|
|
}
|
|
|
|
Properties PbapSession::supportedProperties() const
|
|
{
|
|
Properties props;
|
|
static const std::set<std::string> supported =
|
|
boost::assign::list_of("VERSION")
|
|
("FN")
|
|
("N")
|
|
("PHOTO")
|
|
("BDAY")
|
|
("ADR")
|
|
("LABEL")
|
|
("TEL")
|
|
("EMAIL")
|
|
("MAILER")
|
|
("TZ")
|
|
("GEO")
|
|
("TITLE")
|
|
("ROLE")
|
|
("LOGO")
|
|
("AGENT")
|
|
("ORG")
|
|
("NOTE")
|
|
("REV")
|
|
("SOUND")
|
|
("URL")
|
|
("UID")
|
|
("KEY")
|
|
("NICKNAME")
|
|
("CATEGORIES")
|
|
("CLASS");
|
|
|
|
BOOST_FOREACH (const std::string &prop, m_filterFields) {
|
|
// Be conservative and only ask for properties that we
|
|
// really know how to use. obexd also lists the bit field
|
|
// strings ("BIT01") but phones have been seen to reject
|
|
// queries when those were enabled.
|
|
if (supported.find(prop) != supported.end()) {
|
|
props.push_back(prop);
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
|
|
void PbapSession::initSession(const std::string &address, const std::string &format)
|
|
{
|
|
if (m_session.get()) {
|
|
return;
|
|
}
|
|
|
|
// format string uses:
|
|
// [(2.1|3.0):][^]propname,propname,...
|
|
//
|
|
// 3.0:^PHOTO = download in vCard 3.0 format, excluding PHOTO
|
|
// 2.1:PHOTO = download in vCard 2.1 format, only the PHOTO
|
|
|
|
std::string version;
|
|
std::string tmp;
|
|
std::string properties;
|
|
const pcrecpp::RE re("(?:(2\\.1|3\\.0):?)?(\\^?)([-a-zA-Z,]*)");
|
|
if (!re.FullMatch(format, &version, &tmp, &properties)) {
|
|
m_parent.throwError(SE_HERE, StringPrintf("invalid specification of PBAP vCard format (databaseFormat): %s",
|
|
format.c_str()));
|
|
}
|
|
char negated = tmp.c_str()[0];
|
|
if (version.empty()) {
|
|
// same default as in obexd
|
|
version = "2.1";
|
|
}
|
|
if (version != "2.1" && version != "3.0") {
|
|
m_parent.throwError(SE_HERE, StringPrintf("invalid vCard version prefix in PBAP vCard format specification (databaseFormat): %s",
|
|
format.c_str()));
|
|
}
|
|
std::set<std::string> keywords;
|
|
boost::split(keywords, properties, boost::is_from_range(',', ','));
|
|
typedef std::map<std::string, boost::variant<std::string> > Params;
|
|
|
|
Params params;
|
|
params["Target"] = std::string("PBAP");
|
|
|
|
std::string session;
|
|
GDBusCXX::DBusConnectionPtr conn = GDBusCXX::dbus_get_bus_connection("SESSION", NULL, true, NULL);
|
|
|
|
// We must attempt to use the new interface(s), otherwise we won't know whether
|
|
// the daemon exists or can be started.
|
|
m_obexAPI = BLUEZ5;
|
|
m_client.reset(new GDBusCXX::DBusRemoteObject(conn,
|
|
"/org/bluez/obex",
|
|
OBC_CLIENT_INTERFACE_NEW5,
|
|
OBC_SERVICE_NEW5, true));
|
|
try {
|
|
SE_LOG_DEBUG(NULL, "trying to use bluez 5 obexd service %s", OBC_SERVICE_NEW5);
|
|
session =
|
|
GDBusCXX::DBusClientCall1<GDBusCXX::DBusObject_t>(*m_client, "CreateSession")(address, params);
|
|
} catch (const std::exception &error) {
|
|
if (!strstr(error.what(), "org.freedesktop.DBus.Error.ServiceUnknown") &&
|
|
!strstr(error.what(), "org.freedesktop.DBus.Error.UnknownObject")) {
|
|
throw;
|
|
}
|
|
// Fall back to old interface.
|
|
SE_LOG_DEBUG(NULL, "bluez obex service not available (%s), falling back to previous obexd one %s",
|
|
error.what(),
|
|
OBC_SERVICE_NEW);
|
|
m_obexAPI = OBEXD_NEW;
|
|
}
|
|
|
|
if (session.empty()) {
|
|
m_client.reset(new GDBusCXX::DBusRemoteObject(conn, "/", OBC_CLIENT_INTERFACE_NEW,
|
|
OBC_SERVICE_NEW, true));
|
|
try {
|
|
SE_LOG_DEBUG(NULL, "trying to use new obexd service %s", OBC_SERVICE_NEW);
|
|
session =
|
|
GDBusCXX::DBusClientCall1<GDBusCXX::DBusObject_t>(*m_client, "CreateSession")(address, params);
|
|
} catch (const std::exception &error) {
|
|
if (!strstr(error.what(), "org.freedesktop.DBus.Error.ServiceUnknown")) {
|
|
throw;
|
|
}
|
|
// Fall back to old interface.
|
|
SE_LOG_DEBUG(NULL, "new obexd service(s) not available (%s), falling back to old one %s",
|
|
error.what(),
|
|
OBC_SERVICE);
|
|
m_obexAPI = OBEXD_OLD;
|
|
}
|
|
}
|
|
|
|
if (session.empty()) {
|
|
m_client.reset(new GDBusCXX::DBusRemoteObject(conn, "/", OBC_CLIENT_INTERFACE,
|
|
OBC_SERVICE, true));
|
|
params["Destination"] = std::string(address);
|
|
session = GDBusCXX::DBusClientCall1<GDBusCXX::DBusObject_t>(*m_client, "CreateSession")(params);
|
|
}
|
|
|
|
if (session.empty()) {
|
|
m_parent.throwError(SE_HERE, "PBAP: failed to create session");
|
|
}
|
|
|
|
if (m_obexAPI != OBEXD_OLD) {
|
|
m_session.reset(new GDBusCXX::DBusRemoteObject(m_client->getConnection(),
|
|
session,
|
|
m_obexAPI == BLUEZ5 ? OBC_PBAP_INTERFACE_NEW5 : OBC_PBAP_INTERFACE_NEW,
|
|
m_obexAPI == BLUEZ5 ? OBC_SERVICE_NEW5 : OBC_SERVICE_NEW,
|
|
true));
|
|
|
|
// Filter Transfer signals via path prefix. Discussions on Bluez
|
|
// list showed that this is meant to be possible, even though the
|
|
// client-api.txt documentation itself didn't (and still doesn't)
|
|
// make it clear:
|
|
// "[PATCH obexd v0] client-doc: Guarantee prefix in transfer paths"
|
|
// http://www.spinics.net/lists/linux-bluetooth/msg28409.html
|
|
//
|
|
// Be extra careful with asynchronous callbacks: bind to weak
|
|
// pointer and ignore callback when the instance is already gone.
|
|
// Should not happen with signals (destructing the class unregisters
|
|
// the watch), but very well may happen in asynchronous method
|
|
// calls. Therefore maintain m_self and show how to use it here.
|
|
if (m_obexAPI == BLUEZ5) {
|
|
// Bluez 5
|
|
m_propChangedSignal.reset(new PropChangedSignal_t
|
|
(GDBusCXX::SignalFilter(m_client->getConnection(),
|
|
session,
|
|
"org.freedesktop.DBus.Properties",
|
|
"PropertiesChanged",
|
|
GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
|
|
m_propChangedSignal->activate(boost::bind(&PbapSession::propChangedCb, m_self, _1, _2, _3, _4));
|
|
} else {
|
|
// obexd >= 0.47
|
|
m_completeSignal.reset(new CompleteSignal_t
|
|
(GDBusCXX::SignalFilter(m_client->getConnection(),
|
|
session,
|
|
OBC_TRANSFER_INTERFACE_NEW,
|
|
"Complete",
|
|
GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
|
|
m_completeSignal->activate(boost::bind(&PbapSession::completeCb, m_self, _1));
|
|
|
|
// same for error
|
|
m_errorSignal.reset(new GDBusCXX::SignalWatch3<GDBusCXX::Path_t, std::string, std::string>
|
|
(GDBusCXX::SignalFilter(m_client->getConnection(),
|
|
session,
|
|
OBC_TRANSFER_INTERFACE_NEW,
|
|
"Error",
|
|
GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
|
|
m_errorSignal->activate(boost::bind(&PbapSession::errorCb, m_self, _1, _2, _3));
|
|
|
|
// and property changes
|
|
m_propertyChangedSignal.reset(new PropertyChangedSignal_t(GDBusCXX::SignalFilter(m_client->getConnection(),
|
|
session,
|
|
OBC_TRANSFER_INTERFACE_NEW,
|
|
"PropertyChanged",
|
|
GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
|
|
m_propertyChangedSignal->activate(boost::bind(&PbapSession::propertyChangedCb, m_self, _1, _2, _3));
|
|
}
|
|
} else {
|
|
// obexd < 0.47
|
|
m_session.reset(new GDBusCXX::DBusRemoteObject(m_client->getConnection(),
|
|
session,
|
|
OBC_PBAP_INTERFACE,
|
|
OBC_SERVICE,
|
|
true));
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "PBAP session created: %s", m_session->getPath());
|
|
|
|
// get filter list so that we can continue validating our format specifier
|
|
m_filterFields = GDBusCXX::DBusClientCall1< Properties >(*m_session, "ListFilterFields")();
|
|
SE_LOG_DEBUG(NULL, "supported PBAP filter fields:\n %s",
|
|
boost::join(m_filterFields, "\n ").c_str());
|
|
|
|
Properties filter;
|
|
if (negated) {
|
|
// negated, start with everything set
|
|
filter = supportedProperties();
|
|
}
|
|
|
|
// validate parameters and update filter
|
|
BOOST_FOREACH (const std::string &prop, keywords) {
|
|
if (prop.empty()) {
|
|
continue;
|
|
}
|
|
|
|
Properties::const_iterator entry =
|
|
std::find_if(m_filterFields.begin(),
|
|
m_filterFields.end(),
|
|
boost::bind(&boost::iequals<std::string,std::string>, _1, prop, std::locale()));
|
|
|
|
if (entry == m_filterFields.end()) {
|
|
m_parent.throwError(SE_HERE, StringPrintf("invalid property name in PBAP vCard format specification (databaseFormat): %s",
|
|
prop.c_str()));
|
|
}
|
|
|
|
if (negated) {
|
|
filter.remove(*entry);
|
|
} else {
|
|
filter.push_back(*entry);
|
|
}
|
|
}
|
|
|
|
GDBusCXX::DBusClientCall0(*m_session, "Select")(std::string("int"), std::string("PB"));
|
|
m_filter5["Format"] = version == "2.1" ? "vcard21" : "vcard30";
|
|
m_filter5["Fields"] = filter;
|
|
|
|
SE_LOG_DEBUG(NULL, "PBAP session initialized");
|
|
}
|
|
|
|
boost::shared_ptr<PullAll> PbapSession::startPullAll(const PullParams &pullParams)
|
|
{
|
|
resetTransfer();
|
|
blockOnFreeze();
|
|
|
|
// Update prepared filter to match pullData.
|
|
Bluez5Filter currentFilter = m_filter5;
|
|
std::string &format = boost::get<std::string>(currentFilter["Format"]);
|
|
std::list<std::string> &filter = boost::get< std::list<std::string> >(currentFilter["Fields"]);
|
|
switch (pullParams.m_pullData) {
|
|
case PULL_AS_CONFIGURED:
|
|
// Avoid empty filter. Android 4.3 on Samsung Galaxy S3
|
|
// only returns the mandatory FN, N, TEL fields when no
|
|
// filter is set.
|
|
const char *filterSource;
|
|
if (filter.empty()) {
|
|
filterSource = "default properties";
|
|
filter = supportedProperties();
|
|
} else {
|
|
filterSource = "configured";
|
|
}
|
|
SE_LOG_DEBUG(NULL, "pull all with %s filter: '%s'",
|
|
filterSource,
|
|
boost::join(filter, " ").c_str());
|
|
break;
|
|
case PULL_WITHOUT_PHOTOS:
|
|
// Remove PHOTO from list or create list with the other properties.
|
|
if (filter.empty()) {
|
|
filter = supportedProperties();
|
|
}
|
|
for (Properties::iterator it = filter.begin();
|
|
it != filter.end();
|
|
++it) {
|
|
if (*it == "PHOTO") {
|
|
filter.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
SE_LOG_DEBUG(NULL, "pull all without photos: '%s'",
|
|
boost::join(filter, " ").c_str());
|
|
break;
|
|
}
|
|
|
|
bool pullAllWithFiltersFallback = false;
|
|
if (m_obexAPI == OBEXD_OLD ||
|
|
m_obexAPI == OBEXD_NEW) {
|
|
try {
|
|
GDBusCXX::DBusClientCall0(*m_session, "SetFilter")(filter);
|
|
GDBusCXX::DBusClientCall0(*m_session, "SetFormat")(format);
|
|
} catch (...) {
|
|
// Ignore failure, can happen with 0.48. Instead send filter together
|
|
// with PullAll method call.
|
|
Exception::handle(HANDLE_EXCEPTION_NO_ERROR);
|
|
pullAllWithFiltersFallback = true;
|
|
}
|
|
}
|
|
|
|
boost::shared_ptr<PullAll> state(new PullAll);
|
|
state->m_pullParams = pullParams;
|
|
state->m_contentStartIndex = 0;
|
|
state->m_currentContact = 0;
|
|
state->m_transferOffset = 0;
|
|
state->m_desiredMaxCount = 0;
|
|
state->m_initialOffset = 0;
|
|
state->m_transferMaxCount = 0;
|
|
state->m_lastTransferRate = 0;
|
|
state->m_lastContactSizeAverage = 0;
|
|
state->m_wasSuspended = false;
|
|
if (m_obexAPI != OBEXD_OLD) {
|
|
// Beware, this will lead to a "Complete" signal in obexd
|
|
// 0.47. We need to be careful with looking at the right
|
|
// transfer to determine whether PullAll completed.
|
|
state->m_numContacts = GDBusCXX::DBusClientCall1<uint16_t>(*m_session, "GetSize")();
|
|
SE_LOG_DEBUG(NULL, "Expecting %d contacts.", state->m_numContacts);
|
|
|
|
state->m_tmpFile.create(TmpFile::FILE);
|
|
SE_LOG_DEBUG(NULL, "Created temporary file for PullAll %s", state->m_tmpFile.filename().c_str());
|
|
|
|
// Start chunk size depends on whether we pull PHOTOs.
|
|
bool pullPhotos = std::find(filter.begin(), filter.end(), "PHOTO") != filter.end();
|
|
state->m_transferMaxCount = pullParams.m_startMaxCount[pullPhotos];
|
|
if (state->m_transferMaxCount > 0 &&
|
|
(pullAllWithFiltersFallback || m_obexAPI == BLUEZ5)) {
|
|
// Enable transfering in chunks.
|
|
state->m_desiredMaxCount = state->m_transferMaxCount;
|
|
|
|
state->m_initialOffset =
|
|
state->m_transferOffset = pullParams.m_startOffset < 0 ?
|
|
0 :
|
|
(pullParams.m_startOffset % state->m_numContacts);
|
|
uint16_t available = state->m_numContacts - state->m_transferOffset;
|
|
if (available < state->m_transferMaxCount) {
|
|
// Don't read past end of contacts.
|
|
state->m_transferMaxCount = available;
|
|
}
|
|
currentFilter["Offset"] = state->m_transferOffset;
|
|
currentFilter["MaxCount"] = state->m_transferMaxCount;
|
|
state->m_filter = currentFilter;
|
|
}
|
|
|
|
state->m_transferStart.resetMonotonic();
|
|
Bluez5PullAllResult tuple =
|
|
pullAllWithFiltersFallback ?
|
|
// 0.48
|
|
GDBusCXX::DBusClientCall1<std::pair<GDBusCXX::DBusObject_t, Params> >(*m_session, "PullAll")(state->m_tmpFile.filename(), currentFilter) :
|
|
m_obexAPI == OBEXD_NEW ?
|
|
// 0.47
|
|
GDBusCXX::DBusClientCall1<std::pair<GDBusCXX::DBusObject_t, Params> >(*m_session, "PullAll")(state->m_tmpFile.filename()) :
|
|
// 5.x
|
|
GDBusCXX::DBusClientCall2<GDBusCXX::DBusObject_t, Params>(*m_session, "PullAll")(state->m_tmpFile.filename(), currentFilter);
|
|
const GDBusCXX::DBusObject_t &transfer = tuple.first;
|
|
const Params &properties = tuple.second;
|
|
m_currentTransfer = transfer;
|
|
SE_LOG_DEBUG(NULL, "start pullall offset #%u count %u, transfer path %s, %ld properties",
|
|
state->m_transferOffset,
|
|
state->m_transferMaxCount,
|
|
transfer.c_str(),
|
|
(long)properties.size());
|
|
// Work will be finished incrementally in PullAll::getContact().
|
|
//
|
|
// In the meantime we return IDs by simply enumerating the expected ones.
|
|
// If we don't get as many contacts as expected, we return 404 in getContact()
|
|
// and the Synthesis engine will ignore the ID (src/sysync/binfileimplds.cpp:
|
|
// "Record does not exist any more in database%s -> ignore").
|
|
state->m_tmpFileOffset = 0;
|
|
state->m_session = m_self.lock();
|
|
state->m_filter = currentFilter;
|
|
} else {
|
|
// < 0.47
|
|
//
|
|
// This only works once. Incremental syncing with the same
|
|
// session leads to a "PullAll method with no arguments not
|
|
// found" error from obex-client. Looks like a bug/limitation
|
|
// of obex-client < 0.47. Not sure what we should do about
|
|
// this: disable incremental sync for old obex-client? Reject
|
|
// it? Catch the error and add a better exlanation?
|
|
GDBusCXX::DBusClientCall1<std::string> pullall(*m_session, "PullAll");
|
|
state->m_buffer = pullall();
|
|
state->addVCards(0, state->m_buffer);
|
|
state->m_numContacts = state->m_content.size();
|
|
}
|
|
return state;
|
|
}
|
|
|
|
const char *PullAll::addVCards(int startIndex, const pcrecpp::StringPiece &vcards)
|
|
{
|
|
pcrecpp::StringPiece vcarddata;
|
|
pcrecpp::StringPiece tmp = vcards;
|
|
int count = startIndex;
|
|
pcrecpp::RE re("[\\r\\n]*(^BEGIN:VCARD.*?^END:VCARD)",
|
|
pcrecpp::RE_Options().set_dotall(true).set_multiline(true));
|
|
while (re.Consume(&tmp, &vcarddata)) {
|
|
m_content[count] = vcarddata;
|
|
++count;
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "PBAP content parsed: %d contacts starting at ID %d", count - startIndex, startIndex);
|
|
return tmp.data();
|
|
}
|
|
|
|
void PbapSession::continuePullAll(PullAll &state)
|
|
{
|
|
m_transfers.clear();
|
|
state.m_transferStart.resetMonotonic();
|
|
blockOnFreeze();
|
|
|
|
Bluez5PullAllResult tuple =
|
|
m_obexAPI == BLUEZ5 ?
|
|
GDBusCXX::DBusClientCall2<GDBusCXX::DBusObject_t, Params>(*m_session, "PullAll")(state.m_tmpFile.filename(), state.m_filter) :
|
|
// must be 0.48
|
|
GDBusCXX::DBusClientCall1<std::pair<GDBusCXX::DBusObject_t, Params> >(*m_session, "PullAll")(state.m_tmpFile.filename(), state.m_filter);
|
|
|
|
const GDBusCXX::DBusObject_t &transfer = tuple.first;
|
|
const Params &properties = tuple.second;
|
|
m_currentTransfer = transfer;
|
|
SE_LOG_DEBUG(NULL, "continue pullall offset #%u count %u, transfer path %s, %ld properties",
|
|
state.m_transferOffset,
|
|
state.m_transferMaxCount,
|
|
transfer.c_str(),
|
|
(long)properties.size());
|
|
}
|
|
|
|
void PbapSession::checkForError()
|
|
{
|
|
Transfers::const_iterator it = m_transfers.find(m_currentTransfer);
|
|
if (it != m_transfers.end()) {
|
|
if (!it->second.m_transferErrorCode.empty()) {
|
|
m_parent.throwError(SE_HERE, StringPrintf("%s: %s",
|
|
it->second.m_transferErrorCode.c_str(),
|
|
it->second.m_transferErrorMsg.c_str()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Timespec PbapSession::transferComplete() const
|
|
{
|
|
Timespec res;
|
|
Transfers::const_iterator it = m_transfers.find(m_currentTransfer);
|
|
if (it != m_transfers.end()) {
|
|
res = it->second.m_transferComplete;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void PbapSession::resetTransfer()
|
|
{
|
|
m_transfers.clear();
|
|
}
|
|
|
|
std::string PullAll::getNextID()
|
|
{
|
|
std::string id;
|
|
if (m_currentContact < m_numContacts) {
|
|
id = StringPrintf("%d", m_currentContact);
|
|
m_currentContact++;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
bool PullAll::getContact(const char *id, pcrecpp::StringPiece &vcard)
|
|
{
|
|
int contactNumber = atoi(id);
|
|
SE_LOG_DEBUG(NULL, "get PBAP contact ID %s", id);
|
|
if (contactNumber < 0 ||
|
|
contactNumber >= m_numContacts) {
|
|
SE_LOG_DEBUG(NULL, "invalid contact number");
|
|
return false;
|
|
}
|
|
|
|
Content::iterator it;
|
|
SuspendFlags &s = SuspendFlags::getSuspendFlags();
|
|
while ((it = m_content.find(contactNumber)) == m_content.end() &&
|
|
m_session &&
|
|
(!m_session->transferComplete() ||
|
|
m_tmpFile.moreData() ||
|
|
m_transferMaxCount)) {
|
|
// Wait? We rely on regular propgress signals to wake us up.
|
|
// obex 0.47 sends them every 64KB, at least in combination
|
|
// with a Samsung Galaxy SIII. This may depend on both obexd
|
|
// and the phone, so better check ourselves and perhaps do it
|
|
// less often - unmap/map can be expensive and invalidates
|
|
// some of the unread data (at least how it is implemented
|
|
// now).
|
|
while (!m_session->transferComplete() && m_tmpFile.moreData() < 128 * 1024) {
|
|
s.checkForNormal();
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
m_session->checkForError();
|
|
|
|
Timespec completed = m_session->transferComplete();
|
|
if (m_tmpFile.moreData()) {
|
|
// Remap. This shifts all addresses already stored in
|
|
// m_content, so beware and update those.
|
|
pcrecpp::StringPiece oldMem = m_tmpFile.stringPiece();
|
|
m_tmpFile.unmap();
|
|
m_tmpFile.map();
|
|
pcrecpp::StringPiece newMem = m_tmpFile.stringPiece();
|
|
ssize_t delta = newMem.data() - oldMem.data();
|
|
BOOST_FOREACH (Content::value_type &entry, m_content) {
|
|
pcrecpp::StringPiece &vcard = entry.second;
|
|
vcard.set(vcard.data() + delta, vcard.size());
|
|
}
|
|
|
|
// File exists and obexd has written into it, so now we
|
|
// can unlink it to avoid leaking it if we crash.
|
|
m_tmpFile.remove();
|
|
|
|
// Continue parsing where we stopped before.
|
|
pcrecpp::StringPiece next(newMem.data() + m_tmpFileOffset,
|
|
newMem.size() - m_tmpFileOffset);
|
|
const char *end = addVCards(m_contentStartIndex + m_content.size(), next);
|
|
size_t newTmpFileOffset = end - newMem.data();
|
|
SE_LOG_DEBUG(NULL, "PBAP content parsed: %ld out of %d (total), %d out of %d (last update)",
|
|
(long)newTmpFileOffset,
|
|
newMem.size(),
|
|
(int)(end - next.data()),
|
|
next.size());
|
|
m_tmpFileOffset = newTmpFileOffset;
|
|
|
|
if (completed) {
|
|
double duration = (completed - m_transferStart).duration();
|
|
m_lastTransferRate = duration > 0 ? m_tmpFile.size() / duration : 0;
|
|
m_lastContactSizeAverage = m_content.size() ? (double)m_tmpFile.size() / (double)m_content.size() : 0;
|
|
|
|
SE_LOG_DEBUG(NULL, "transferred %ldKB and %ld contacts in %.1fs -> transfer rate %.1fKB/s and %.1fcontacts/s, average contact size %.0fB",
|
|
(long)m_tmpFile.size() / 1024,
|
|
(long)m_content.size(),
|
|
duration,
|
|
m_lastTransferRate / 1024,
|
|
m_content.size() / duration,
|
|
m_lastContactSizeAverage);
|
|
}
|
|
} else if (completed && m_transferMaxCount > 0) {
|
|
// Tune m_desiredMaxCount to achieve the intended transfer
|
|
// time. Ignore clipped or suspended transfers, they are
|
|
// not representative. Also avoid completely bogus
|
|
// observations.
|
|
if (m_pullParams.m_timePerChunk > 0 &&
|
|
!m_wasSuspended &&
|
|
m_transferMaxCount == m_desiredMaxCount &&
|
|
m_lastTransferRate > 0 &&
|
|
m_lastContactSizeAverage > 0) {
|
|
// Use exponential moving average.
|
|
double count = m_lastTransferRate * m_pullParams.m_timePerChunk / m_lastContactSizeAverage;
|
|
double newcount = m_desiredMaxCount * m_pullParams.m_timeLambda +
|
|
count * (1 - m_pullParams.m_timeLambda);
|
|
uint16_t nextcount = (newcount < 0 || newcount > 0xFFFF) ? 0xFFFF : (uint16_t)newcount;
|
|
SE_LOG_DEBUG(NULL, "old max count %u, measured max count for last transfer %.1f, lambda %f, next max count %u",
|
|
m_desiredMaxCount, count, m_pullParams.m_timeLambda, nextcount);
|
|
m_desiredMaxCount = nextcount;
|
|
}
|
|
m_wasSuspended = false;
|
|
if (m_transferOffset + m_transferMaxCount < m_numContacts) {
|
|
// Move one chunk forward.
|
|
m_transferOffset += m_transferMaxCount;
|
|
m_transferMaxCount = std::min((uint16_t)(((m_transferOffset < m_initialOffset) ? m_initialOffset : m_numContacts) - m_transferOffset),
|
|
m_desiredMaxCount);
|
|
} else {
|
|
// Wrap around to offset #0.
|
|
m_transferOffset = 0;
|
|
m_transferMaxCount = std::min(m_initialOffset, m_desiredMaxCount);
|
|
}
|
|
|
|
if (m_transferMaxCount > 0) {
|
|
m_filter["Offset"] = m_transferOffset;
|
|
m_filter["MaxCount"] = m_transferMaxCount;
|
|
|
|
m_tmpFileOffset = 0;
|
|
m_tmpFile.close();
|
|
m_tmpFile.unmap();
|
|
m_tmpFile.create(TmpFile::FILE);
|
|
SE_LOG_DEBUG(NULL, "Created next temporary file for PullAll %s", m_tmpFile.filename().c_str());
|
|
m_contentStartIndex += m_content.size();
|
|
m_content.clear();
|
|
m_session->continuePullAll(*this);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (it == m_content.end()) {
|
|
SE_LOG_DEBUG(NULL, "did not get the expected contact #%d, perhaps some contacts were deleted?",
|
|
contactNumber);
|
|
return false;
|
|
}
|
|
|
|
vcard = it->second;
|
|
|
|
return true;
|
|
}
|
|
|
|
void PbapSession::shutdown(void)
|
|
{
|
|
GDBusCXX::DBusClientCall0 removeSession(*m_client, "RemoveSession");
|
|
|
|
// always clear pointer, even if method call fails
|
|
GDBusCXX::DBusObject_t path(m_session->getPath());
|
|
//m_session.reset();
|
|
SE_LOG_DEBUG(NULL, "removed session: %s", path.c_str());
|
|
|
|
removeSession(path);
|
|
|
|
SE_LOG_DEBUG(NULL, "PBAP session closed");
|
|
}
|
|
|
|
void PbapSession::setFreeze(bool freeze)
|
|
{
|
|
SE_LOG_DEBUG(NULL, "PbapSession::setFreeze(%s, %s)",
|
|
m_currentTransfer.c_str(),
|
|
freeze ? "freeze" : "thaw");
|
|
if (freeze == m_frozen) {
|
|
SE_LOG_DEBUG(NULL, "no change in freeze state");
|
|
return;
|
|
}
|
|
if (m_client.get()) {
|
|
if (m_obexAPI == OBEXD_OLD) {
|
|
SE_THROW("freezing OBEX transfer not possible with old obexd");
|
|
}
|
|
if (!m_currentTransfer.empty()) {
|
|
// Suspend/Resume implemented since Bluez 5.15. If not
|
|
// implemented, we will get a D-Bus exception that is returned
|
|
// to the caller as error, which will abort the sync.
|
|
GDBusCXX::DBusRemoteObject transfer(m_client->getConnection(),
|
|
m_currentTransfer,
|
|
OBC_TRANSFER_INTERFACE_NEW5,
|
|
OBC_SERVICE_NEW5,
|
|
true);
|
|
try {
|
|
if (freeze) {
|
|
GDBusCXX::DBusClientCall0(transfer, "Suspend")();
|
|
} else {
|
|
GDBusCXX::DBusClientCall0(transfer, "Resume")();
|
|
}
|
|
} catch (...) {
|
|
std::string explanation;
|
|
Exception::handle(explanation, HANDLE_EXCEPTION_NO_ERROR);
|
|
|
|
if (m_currentTransfer.empty() || transferComplete()) {
|
|
// Transfer already finished. This causes obexd to report
|
|
// "GDBus.Error:org.freedesktop.DBus.Error.UnknownObject: Method "xxx" with signature "" on interface "org.bluez.obex.Transfer1" doesn't exist."
|
|
//
|
|
// We can ignore any error for suspend/resume when
|
|
// there is no active transfer. The sync engine will
|
|
// handle suspending/resuming the processing of the data.
|
|
SE_LOG_DEBUG(NULL, "ignore error after transfer completed: %s", explanation.c_str());
|
|
} else if (freeze && explanation.find("org.bluez.obex.Error.NotInProgress") != explanation.npos) {
|
|
// Suspending failed because the transfer had not
|
|
// started yet (still queuing), see
|
|
// "org.bluez.obex.Transfer1 Suspend/Resume in
|
|
// queued state" on linux-bluetooth.
|
|
// Ignore this and retry the Suspend when the transfer
|
|
// becomes active.
|
|
SE_LOG_DEBUG(NULL, "must retry Suspend(), got error at the moment: %s", explanation.c_str());
|
|
} else {
|
|
// Have to abort.
|
|
GDBusCXX::DBusClientCall0(transfer, "Cancel")();
|
|
|
|
// Bluez does not change the transfer status when cancelling it,
|
|
// so our propChangedCb() doesn't get called. We need to record
|
|
// the end of the transfer directly to stop the syncing.
|
|
Completion completion = Completion::now();
|
|
completion.m_transferErrorCode = "cancelled";
|
|
completion.m_transferErrorMsg = "transfer cancelled because suspending not possible";
|
|
m_transfers[m_currentTransfer] = completion;
|
|
|
|
Exception::tryRethrow(explanation, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Handle setFreeze() before and after we have a running
|
|
// transfer by setting a flag and checking that flag before
|
|
// initiating a new transfer.
|
|
m_frozen = freeze;
|
|
}
|
|
|
|
void PbapSession::blockOnFreeze()
|
|
{
|
|
SuspendFlags &s = SuspendFlags::getSuspendFlags();
|
|
while (m_frozen) {
|
|
s.checkForNormal();
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
}
|
|
|
|
PbapSyncSource::PbapSyncSource(const SyncSourceParams ¶ms) :
|
|
SyncSource(params)
|
|
{
|
|
SyncSourceSession::init(m_operations);
|
|
m_operations.m_readNextItem = boost::bind(&PbapSyncSource::readNextItem, this, _1, _2, _3);
|
|
m_operations.m_readItemAsKey = boost::bind(&PbapSyncSource::readItemAsKey,
|
|
this, _1, _2);
|
|
m_session = PbapSession::create(*this);
|
|
const char *PBAPSyncMode = getenv("SYNCEVOLUTION_PBAP_SYNC");
|
|
m_PBAPSyncMode = !PBAPSyncMode ? PBAP_SYNC_NORMAL :
|
|
boost::iequals(PBAPSyncMode, "incremental") ? PBAP_SYNC_INCREMENTAL :
|
|
boost::iequals(PBAPSyncMode, "text") ? PBAP_SYNC_TEXT :
|
|
boost::iequals(PBAPSyncMode, "all") ? PBAP_SYNC_NORMAL :
|
|
(throwError(SE_HERE, StringPrintf("invalid value for SYNCEVOLUTION_PBAP_SYNC: %s", PBAPSyncMode)), PBAP_SYNC_NORMAL);
|
|
m_isFirstCycle = true;
|
|
m_hadContacts = false;
|
|
}
|
|
|
|
PbapSyncSource::~PbapSyncSource()
|
|
{
|
|
}
|
|
|
|
void PbapSyncSource::open()
|
|
{
|
|
string database = getDatabaseID();
|
|
const string prefix("obex-bt://");
|
|
|
|
if (!boost::starts_with(database, prefix)) {
|
|
throwError(SE_HERE, "database should specifiy device address (obex-bt://<bt-addr>)");
|
|
}
|
|
|
|
std::string address = database.substr(prefix.size());
|
|
|
|
m_session->initSession(address, getDatabaseFormat());
|
|
}
|
|
|
|
void PbapSyncSource::beginSync(const std::string &lastToken, const std::string &resumeToken)
|
|
{
|
|
if (!lastToken.empty()) {
|
|
throwError(SE_HERE, STATUS_SLOW_SYNC_508, std::string("PBAP cannot do change detection"));
|
|
}
|
|
}
|
|
|
|
std::string PbapSyncSource::endSync(bool success)
|
|
{
|
|
m_pullAll.reset();
|
|
// Non-empty so that beginSync() can detect non-slow syncs and ask
|
|
// for one.
|
|
return "1";
|
|
}
|
|
|
|
bool PbapSyncSource::isEmpty()
|
|
{
|
|
return false; // We don't know for sure. Doesn't matter, so pretend to not be empty.
|
|
}
|
|
|
|
void PbapSyncSource::close()
|
|
{
|
|
m_session->shutdown();
|
|
}
|
|
|
|
void PbapSyncSource::setFreeze(bool freeze)
|
|
{
|
|
if (m_session) {
|
|
m_session->setFreeze(freeze);
|
|
}
|
|
if (m_pullAll) {
|
|
m_pullAll->m_wasSuspended = true;
|
|
}
|
|
}
|
|
|
|
|
|
PbapSyncSource::Databases PbapSyncSource::getDatabases()
|
|
{
|
|
Databases result;
|
|
|
|
result.push_back(Database("select database via bluetooth address",
|
|
"[obex-bt://]<bt-addr>",
|
|
false,
|
|
true));
|
|
return result;
|
|
}
|
|
|
|
void PbapSyncSource::enableServerMode()
|
|
{
|
|
SE_THROW("PbapSyncSource does not implement server mode.");
|
|
}
|
|
|
|
bool PbapSyncSource::serverModeEnabled() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::string PbapSyncSource::getPeerMimeType() const
|
|
{
|
|
return "text/vcard";
|
|
}
|
|
|
|
void PbapSyncSource::getSynthesisInfo(SynthesisInfo &info,
|
|
XMLConfigFragments &fragments)
|
|
{
|
|
// Use vCard 3.0 with minimal conversion by default.
|
|
std::string type = "raw/text/vcard";
|
|
SourceType sourceType = getSourceType();
|
|
if (!sourceType.m_format.empty()) {
|
|
type = sourceType.m_format;
|
|
}
|
|
if (type == "raw/text/vcard") {
|
|
// Raw mode.
|
|
info.m_native = "vCard30";
|
|
info.m_fieldlist = "Raw";
|
|
info.m_profile = "";
|
|
} else {
|
|
// Assume that it's something more traditional requiring parsing.
|
|
info.m_native = "vCard21";
|
|
info.m_fieldlist = "contacts";
|
|
info.m_profile = "\"vCard\", 1";
|
|
}
|
|
info.m_datatypes = getDataTypeSupport(type, sourceType.m_forceFormat);
|
|
|
|
/**
|
|
* Access to data must be done early so that a slow sync can be
|
|
* enforced.
|
|
*/
|
|
info.m_earlyStartDataRead = true;
|
|
}
|
|
|
|
// TODO: return IDs based on GetSize(), read only when engine needs data.
|
|
|
|
sysync::TSyError PbapSyncSource::readNextItem(sysync::ItemID aID,
|
|
sysync::sInt32 *aStatus,
|
|
bool aFirst)
|
|
{
|
|
if (aFirst) {
|
|
PullParams params;
|
|
|
|
params.m_pullData = (m_PBAPSyncMode == PBAP_SYNC_TEXT ||
|
|
(m_PBAPSyncMode == PBAP_SYNC_INCREMENTAL && m_isFirstCycle)) ?
|
|
PULL_WITHOUT_PHOTOS :
|
|
PULL_AS_CONFIGURED;
|
|
|
|
const char *env;
|
|
if ((env = getenv("SYNCEVOLUTION_PBAP_CHUNK_TRANSFER_TIME")) != NULL) {
|
|
params.m_timePerChunk = atof(env);
|
|
} else {
|
|
params.m_timePerChunk = 30;
|
|
}
|
|
static const double LAMBDA_DEF = 0.1;
|
|
if ((env = getenv("SYNCEVOLUTION_PBAP_CHUNK_TIME_LAMBDA")) != NULL) {
|
|
params.m_timeLambda = atof(env);
|
|
} else {
|
|
params.m_timeLambda = LAMBDA_DEF;
|
|
}
|
|
if (params.m_timeLambda < 0 ||
|
|
params.m_timeLambda > 1) {
|
|
params.m_timeLambda = LAMBDA_DEF;
|
|
}
|
|
if ((env = getenv("SYNCEVOLUTION_PBAP_CHUNK_MAX_COUNT_PHOTO")) != NULL) {
|
|
params.m_startMaxCount[true] = atoi(env);
|
|
}
|
|
if ((env = getenv("SYNCEVOLUTION_PBAP_CHUNK_MAX_COUNT_NO_PHOTO")) != NULL) {
|
|
params.m_startMaxCount[false] = atoi(env);
|
|
}
|
|
if ((env = getenv("SYNCEVOLUTION_PBAP_CHUNK_OFFSET")) != NULL) {
|
|
params.m_startOffset = atoi(env);
|
|
} else {
|
|
unsigned int seed = (unsigned int)Timespec::system().seconds();
|
|
// Clip it such that it is >= 0 and < 0x10000.
|
|
params.m_startOffset = rand_r(&seed) % 0x10000;
|
|
}
|
|
|
|
m_pullAll = m_session->startPullAll(params);
|
|
}
|
|
if (!m_pullAll) {
|
|
throwError(SE_HERE, "logic error: readNextItem without aFirst=true before");
|
|
}
|
|
std::string id = m_pullAll->getNextID();
|
|
if (id.empty()) {
|
|
*aStatus = sysync::ReadNextItem_EOF;
|
|
if (m_PBAPSyncMode == PBAP_SYNC_INCREMENTAL &&
|
|
m_hadContacts &&
|
|
m_isFirstCycle) {
|
|
requestAnotherSync();
|
|
m_isFirstCycle = false;
|
|
}
|
|
} else {
|
|
*aStatus = sysync::ReadNextItem_Unchanged;
|
|
aID->item = StrAlloc(id.c_str());
|
|
aID->parent = NULL;
|
|
m_hadContacts = true;
|
|
}
|
|
return sysync::LOCERR_OK;
|
|
}
|
|
|
|
sysync::TSyError PbapSyncSource::readItemAsKey(sysync::cItemID aID, sysync::KeyH aItemKey)
|
|
{
|
|
if (!m_pullAll) {
|
|
throwError(SE_HERE, "logic error: readItemAsKey() without preceeding readNextItem()");
|
|
}
|
|
pcrecpp::StringPiece vcard;
|
|
if (m_pullAll->getContact(aID->item, vcard)) {
|
|
return getSynthesisAPI()->setValue(aItemKey, "itemdata", vcard.data(), vcard.size());
|
|
} else {
|
|
return sysync::DB_NotFound;
|
|
}
|
|
}
|
|
|
|
SyncSourceRaw::InsertItemResult PbapSyncSource::insertItemRaw(const std::string &luid, const std::string &item)
|
|
{
|
|
throwError(SE_HERE, "writing via PBAP is not supported");
|
|
return InsertItemResult();
|
|
}
|
|
|
|
void PbapSyncSource::readItemRaw(const std::string &luid, std::string &item)
|
|
{
|
|
if (!m_pullAll) {
|
|
throwError(SE_HERE, "logic error: readItemRaw() without preceeding readNextItem()");
|
|
}
|
|
pcrecpp::StringPiece vcard;
|
|
if (m_pullAll->getContact(luid.c_str(), vcard)) {
|
|
item.assign(vcard.data(), vcard.size());
|
|
} else {
|
|
throwError(SE_HERE, STATUS_NOT_FOUND, string("retrieving item: ") + luid);
|
|
}
|
|
}
|
|
|
|
SE_END_CXX
|
|
|
|
#endif /* ENABLE_PBAP */
|
|
|
|
#ifdef ENABLE_MODULES
|
|
# include "PbapSyncSourceRegister.cpp"
|
|
#endif
|