Imported Upstream version 1.3.99.4
[platform/upstream/syncevolution.git] / src / backends / pbap / PbapSyncSource.cpp
1 /*
2  * Copyright (C) 2007-2009 Patrick Ohly <patrick.ohly@gmx.de>
3  * Copyright (C) 2009 Intel Corporation
4  * Copyright (C) 2012 BMW Car IT GmbH. All rights reserved.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) version 3.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19  * 02110-1301  USA
20  */
21
22 #include "config.h"
23
24 #ifdef ENABLE_PBAP
25
26 #include "PbapSyncSource.h"
27
28 #include <boost/assign/list_of.hpp>
29 #include <boost/algorithm/string/case_conv.hpp>
30 #include <boost/tokenizer.hpp>
31
32 #include <errno.h>
33 #include <unistd.h>
34
35 #include <pcrecpp.h>
36 #include <algorithm>
37
38 #include <syncevo/GLibSupport.h> // PBAP backend does not compile without GLib.
39 #include <syncevo/util.h>
40 #include <syncevo/BoostHelper.h>
41
42 #include "gdbus-cxx-bridge.h"
43
44 #include <boost/algorithm/string/predicate.hpp>
45 #include <boost/bind.hpp>
46
47 #include <synthesis/SDK_util.h>
48
49 #include <syncevo/SyncContext.h>
50 #include <syncevo/declarations.h>
51 SE_BEGIN_CXX
52
53 #define OBC_SERVICE "org.openobex.client" // obexd < 0.47
54 #define OBC_SERVICE_NEW "org.bluez.obex.client" // obexd >= 0.47
55 #define OBC_SERVICE_NEW5 "org.bluez.obex" // obexd in Bluez 5.0
56 #define OBC_CLIENT_INTERFACE "org.openobex.Client"
57 #define OBC_CLIENT_INTERFACE_NEW "org.bluez.obex.Client"
58 #define OBC_CLIENT_INTERFACE_NEW5 "org.bluez.obex.Client1"
59 #define OBC_PBAP_INTERFACE "org.openobex.PhonebookAccess"
60 #define OBC_PBAP_INTERFACE_NEW "org.bluez.obex.PhonebookAccess"
61 #define OBC_PBAP_INTERFACE_NEW5 "org.bluez.obex.PhonebookAccess1"
62 #define OBC_TRANSFER_INTERFACE_NEW "org.bluez.obex.Transfer"
63 #define OBC_TRANSFER_INTERFACE_NEW5 "org.bluez.obex.Transfer1"
64
65 typedef std::map<int, pcrecpp::StringPiece> Content;
66
67 class PullAll
68 {
69     std::string m_buffer; // vCards kept in memory when using old obexd.
70     TmpFile m_tmpFile; // Stored in temporary file and mmapped with more recent obexd.
71     Content m_content; // Refers to chunks of m_buffer or m_tmpFile without copying them.
72     int m_numContacts; // Number of existing contacts, according to GetSize() or after downloading.
73     int m_currentContact; // Numbered starting with zero according to discovery in addVCards.
74     boost::shared_ptr<PbapSession> m_session; // Only set when there is a transfer ongoing.
75     int m_tmpFileOffset; // Number of bytes already parsed.
76
77     friend class PbapSession;
78 public:
79     std::string getNextID();
80     bool getContact(int contactNumber, pcrecpp::StringPiece &vcard);
81     const char *addVCards(int startIndex, const pcrecpp::StringPiece &content);
82 };
83
84 enum PullData
85 {
86     PULL_AS_CONFIGURED,
87     PULL_WITHOUT_PHOTOS
88 };
89
90 class PbapSession : private boost::noncopyable {
91 public:
92     static boost::shared_ptr<PbapSession> create(PbapSyncSource &parent);
93
94     void initSession(const std::string &address, const std::string &format);
95
96     typedef std::map<std::string, pcrecpp::StringPiece> Content;
97     typedef std::map<std::string, boost::variant<std::string> > Params;
98
99     boost::shared_ptr<PullAll> startPullAll(PullData pullata);
100     void checkForError(); // Throws exception if transfer failed.
101     Timespec transferComplete() const;
102     void resetTransfer();
103     void shutdown(void);
104
105 private:
106     PbapSession(PbapSyncSource &parent);
107
108     PbapSyncSource &m_parent;
109     boost::weak_ptr<PbapSession> m_self;
110     std::auto_ptr<GDBusCXX::DBusRemoteObject> m_client;
111     enum {
112         OBEXD_OLD, // obexd < 0.47
113         OBEXD_NEW, // obexd == 0.47, file-based transfer
114         // OBEXD_048 // obexd == 0.48, file-based transfer without SetFilter and with filter parameter to PullAll()
115         BLUEZ5     // obexd in Bluez >= 5.0
116     } m_obexAPI;
117
118     /** filter parameters for BLUEZ5 PullAll */
119     typedef std::list<std::string> Properties;
120     typedef boost::variant< std::string, Properties > Bluez5Values;
121     std::map<std::string, Bluez5Values> m_filter5;
122     Properties m_filterFields;
123     Properties supportedProperties() const;
124
125     /**
126      * m_transferComplete will be set to the current monotonic time when observing a
127      * "Complete" signal on a transfer object path which has the
128      * current session as prefix. There may be more than one such transfer,
129      * so record all completions that we see and then pick the right one.
130      *
131      * It also gets set when an error occurred for such a transfer,
132      * in which case m_error will also be set.
133      *
134      * This only works as long as the session is only used for a
135      * single transfer. Otherwise a more complex tracking of
136      * completion, for example per transfer object path, is needed.
137      */
138     class Completion {
139     public:
140         Timespec m_transferComplete;
141         std::string m_transferErrorCode;
142         std::string m_transferErrorMsg;
143
144         static Completion now() {
145             Completion res;
146             res.m_transferComplete = Timespec::monotonic();
147             return res;
148         }
149     };
150     typedef std::map<std::string, Completion> Transfers;
151     Transfers m_transfers;
152     std::string m_currentTransfer;
153
154     std::auto_ptr<GDBusCXX::SignalWatch3<GDBusCXX::Path_t, std::string, std::string> >
155         m_errorSignal;
156     void errorCb(const GDBusCXX::Path_t &path, const std::string &error,
157                  const std::string &msg);
158
159     // Bluez 5
160     typedef GDBusCXX::SignalWatch4<GDBusCXX::Path_t, std::string, Params, std::vector<std::string> > PropChangedSignal_t;
161     std::auto_ptr<PropChangedSignal_t> m_propChangedSignal;
162     void propChangedCb(const GDBusCXX::Path_t &path,
163                        const std::string &interface,
164                        const Params &changed,
165                        const std::vector<std::string> &invalidated);
166
167     // new obexd API
168     typedef GDBusCXX::SignalWatch1<GDBusCXX::Path_t> CompleteSignal_t;
169     std::auto_ptr<CompleteSignal_t> m_completeSignal;
170     void completeCb(const GDBusCXX::Path_t &path);
171     typedef GDBusCXX::SignalWatch3<GDBusCXX::Path_t, std::string, boost::variant<int64_t> > PropertyChangedSignal_t;
172     std::auto_ptr<PropertyChangedSignal_t> m_propertyChangedSignal;
173     void propertyChangedCb(const GDBusCXX::Path_t &path, const std::string &name, const boost::variant<int64_t> &value);
174
175     std::auto_ptr<GDBusCXX::DBusRemoteObject> m_session;
176 };
177
178 PbapSession::PbapSession(PbapSyncSource &parent) :
179     m_parent(parent)
180 {
181 }
182
183 boost::shared_ptr<PbapSession> PbapSession::create(PbapSyncSource &parent)
184 {
185     boost::shared_ptr<PbapSession> session(new PbapSession(parent));
186     session->m_self = session;
187     return session;
188 }
189
190 void PbapSession::propChangedCb(const GDBusCXX::Path_t &path,
191                                 const std::string &interface,
192                                 const Params &changed,
193                                 const std::vector<std::string> &invalidated)
194 {
195     // Called for a path which matches the current session, so we know
196     // that the signal is for our transfer. Only need to check the status.
197     Params::const_iterator it = changed.find("Status");
198     if (it != changed.end()) {
199         std::string status = boost::get<std::string>(it->second);
200         SE_LOG_DEBUG(NULL, "OBEXD transfer %s: %s",
201                      path.c_str(), status.c_str());
202         Completion completion = Completion::now();
203         SE_LOG_DEBUG(NULL, "obexd transfer %s: %s", path.c_str(), status.c_str());
204         if (status == "error") {
205             // We have to make up some error descriptions. The Bluez
206             // 5 API no longer seems to provide that.
207             completion.m_transferErrorCode = "transfer failed";
208             completion.m_transferErrorMsg = "reason unknown";
209         }
210         m_transfers[path] = completion;
211     }
212 }
213
214 void PbapSession::completeCb(const GDBusCXX::Path_t &path)
215 {
216     SE_LOG_DEBUG(NULL, "obexd transfer %s completed", path.c_str());
217     m_transfers[path] = Completion::now();
218 }
219
220 void PbapSession::errorCb(const GDBusCXX::Path_t &path,
221                           const std::string &error,
222                           const std::string &msg)
223 {
224     SE_LOG_DEBUG(NULL, "obexd transfer %s failed: %s %s",
225                  path.c_str(), error.c_str(), msg.c_str());
226     Completion &completion = m_transfers[path];
227     completion.m_transferComplete = Timespec::monotonic();
228     completion.m_transferErrorCode = error;
229     completion.m_transferErrorMsg = msg;
230 }
231
232 void PbapSession::propertyChangedCb(const GDBusCXX::Path_t &path,
233                                     const std::string &name,
234                                     const boost::variant<int64_t> &value)
235 {
236     const int64_t *tmp = boost::get<int64_t>(&value);
237     if (tmp) {
238         SE_LOG_DEBUG(NULL, "obexd transfer %s property change: %s = %ld",
239                      path.c_str(), name.c_str(), (long signed)*tmp);
240     } else {
241         SE_LOG_DEBUG(NULL, "obexd transfer %s property change: %s",
242                      path.c_str(), name.c_str());
243     }
244 }
245
246 PbapSession::Properties PbapSession::supportedProperties() const
247 {
248     Properties props;
249     static const std::set<std::string> supported =
250         boost::assign::list_of("VERSION")
251         ("FN")
252         ("N")
253         ("PHOTO")
254         ("BDAY")
255         ("ADR")
256         ("LABEL")
257         ("TEL")
258         ("EMAIL")
259         ("MAILER")
260         ("TZ")
261         ("GEO")
262         ("TITLE")
263         ("ROLE")
264         ("LOGO")
265         ("AGENT")
266         ("ORG")
267         ("NOTE")
268         ("REV")
269         ("SOUND")
270         ("URL")
271         ("UID")
272         ("KEY")
273         ("NICKNAME")
274         ("CATEGORIES")
275         ("CLASS");
276
277     BOOST_FOREACH (const std::string &prop, m_filterFields) {
278         // Be conservative and only ask for properties that we
279         // really know how to use. obexd also lists the bit field
280         // strings ("BIT01") but phones have been seen to reject
281         // queries when those were enabled.
282         if (supported.find(prop) != supported.end()) {
283             props.push_back(prop);
284         }
285     }
286     return props;
287 }
288
289 void PbapSession::initSession(const std::string &address, const std::string &format)
290 {
291     if (m_session.get()) {
292         return;
293     }
294
295     // format string uses:
296     // [(2.1|3.0):][^]propname,propname,...
297     //
298     // 3.0:^PHOTO = download in vCard 3.0 format, excluding PHOTO
299     // 2.1:PHOTO = download in vCard 2.1 format, only the PHOTO
300
301     std::string version;
302     std::string tmp;
303     std::string properties;
304     const pcrecpp::RE re("(?:(2\\.1|3\\.0):?)?(\\^?)([-a-zA-Z,]*)");
305     if (!re.FullMatch(format, &version, &tmp, &properties)) {
306         m_parent.throwError(StringPrintf("invalid specification of PBAP vCard format (databaseFormat): %s",
307                                          format.c_str()));
308     }
309     char negated = tmp.c_str()[0];
310     if (version.empty()) {
311         // same default as in obexd
312         version = "2.1";
313     }
314     if (version != "2.1" && version != "3.0") {
315         m_parent.throwError(StringPrintf("invalid vCard version prefix in PBAP vCard format specification (databaseFormat): %s",
316                                          format.c_str()));
317     }
318     std::set<std::string> keywords;
319     boost::split(keywords, properties, boost::is_from_range(',', ','));
320     typedef std::map<std::string, boost::variant<std::string> > Params;
321
322     Params params;
323     params["Target"] = std::string("PBAP");
324
325     std::string session;
326     GDBusCXX::DBusConnectionPtr conn = GDBusCXX::dbus_get_bus_connection("SESSION", NULL, true, NULL);
327
328     // We must attempt to use the new interface(s), otherwise we won't know whether
329     // the daemon exists or can be started.
330     m_obexAPI = BLUEZ5;
331     m_client.reset(new GDBusCXX::DBusRemoteObject(conn,
332                                                   "/org/bluez/obex",
333                                                   OBC_CLIENT_INTERFACE_NEW5,
334                                                   OBC_SERVICE_NEW5, true));
335     try {
336         SE_LOG_DEBUG(NULL, "trying to use bluez 5 obexd service %s", OBC_SERVICE_NEW5);
337         session =
338             GDBusCXX::DBusClientCall1<GDBusCXX::DBusObject_t>(*m_client, "CreateSession")(address, params);
339     } catch (const std::exception &error) {
340         if (!strstr(error.what(), "org.freedesktop.DBus.Error.ServiceUnknown") &&
341             !strstr(error.what(), "org.freedesktop.DBus.Error.UnknownObject")) {
342             throw;
343         }
344         // Fall back to old interface.
345         SE_LOG_DEBUG(NULL, "bluez obex service not available (%s), falling back to previous obexd one %s",
346                      error.what(),
347                      OBC_SERVICE_NEW);
348         m_obexAPI = OBEXD_NEW;
349     }
350
351     if (session.empty()) {
352         m_client.reset(new GDBusCXX::DBusRemoteObject(conn, "/", OBC_CLIENT_INTERFACE_NEW,
353                                                       OBC_SERVICE_NEW, true));
354         try {
355             SE_LOG_DEBUG(NULL, "trying to use new obexd service %s", OBC_SERVICE_NEW);
356             session =
357                 GDBusCXX::DBusClientCall1<GDBusCXX::DBusObject_t>(*m_client, "CreateSession")(address, params);
358         } catch (const std::exception &error) {
359             if (!strstr(error.what(), "org.freedesktop.DBus.Error.ServiceUnknown")) {
360                 throw;
361             }
362             // Fall back to old interface.
363             SE_LOG_DEBUG(NULL, "new obexd service(s) not available (%s), falling back to old one %s",
364                          error.what(),
365                          OBC_SERVICE);
366             m_obexAPI = OBEXD_OLD;
367         }
368     }
369
370     if (session.empty()) {
371         m_client.reset(new GDBusCXX::DBusRemoteObject(conn, "/", OBC_CLIENT_INTERFACE,
372                                                       OBC_SERVICE, true));
373         params["Destination"] = std::string(address);
374         session = GDBusCXX::DBusClientCall1<GDBusCXX::DBusObject_t>(*m_client, "CreateSession")(params);
375     }
376
377     if (session.empty()) {
378         m_parent.throwError("PBAP: failed to create session");
379     }
380
381     if (m_obexAPI != OBEXD_OLD) {
382         m_session.reset(new GDBusCXX::DBusRemoteObject(m_client->getConnection(),
383                                                        session,
384                                                        m_obexAPI == BLUEZ5 ? OBC_PBAP_INTERFACE_NEW5 : OBC_PBAP_INTERFACE_NEW,
385                                                        m_obexAPI == BLUEZ5 ? OBC_SERVICE_NEW5 : OBC_SERVICE_NEW,
386                                                        true));
387         
388         // Filter Transfer signals via path prefix. Discussions on Bluez
389         // list showed that this is meant to be possible, even though the
390         // client-api.txt documentation itself didn't (and still doesn't)
391         // make it clear:
392         // "[PATCH obexd v0] client-doc: Guarantee prefix in transfer paths"
393         // http://www.spinics.net/lists/linux-bluetooth/msg28409.html
394         //
395         // Be extra careful with asynchronous callbacks: bind to weak
396         // pointer and ignore callback when the instance is already gone.
397         // Should not happen with signals (destructing the class unregisters
398         // the watch), but very well may happen in asynchronous method
399         // calls. Therefore maintain m_self and show how to use it here.
400         if (m_obexAPI == BLUEZ5) {
401             // Bluez 5
402             m_propChangedSignal.reset(new PropChangedSignal_t
403                                       (GDBusCXX::SignalFilter(m_client->getConnection(),
404                                                               session,
405                                                               "org.freedesktop.DBus.Properties",
406                                                               "PropertiesChanged",
407                                                               GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
408             m_propChangedSignal->activate(boost::bind(&PbapSession::propChangedCb, m_self, _1, _2, _3, _4));
409         } else {
410             // obexd >= 0.47
411             m_completeSignal.reset(new CompleteSignal_t
412                                    (GDBusCXX::SignalFilter(m_client->getConnection(),
413                                                            session,
414                                                            OBC_TRANSFER_INTERFACE_NEW,
415                                                            "Complete",
416                                                            GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
417             m_completeSignal->activate(boost::bind(&PbapSession::completeCb, m_self, _1));
418
419             // same for error
420             m_errorSignal.reset(new GDBusCXX::SignalWatch3<GDBusCXX::Path_t, std::string, std::string>
421                                 (GDBusCXX::SignalFilter(m_client->getConnection(),
422                                                         session,
423                                                         OBC_TRANSFER_INTERFACE_NEW,
424                                                         "Error",
425                                                         GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
426             m_errorSignal->activate(boost::bind(&PbapSession::errorCb, m_self, _1, _2, _3));
427
428             // and property changes
429             m_propertyChangedSignal.reset(new PropertyChangedSignal_t(GDBusCXX::SignalFilter(m_client->getConnection(),
430                                                                                              session,
431                                                                                              OBC_TRANSFER_INTERFACE_NEW,
432                                                                                              "PropertyChanged",
433                                                                                              GDBusCXX::SignalFilter::SIGNAL_FILTER_PATH_PREFIX)));
434             m_propertyChangedSignal->activate(boost::bind(&PbapSession::propertyChangedCb, m_self, _1, _2, _3));
435         }
436     } else {
437         // obexd < 0.47
438         m_session.reset(new GDBusCXX::DBusRemoteObject(m_client->getConnection(),
439                                                    session,
440                                                    OBC_PBAP_INTERFACE,
441                                                    OBC_SERVICE,
442                                                    true));
443     }
444
445     SE_LOG_DEBUG(NULL, "PBAP session created: %s", m_session->getPath());
446
447     // get filter list so that we can continue validating our format specifier
448     m_filterFields = GDBusCXX::DBusClientCall1< Properties >(*m_session, "ListFilterFields")();
449     SE_LOG_DEBUG(NULL, "supported PBAP filter fields:\n    %s",
450                  boost::join(m_filterFields, "\n    ").c_str());
451
452     Properties filter;
453     if (negated) {
454         // negated, start with everything set
455         filter = supportedProperties();
456     }
457
458     // validate parameters and update filter
459     BOOST_FOREACH (const std::string &prop, keywords) {
460         if (prop.empty()) {
461             continue;
462         }
463
464         Properties::const_iterator entry =
465             std::find_if(m_filterFields.begin(),
466                          m_filterFields.end(),
467                          boost::bind(&boost::iequals<std::string,std::string>, _1, prop, std::locale()));
468
469         if (entry == m_filterFields.end()) {
470             m_parent.throwError(StringPrintf("invalid property name in PBAP vCard format specification (databaseFormat): %s",
471                                              prop.c_str()));
472         }
473
474         if (negated) {
475             filter.remove(*entry);
476         } else {
477             filter.push_back(*entry);
478         }
479     }
480
481     GDBusCXX::DBusClientCall0(*m_session, "Select")(std::string("int"), std::string("PB"));
482     m_filter5["Format"] = version == "2.1" ? "vcard21" : "vcard30";
483     m_filter5["Fields"] = filter;
484
485     SE_LOG_DEBUG(NULL, "PBAP session initialized");
486 }
487
488 boost::shared_ptr<PullAll> PbapSession::startPullAll(PullData pullData)
489 {
490     resetTransfer();
491
492     // Update prepared filter to match pullData.
493     std::map<std::string, Bluez5Values> currentFilter = m_filter5;
494     std::string &format = boost::get<std::string>(currentFilter["Format"]);
495     std::list<std::string> &filter = boost::get< std::list<std::string> >(currentFilter["Fields"]);
496     switch (pullData) {
497     case PULL_AS_CONFIGURED:
498         SE_LOG_DEBUG(NULL, "pull all with configured filter: '%s'",
499                      boost::join(filter, " ").c_str());
500         break;
501     case PULL_WITHOUT_PHOTOS:
502         // Remove PHOTO from list or create list with the other properties.
503         if (filter.empty()) {
504             filter = supportedProperties();
505         }
506         for (Properties::iterator it = filter.begin();
507              it != filter.end();
508              ++it) {
509             if (*it == "PHOTO") {
510                 filter.erase(it);
511                 break;
512             }
513         }
514         SE_LOG_DEBUG(NULL, "pull all without photos: '%s'",
515                      boost::join(filter, " ").c_str());
516         break;
517     }
518
519     if (m_obexAPI == OBEXD_OLD ||
520         m_obexAPI == OBEXD_NEW) {
521         GDBusCXX::DBusClientCall0(*m_session, "SetFilter")(filter);
522         GDBusCXX::DBusClientCall0(*m_session, "SetFormat")(format);
523     }
524
525     boost::shared_ptr<PullAll> state(new PullAll);
526     state->m_currentContact = 0;
527     if (m_obexAPI != OBEXD_OLD) {
528         // Beware, this will lead to a "Complete" signal in obexd
529         // 0.47. We need to be careful with looking at the right
530         // transfer to determine whether PullAll completed.
531         state->m_numContacts = GDBusCXX::DBusClientCall1<uint16>(*m_session, "GetSize")();
532         SE_LOG_DEBUG(NULL, "Expecting %d contacts.", state->m_numContacts);
533
534         state->m_tmpFile.create();
535         SE_LOG_DEBUG(NULL, "Created temporary file for PullAll %s", state->m_tmpFile.filename().c_str());
536         GDBusCXX::DBusClientCall1<std::pair<GDBusCXX::DBusObject_t, Params> > pullall(*m_session, "PullAll");
537         std::pair<GDBusCXX::DBusObject_t, Params> tuple =
538             m_obexAPI == OBEXD_NEW ?
539             GDBusCXX::DBusClientCall1<std::pair<GDBusCXX::DBusObject_t, Params> >(*m_session, "PullAll")(state->m_tmpFile.filename()) :
540             GDBusCXX::DBusClientCall2<GDBusCXX::DBusObject_t, Params>(*m_session, "PullAll")(state->m_tmpFile.filename(), currentFilter);
541         const GDBusCXX::DBusObject_t &transfer = tuple.first;
542         const Params &properties = tuple.second;
543         m_currentTransfer = transfer;
544         SE_LOG_DEBUG(NULL, "pullall transfer path %s, %ld properties", transfer.c_str(), (long)properties.size());
545         // Work will be finished incrementally in PullAll::getContact().
546         //
547         // In the meantime we return IDs by simply enumerating the expected ones.
548         // If we don't get as many contacts as expected, we return 404 in getContact()
549         // and the Synthesis engine will ignore the ID (src/sysync/binfileimplds.cpp:
550         // "Record does not exist any more in database%s -> ignore").
551         state->m_tmpFileOffset = 0;
552         state->m_session = m_self.lock();
553     } else {
554         GDBusCXX::DBusClientCall1<std::string> pullall(*m_session, "PullAll");
555         state->m_buffer = pullall();
556         state->addVCards(0, state->m_buffer);
557         state->m_numContacts = state->m_content.size();
558     }
559     return state;
560 }
561
562 const char *PullAll::addVCards(int startIndex, const pcrecpp::StringPiece &vcards)
563 {
564     pcrecpp::StringPiece vcarddata;
565     pcrecpp::StringPiece tmp = vcards;
566     int count = startIndex;
567     pcrecpp::RE re("[\\r\\n]*(^BEGIN:VCARD.*?^END:VCARD)",
568                    pcrecpp::RE_Options().set_dotall(true).set_multiline(true));
569     while (re.Consume(&tmp, &vcarddata)) {
570         m_content[count] = vcarddata;
571         ++count;
572     }
573
574     SE_LOG_DEBUG(NULL, "PBAP content parsed: %d entries starting at %d", count - startIndex, startIndex);
575     return tmp.data();
576 }
577
578 void PbapSession::checkForError()
579 {
580     Transfers::const_iterator it = m_transfers.find(m_currentTransfer);
581     if (it != m_transfers.end()) {
582         if (!it->second.m_transferErrorCode.empty()) {
583             m_parent.throwError(StringPrintf("%s: %s",
584                                              it->second.m_transferErrorCode.c_str(),
585                                              it->second.m_transferErrorMsg.c_str()));
586         }
587     }
588 }
589
590 Timespec PbapSession::transferComplete() const
591 {
592     Timespec res;
593     Transfers::const_iterator it = m_transfers.find(m_currentTransfer);
594     if (it != m_transfers.end()) {
595         res = it->second.m_transferComplete;
596     }
597     return res;
598 }
599
600 void PbapSession::resetTransfer()
601 {
602     m_transfers.clear();
603 }
604
605 std::string PullAll::getNextID()
606 {
607     std::string id;
608     if (m_currentContact < m_numContacts) {
609         id = StringPrintf("%d", m_currentContact);
610         m_currentContact++;
611     }
612     return id;
613 }
614
615 bool PullAll::getContact(int contactNumber, pcrecpp::StringPiece &vcard)
616 {
617     SE_LOG_DEBUG(NULL, "get PBAP contact #%d", contactNumber);
618     if (contactNumber < 0 ||
619         contactNumber >= m_numContacts) {
620         SE_LOG_DEBUG(NULL, "invalid contact number");
621         return false;
622     }
623
624     Content::iterator it;
625     while ((it = m_content.find(contactNumber)) == m_content.end() &&
626            m_session &&
627            (!m_session->transferComplete() ||
628             m_tmpFile.moreData())) {
629         // Wait? We rely on regular propgress signals to wake us up.
630         // obex 0.47 sends them every 64KB, at least in combination
631         // with a Samsung Galaxy SIII. This may depend on both obexd
632         // and the phone, so better check ourselves and perhaps do it
633         // less often - unmap/map can be expensive and invalidates
634         // some of the unread data (at least how it is implemented
635         // now).
636         while (!m_session->transferComplete() && m_tmpFile.moreData() < 128 * 1024) {
637             g_main_context_iteration(NULL, true);
638         }
639         m_session->checkForError();
640         if (m_tmpFile.moreData()) {
641             // Remap. This shifts all addresses already stored in
642             // m_content, so beware and update those.
643             pcrecpp::StringPiece oldMem = m_tmpFile.stringPiece();
644             m_tmpFile.unmap();
645             m_tmpFile.map();
646             pcrecpp::StringPiece newMem = m_tmpFile.stringPiece();
647             ssize_t delta = newMem.data() - oldMem.data();
648             BOOST_FOREACH (Content::value_type &entry, m_content) {
649                 pcrecpp::StringPiece &vcard = entry.second;
650                 vcard.set(vcard.data() + delta, vcard.size());
651             }
652
653             // File exists and obexd has written into it, so now we
654             // can unlink it to avoid leaking it if we crash.
655             m_tmpFile.remove();
656
657             // Continue parsing where we stopped before.
658             pcrecpp::StringPiece next(newMem.data() + m_tmpFileOffset,
659                                       newMem.size() - m_tmpFileOffset);
660             const char *end = addVCards(m_content.size(), next);
661             int newTmpFileOffset = end - newMem.data();
662             SE_LOG_DEBUG(NULL, "PBAP content parsed: %d out of %d (total), %d out of %d (last update)",
663                          newTmpFileOffset,
664                          newMem.size(),
665                          (int)(end - next.data()),
666                          next.size());
667             m_tmpFileOffset = newTmpFileOffset;
668         }
669     }
670
671     if (it == m_content.end()) {
672         SE_LOG_DEBUG(NULL, "did not get the expected contact #%d, perhaps some contacts were deleted?", contactNumber);
673         return false;
674     }
675     vcard = it->second;
676     return true;
677 }
678
679 void PbapSession::shutdown(void)
680 {
681     GDBusCXX::DBusClientCall0 removeSession(*m_client, "RemoveSession");
682
683     // always clear pointer, even if method call fails
684     GDBusCXX::DBusObject_t path(m_session->getPath());
685     //m_session.reset();
686     SE_LOG_DEBUG(NULL, "removed session: %s", path.c_str());
687
688     removeSession(path);
689
690     SE_LOG_DEBUG(NULL, "PBAP session closed");
691 }
692
693 PbapSyncSource::PbapSyncSource(const SyncSourceParams &params) :
694     SyncSource(params)
695 {
696     SyncSourceSession::init(m_operations);
697     m_operations.m_readNextItem = boost::bind(&PbapSyncSource::readNextItem, this, _1, _2, _3);
698     m_operations.m_readItemAsKey = boost::bind(&PbapSyncSource::readItemAsKey,
699                                                this, _1, _2);
700     m_session = PbapSession::create(*this);
701     const char *PBAPSyncMode = getenv("SYNCEVOLUTION_PBAP_SYNC");
702     m_PBAPSyncMode = !PBAPSyncMode ? PBAP_SYNC_NORMAL :
703         boost::iequals(PBAPSyncMode, "incremental") ? PBAP_SYNC_INCREMENTAL :
704         boost::iequals(PBAPSyncMode, "text") ? PBAP_SYNC_TEXT :
705         boost::iequals(PBAPSyncMode, "all") ? PBAP_SYNC_NORMAL :
706         (throwError(StringPrintf("invalid value for SYNCEVOLUTION_PBAP_SYNC: %s", PBAPSyncMode)), PBAP_SYNC_NORMAL);
707     m_isFirstCycle = true;
708     m_hadContacts = false;
709 }
710
711 PbapSyncSource::~PbapSyncSource()
712 {
713 }
714
715 void PbapSyncSource::open()
716 {
717     string database = getDatabaseID();
718     const string prefix("obex-bt://");
719
720     if (!boost::starts_with(database, prefix)) {
721         throwError("database should specifiy device address (obex-bt://<bt-addr>)");
722     }
723
724     std::string address = database.substr(prefix.size());
725
726     m_session->initSession(address, getDatabaseFormat());
727 }
728
729 void PbapSyncSource::beginSync(const std::string &lastToken, const std::string &resumeToken)
730 {
731     if (!lastToken.empty()) {
732         throwError(STATUS_SLOW_SYNC_508, std::string("PBAP cannot do change detection"));
733     }
734 }
735
736 std::string PbapSyncSource::endSync(bool success)
737 {
738     m_pullAll.reset();
739     // Non-empty so that beginSync() can detect non-slow syncs and ask
740     // for one.
741     return "1";
742 }
743
744 bool PbapSyncSource::isEmpty()
745 {
746     return false; // We don't know for sure. Doesn't matter, so pretend to not be empty.
747 }
748
749 void PbapSyncSource::close()
750 {
751     m_session->shutdown();
752 }
753
754 PbapSyncSource::Databases PbapSyncSource::getDatabases()
755 {
756     Databases result;
757
758     result.push_back(Database("select database via bluetooth address",
759                               "[obex-bt://]<bt-addr>"));
760     return result;
761 }
762
763 void PbapSyncSource::enableServerMode()
764 {
765     SE_THROW("PbapSyncSource does not implement server mode.");
766 }
767
768 bool PbapSyncSource::serverModeEnabled() const
769 {
770     return false;
771 }
772
773 std::string PbapSyncSource::getPeerMimeType() const
774 {
775     return "text/vcard";
776 }
777
778 void PbapSyncSource::getSynthesisInfo(SynthesisInfo &info,
779                                       XMLConfigFragments &fragments)
780 {
781     // We send vCards in either 2.1 or 3.0. The Synthesis engine
782     // handles both when parsing, so we don't have to be exact here.
783     info.m_native = "vCard21";
784     info.m_fieldlist = "contacts";
785     info.m_profile = "\"vCard\", 1";
786
787     // vCard 3.0 is the more sane format for exchanging with the
788     // Synthesis peer in a local sync, so use that by default.
789     std::string type = "text/vcard";
790     SourceType sourceType = getSourceType();
791     if (!sourceType.m_format.empty()) {
792         type = sourceType.m_format;
793     }
794     info.m_datatypes = getDataTypeSupport(type, sourceType.m_forceFormat);
795
796     /**
797      * Access to data must be done early so that a slow sync can be
798      * enforced.
799      */
800     info.m_earlyStartDataRead = true;
801 }
802
803 // TODO: return IDs based on GetSize(), read only when engine needs data.
804
805 sysync::TSyError PbapSyncSource::readNextItem(sysync::ItemID aID,
806                                   sysync::sInt32 *aStatus,
807                                   bool aFirst)
808 {
809     if (aFirst) {
810         m_pullAll = m_session->startPullAll((m_PBAPSyncMode == PBAP_SYNC_TEXT ||
811                                              (m_PBAPSyncMode == PBAP_SYNC_INCREMENTAL && m_isFirstCycle)) ? PULL_WITHOUT_PHOTOS :
812                                             PULL_AS_CONFIGURED);
813     }
814     if (!m_pullAll) {
815         throwError("logic error: readNextItem without aFirst=true before");
816     }
817     std::string id = m_pullAll->getNextID();
818     if (id.empty()) {
819         *aStatus = sysync::ReadNextItem_EOF;
820         if (m_PBAPSyncMode == PBAP_SYNC_INCREMENTAL &&
821             m_hadContacts &&
822             m_isFirstCycle) {
823             requestAnotherSync();
824             m_isFirstCycle = false;
825         }
826     } else {
827         *aStatus = sysync::ReadNextItem_Unchanged;
828         aID->item = StrAlloc(id.c_str());
829         aID->parent = NULL;
830         m_hadContacts = true;
831     }
832     return sysync::LOCERR_OK;
833 }
834
835 sysync::TSyError PbapSyncSource::readItemAsKey(sysync::cItemID aID, sysync::KeyH aItemKey)
836 {
837     if (!m_pullAll) {
838         throwError("logic error: readItemAsKey() without preceeding readNextItem()");
839     }
840     pcrecpp::StringPiece vcard;
841     if (m_pullAll->getContact(atoi(aID->item), vcard)) {
842         return getSynthesisAPI()->setValue(aItemKey, "data", vcard.data(), vcard.size());
843     } else {
844         return sysync::DB_NotFound;
845     }
846 }
847
848 SyncSourceRaw::InsertItemResult PbapSyncSource::insertItemRaw(const std::string &luid, const std::string &item)
849 {
850     throwError("writing via PBAP is not supported");
851     return InsertItemResult();
852 }
853
854 void PbapSyncSource::readItemRaw(const std::string &luid, std::string &item)
855 {
856     if (!m_pullAll) {
857         throwError("logic error: readItemRaw() without preceeding readNextItem()");
858     }
859     pcrecpp::StringPiece vcard;
860     if (m_pullAll->getContact(atoi(luid.c_str()), vcard)) {
861         item.assign(vcard.data(), vcard.size());
862     } else {
863         throwError(STATUS_NOT_FOUND, string("retrieving item: ") + luid);
864     }
865 }
866
867 SE_END_CXX
868
869 #endif /* ENABLE_PBAP */
870
871 #ifdef ENABLE_MODULES
872 # include "PbapSyncSourceRegister.cpp"
873 #endif