On Mon, 2012-09-17 at 11:32 +0200, Patrick Ohly wrote:
> On Wed, 2012-09-12 at 14:51 +0200, Patrick Ohly wrote:
> > I have some partial implementation of the API ready and will push it
> > shortly to the new SyncEvolution git repo on freedesktop.org
> 
> The code is now in the for-master/pim branch, see the
> src/dbus/server/manager directory. The API text is in
> src/dbus/server/pim/pim-manager-api.txt. I decided to avoid using IVI in
> the code because it might turn out to be useful elsewhere.

The code now fully implements the proposed API and has been tested by me
for a while. The nightly testing includes the testpim.py tests. Manual
testing with a Nokia N97 Mini also passed (with the caveats below). This
is a good time to make it available to people who do not want to compile
source from a git repo.

Therefore I have merged the code into the master branch, tagged it as
1.3.99.1 and prepared the usual source .tar.gz:
http://downloads.syncevolution.org/syncevolution/sources/syncevolution-1.3.99.1.tar.gz

The benefits for traditional users are fairly small (except perhaps for
the new caching mode) and the binaries would not contain the IVI
features anyway, because the binaries target EDS < 3.6 and older
distros. For these reasons I am not publishing binaries of 1.3.99.1.

There are a few known issues regarding the PIM Manager in this release:

- updating a contact is inefficient and can cause deadlocks in EDS:
http://bugs.freedesktop.org/show_bug.cgi?id=55925

- some minor memory leaks:
http://bugs.freedesktop.org/show_bug.cgi?id=56396

- in violation of the API spec, colon and upper case characters in the
peer UID are not properly supported and should be avoided for now; I'm
undecided whether I should fix the API to match the implementation or
make the implementation more complicated to support the proposed API:
http://bugs.freedesktop.org/show_bug.cgi?id=56436

A full overview of issues is here:
https://bugs.freedesktop.org/showdependencygraph.cgi?id=55916&display=web&rankdir=TB

I'm attaching the current API revision, a README which gives
SyncEvolution specific information and two example scripts. All of this
is also in the release and in the git repo.

Regarding documentation, I'm still undecided whether specific peer
config options, search capabilities and ordering really should be
specified in the API documentation. I'm inclined to make this completely
implementation specific and thus at best give examples in the API
documentation. App developers need to consult their system documentation
anyway to learn which version of the API is supported by the system.

-- 
Best Regards, Patrick Ohly

The content of this message is my personal opinion only and although
I am an employee of Intel, the statements I make here in no way
represent Intel's position on the issue, nor am I authorized to speak
on behalf of Intel on this matter.

Preamble
========

This text describes a D-Bus API. It might get copied into XML API definitions
in future revisions.

The API implements in-vehicle infotainment (IVI) use cases around
contacts:
- cache address books from peers (primarily phones connected via Bluetooth)
  in local address books
- provide a unified address book that combines a configurable (and changing)
  subset of the local address books
- fast phone number lookup
- browsing and searching in the unified address book

Tasks that are expected to be done by the user of this API:
- identify peers and their capabilities
- decide how and when peer data should be cached
- define which data goes into the unified address book

In other words, the API provides the mechanisms and the user the
policy.

Datatypes
=========

Peers
-----

A peer is an entity which has exactly one address book that is meant
to be cached locally. Typically a peer is a phone connected via
Bluetooth and accessed via PBAP, but it could also be a web service
that supports CardDAV or a phone with SyncML support.

Peers are identified by a unique string ID. That ID needs to be
assigned by the user of this API. The string must not be empty and may
only contain characters a-z, 0-9, hyphen and colon. No other
assumptions about its content are made. For example, the phone's
Bluetooth MAC address could be used.

For an entity that has more than one address book, multiple peers must
be configured.

For each peer, enough information must be provided to access its
address book. That information is passed via D-Bus as a
string-to-string dict, with the following keys:

- "protocol" - defines how to access the address book. Currently
  only "PBAP" is implemented. "SyncML", "CardDAV" are documented
  to illustrate how the API would work for them.
- "transport" - defines how to establish
  the connection. Currently only "Bluetooth" is allowed
  (for protocol=PBAP or SyncML) and taken as default when
  "transport" is not set.
- "address" - the Bluetooth MAC address in the aa:bb:cc:dd:ee:ff
  format (for transport=Bluetooth)
- "database" - empty or unset for the internal address book
  (protocol=PBAP), the URI (protocol=SyncML)

Address books
-------------

Address books which mirror data from a specific peer use the string
"peer-<uid>" as ID, where <uid> is the unique ID of that peer.

In addition, there is a system address book which is independent of
any particular phone. Its ID is the empty string.

This naming scheme can be extended later on, to support other kinds
of address books.

Contact
-------

A single contact is transferred via D-Bus as a string->variant dict
where the keys are predefined property names and the values represent
simple values (a string for "full-name") or more complex structures
(list of phone numbers for "phone-numbers", with each list entry
itself being a combination of type flags and the actual value).

[comment: this mirrors the properties of a libfolks Individual:
http://telepathy.freedesktop.org/doc/folks/c/FolksIndividual.html]

Some properties of a FolksIndividual only make sense locally and are
not transmitted, for example the personas it is derived from.

Some other properties provide information not found that way in
FolksIndividual:
- "source" = list of string pairs, where each pair is a combination
  of address book ID and local contact ID inside that address book
  (not necessarily the same as the vCard UID of a contact!)

Property values which are large (like photos) are not sent via
D-Bus. Instead a link to a local file is sent.

TODO: document all properties and their types.

Search results
--------------

The goal is to support a UI which:
- displays an ordered list of the search result,
- can show the initial results with minimal delay,
- can load actual content for the display as needed (only
  load the parts which are visible or will be visible soon).

The content of the unified address book can change at any time. The
API design takes that into account by using a model/view/controller
model.

The model is the complete list of contacts, sorted according to the
currently configured sort order. Sorting is part of the model to
simplify generating views.

The view is the subset of the data that a user of the API has
requested. In the most extreme case, all contacts are part of the
view. Therefore contact data has to be requested explicitly. To
support requesting data in batches, contacts are numbered 0 to n-1 in
each view, where n is the number of contacts in the view. Sort order
is the same as in the underlying model. Change notifications with
these index numbers are sent as contacts are added, modified or
removed.

The controller is the part of the API which allows changing contacts
in the system address book, changing the sort order, enabling or disabling
address books, etc.

Note that removing or adding a contact changes the numbers assigned to
other contacts. Example:

- A view containing 10 contacts is created.
- A notification about "contacts #0 to #9 added" is sent
  (given as pair of first index and count, not list of numbers).
- Data for contacts #5 to #19 gets requested, five contacts #5 to #9
  are returned. It's okay to ask for more contacts than exist,
  because the caller cannot be sure anyway how many contacts
  still exist at the time when the request gets processed.
- Contact #4 gets removed. The user needs to remember that
  the data that it has now corresponds to contacts #4 to #8.
- Contact #5 gets added, before the contact which had that
  number before. The user now has contacts #4 and #6 to #9.
  It should request contact #5 if (or once) it is needed to
  provide a complete list to the user.

[comment: using a view could be simplified by including contact data
in the change notifications. This is not planned at the moment because
it would not work well for large views. When adding it, there should
be an API to restrict which properties of a contact get sent.]


Error handling
==============

D-Bus error messages are not localized. They are meant for debugging,
not for displaying to the user. In cases where the caller may be able
to do something about an error, specific error codes are defined as
part of the API. However, typically errors are generic and the caller
simply has to assume that the PIM storage is currently unusable.

Unless noted otherwise, calls return when the requested operation is
complete.

The following errors are defined. In addition to the D-Bus name of the
error they provide a textual error description.

org._01.pim.contacts.Manager.Aborted
   Some operation was intentionally aborted instead of letting it
   complete. Typically not an error.

org._01.pim.contacts.Manager.BadStatus
   A generic error report. The error description is a string
   which gives further information for debugging.


API
===

PIM Manager
-----------

The PIM manager is used to hold the unified address book in memory,
create views on it, change configuration and control data transfers
from phones.

Service: org._01.pim.contacts
Interface: org._01.pim.contacts.Manager
Object path: /org/01/pim/contacts

Methods:

    void Start()

         The PIM manager does not start loading contact data right
         away. That allows setting the options like sort order first
         and/or delaying the loading until it is needed. After
         Start(), changing options that affect the unified address
         book will take effect immediately.

         Calling Start() is optional, any method asking for data will
         automatically do that.

    void Stop()

         Explicitly tells the PIM manager to discard the unified address
         book and free up the memory if possible (= not currently in use).
         Primarily useful for testing.

    void SetSortOrder(string mode)

         "mode" must be one of "first/last", "last/first", "full name".
         "first/last" sorts based on the first name stored in the "name"
         property, with the last name used to break ties between equal first
         names.

         "last/first" reverts that comparison.

         "full name" sorts based on the full name chosen for the
         contact if there is such a string, otherwise it uses the
         concatenation of the individual name componts without prefix
         (= "[<first name>] [<middle name>] [<last name>] [<suffix>])"
         as fallback. In other words, it sorts correctly when either
         all contacts have a full name explicitly set or the full
         names that were set following the same pattern as the
         fallback.

         Sorting is case-insensitive.

         The sort order is stored persistently. The default is "last/first".

    string GetSortOrder()

         Returns the current sort order.

    list of strings GetActiveAddressBooks()

         Returns the IDs of the address books which currently
         contribute to the unified address book.

    void SetActiveAddressBooks(list of strings)

         Sets the address books which contribute to the unified
         address book.

    void SetPeer(string uid, dict properties)

         Adds or modifies a peer. Modifying a peer does *not* affect
         any contact data which might be cached for it.

    void RemovePeer(string uid)

         Removes a peer and all its cached data. If that data was
         part of the active address books, it will be removed
         automatically.

    void SyncPeer(string uid)

         Retrieve contacts from the peer and ensure that the local
         cache is identical to the address book of the peer. The call
         returns once the operation is complete. Only if there was no
         error can the caller assume that the cache is up-to-date.
         Otherwise it is in an undefined state.

    void StopSync(string uid)

         Stop any running sync for the given peer. The SyncPeer() method
         which started such a sync will return with an "aborted" error
         once the sync was stopped.

    dict of UID to string key/value dict GetAllPeers()

         Returns information about all currently configured peers.

    object Search(list filter, object agent)

         Creates a new view which contains all contacts matching the
         filter. The call returns the object path of a view object
         after validating parameters and starting the result
         gathering, and before completing the search. The view object
         can be used to control the view via the
         org._01.pim.contacts.ViewControl interface.

         Supported searches:
         [ ] - An empty list matches all contacts.

         [ ['phone', '<number>'] ] - Look up a valid phone number (= "caller 
ID").
         The country code for the current locale is added if no country
         code was given in the number. Phone numbers in the unified address
         book must start with the resulting full number, after being normalized
         the same way.

         In other words:
         - Formatting does not matter.
         - Alpha characters are aliases for numbers on the keypad and match
           their corresponding number.
         - Additional digits in the address book are ignored, only
           the prefix must match (extensions may or may not be included in
           <number>).
         - Phone numbers in the address book which cannot be normalized
           cannot be matched.

         [ ['any-contains', '<text>', <flags>] ] - Sub-string search for <text>
         in the following contact values: first, middle or last name,
         formatted name, nick name, phone number (sub-string search which 
ignores
         formatting and treats alpha characters as aliases, in contrast
         to prefix match in 'phone') or email address. Optional flags include:
         'case-insensitive' (the default), 'case-sensitive'.

         Using list will allow extending the search capabilities later,
         for example by allowing multiple terms which all must match
         and/or adding recursive queries like this:
           ['and',
            ['or', ['any-contains', 'joe'], ['any-contains', 'joan']],
            ['phone', '1234']
           ]

         Notifications for the view are sent back to the caller by
         invoking methods from the org._01.pim.contacts.ViewAgent
         interface on the object whose path is given in the "view"
         parameter. If any of these method calls fail, the view will
         automatically be destroyed.

         In other words, the caller first needs to get ready to process
         results by registering an object on the bus before calling
         Search().

         [comment: this allows sending results to just one recipient,
         something that cannot be done easily with the use of signals as in,
         for example, obexd. In obexd, the initiator of a transfer
         has to subscribe to org.bluez.obex.Transfer on the object path
         returned to it when starting the transfer, then check the current
         status before waiting for signals, because the "Completed" signal
         might have been sent before it could register for it.]

    string AddContact(string addressbook, dict contact)

         Adds a new contact to the given address book. Typically
         only the system address book is writable. Contact properties
         which are unknown or cannot be stored are silently ignored.
         Returns the local ID of the new contact in the address book.

         Photo data that is sent inline in the dict will be split out
         into a file that gets associated with the contact. A photo
         file that gets linked will continue to be owned by the
         caller; the contact storage may or may not make a copy of it,
         depending on which storage is used.

    void ModifyContact(string addressbook, string localid, dict contact)

         Updates an existing contact.

    void RemoveContact(string addressbook, string localid)

         Remove the contact and all of its associated data (like the
         photo, if the photo file is owned by the contact storage).


Service: org._01.pim.contacts
Interface: org._01.pim.contacts.ViewControl
Object path: [variable prefix]/{view0,view1,....}
Methods:

        list of contact dicts ReadContacts(int start, int count)

             Requests at most "count" contacts in the view, starting
             with the one at "start" (numbered starting with 1). May
             return less (or no) contacts if the request range is
             beyond the end of the view at the time when the call is
             processed.

             Note that the caller must process the call response
             after all events via the ViewAgent interface if it wants
             to keep in sync with the view. Doing this call asynchronously
             and dealing with the response as part of the main event
             loop will do the right thing automatically, because D-Bus
             guarantees ordering of messages.

             Making this explicit by returning data via another
             org._01.pim.contacts.ViewAgent method was considered and
             rejected a) for the sake of keeping this API simple and
             b) to allow simple synchronous calls where it makes sense
             (testing, for example).

        void Close()

             Closes the view and all resources associated with it.
             Pending ReadContacts() calls will return without any
             data and no error.

        void RefineSearch(list filter)

             Replaces the current filter of the view with a new one.
             The new filter must be stricter than the old one. Contacts
             which were already filtered out will not be added back
             to the view when setting a less restrictive filter (simplifies
             the implementation and improves performance).


Service: [user of the PIM Manager]
Interface: org._01.pim.contacts.ViewAgent
Object path: [as chosen by user of PIM Manager]

Methods:

        void ContactsModified(object view, int start, int count)

             Contacts #start till #start + count (inclusive) have
             changed. Data that the recipient of the call might have
             cached became invalid and should be reloaded. In cases
             where changing a contact changes its position in the
             sorted list, "contact removed" and "contact added"
             notifications will be triggered instead of a "contact
             changed".

        void ContactsAdded(object view, int start, int count)

             New contacts were added to the view.
             The contact which previously had index #start now
             has index #start + count, etc.

        void ContactsRemoved(object view, int start, int count)

             Some contacts were removed from the view.
             The contact which previous had index #start + count
             (if there was one) now has index #start, etc.
Overview
========

The code in this directory only gets compiled when SyncEvolution is
configured with --enable-dbus-service-pim. It uses libfolks and the
PBAP backend to implement a unified address book. This additional
functionality is provided via the org._01.pim D-Bus API.

That API is independent of libfolks and SyncEvolution. That it is
implemented inside syncevo-dbus-server merely simplifies the
implementation, by reusing some code provided by SyncEvolution
and the core Server class:
  * C++ D-Bus bindings
  * logging
  * D-Bus server life cycle control: delay shut down while
    clients make calls, inhibit shutdown while clients are
    registered, restart when files change on disk during system
    update, ...
  * direct access to SyncEvolution instead having to go through
    the http://api.syncevolution.org D-Bus API

Compilation
===========

Use --enable-dbus-service-pim --enable-pbap --enable-ebook.

--enable-ebook is already the default at the moment. It must not be
  disabled.

The PBAP backend can be disabled. However, then fetching data from
phones via PBAP obviously does not work.


More information
================

Public discussion started here, comments can be sent via the gmane
"reply" feature:
http://comments.gmane.org/gmane.comp.mobile.syncevolution/4009

Issues specific to PIM Manager can be found in this graph:
https://bugs.freedesktop.org/showdependencygraph.cgi?id=55916&display=web&rankdir=TB


Dependencies
============

A fairly recent libfolks > 0.7.4 is required. The 837a88 commit
is required and several other pending changes are recommended:

https://bugzilla.gnome.org/show_bug.cgi?id=686693
writing birthday lacks conversion from UTC

https://bugzilla.gnome.org/show_bug.cgi?id=685401
linking by email

https://bugzilla.gnome.org/show_bug.cgi?id=686695
support nickname in add_persona_from_details

Other requirements:
* Evolution Data Server >= 3.6
* boost::locale and libphonenumber (when using the default
  sorting and searching)
* glib >= 2.30
* Python >= 2.7 (only for testing)


Known issues
============

https://bugzilla.gnome.org/show_bug.cgi?id=652659
Modifying many properties in a contact is inefficient and can deadlock in EDS.


Extending the implementation
============================

Sorting and searching can be replaced with a different implementation
at compile time via --enable-dbus-service-pim[=<locale>]: it uses the
file src/dbus/server/pim/locale-factory-<locale>.cpp to implement
sorting and searching.

Any additional library dependencies for that file need to be added to
the main configure.ac.


Sorting + Searching
===================

The default implementation is based on boost::locale and libphonenumber.
It is documented in pim-manager-api.txt.

Lookup and search are different: the former is based on a valid
number, the later on user input.

A 'phone' lookup can compare normalized numbers including the country
code, to ensure that the lookup is exact and does not mismatch numbers
from different countries. Heuristics like suffix matching do not do
this correctly in all cases.

An 'any-contains' search is based on user input, which might contain
just some digits in the middle of the phone number. The search ignores
formatting in both input and address book.

In both cases, alpha characters are treated as aliases for their
corresponding digit on a keypad when matching phone numbers.

In addition, an empty string as sort order picks a simple, ASCII-based
"last/first" sorting. This is used for testing.


Configuration and data handling
===============================

The configuration of peers is mapped to normal SyncEvolution
configurations. Every peer gets its own context, which contains both
the local source definition for the peer and sync plus target configs.
The special name space prefix "pim-manager-" used to identify them and
avoid potential conflicts with normal SyncEvolution configurations.

Look in ~/.config/syncevolution/pim-manager-* to find the
configuration files.

Local EDS databases are created with a fixed UID and name that also
have that prefix. One could go one step further than it is currently
done and set these databases to "disabled" in the ESourceRegistry,
which would hide them in Evolution.

The SyncEvolution command line can be used to view or manipulate
these databases:
https://syncevolution.org/wiki/item-operations

The persistent configuration of the sort order is stored in
~/.config/syncevolution/pim-manager.ini as the "sort" property. It's
values are the same string as in the API.


Testing
=======

Tests are mostly written in Python. See test/test-dbus.py (base
classes and testing of the normal SyncEvolution D-Bus API) and
src/dbus/server/pim/testpim.py.

These operations manipulate the system address book. To avoid
unexpected data loss when a developer runs the script in his
normal environment, check that testpim.py gets run like this:

  XDG_CONFIG_HOME=`pwd`/temp-testpim/config \
  XDG_DATA_HOME=`pwd`/temp-testpim/local/cache \
  PATH=<SyncEvolution install path>/bin:<SyncEvolution install 
path>/libexec:$PATH \
       <path to SyncEvolution source>/test/dbus-session.sh \
       <path to SyncEvolution source>/src/dbus/server/pim/testpim.py

This will use temp-testpim in addition to temp-testdbus in the current
directory for temporary files.

If TEST_DBUS_PBAP_PHONE is set to the Bluetooth MAC address (like
A0:4E:04:1E:AD:30) of a phone, PBAP syncing with that phone is tested.

The phone must be paired, connected and support SyncML in addition to
PBAP. The test hard-codes Nokia SyncML settings to keep it simpe and
because Nokia phones are most likely to be used.
#! /usr/bin/python -u
# -*- coding: utf-8 -*-
# vim: set fileencoding=utf-8 :#
#
# Copyright (C) 2012 Intel Corporation
#
# 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

# Changes sorting, active address books and/or searches.
# Run with no arguments to see the current state.
#
# Examples:
# search.py --order=last/first
# search.py --order=first/last
# search.py --active-address-book '' --active-address-book 'peer-foobar'
# search.py --search "[]"
# search.py --search "[('any-contains', 'Joe')]"
# search.py --search "[('phone', '+49891234')]"
#
# When searching, the script will print the results as they come in,
# then continue waiting for changes until interrupted via CTRL-C.

import dbus
import dbus.service
import gobject
from dbus.mainloop.glib import DBusGMainLoop
import functools
import sys
import traceback
import itertools
from optparse import OptionParser

parser = OptionParser()
parser.add_option("-a", "--active-address-book", dest="address_books",
                  action="append", default=[],
                  help="Set one active address book, repeat to activate more than one. "
                  "Default is to leave the current set unchanged.",
                  metavar="ADDRESS-BOOK-ID")
parser.add_option("-s", "--search", dest="search",
                  default=None,
                  help="Search expression in Python syntax. "
                  "Default is to not search at all.",
                  metavar="SEARCH-EXPRESSION")
parser.add_option("-o", "--order", dest="order",
                  default=None,
                  help="Set new global sort order. Default is to use the existing one.",
                  metavar="ORDER-NAME")
(options, args) = parser.parse_args()

DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
loop = gobject.MainLoop()

# The example does all calls to D-Bus with a very long timeout.  A
# real app should instead either never time out (because all of the
# calls can, in theory, take arbitrarily long to complete) or be
# prepared to deal with timeouts.
timeout = 100000

# Contact PIM Manager.
manager = dbus.Interface(bus.get_object('org._01.pim.contacts',
                                        '/org/01/pim/contacts'),
                         'org._01.pim.contacts.Manager')

# Simplify the output of values returned via D-Bus by replacing
# types like dbus.Dictionary with a normal Python dictionary
# and by sorting lists (the order of all list entries never matters
# in the contact dictionary).
dbus_type_mapping = {
    dbus.Array: list,
    dbus.Boolean: bool,
    dbus.Byte: int,
    dbus.Dictionary: dict,
    dbus.Double: float,
    dbus.Int16: int,
    dbus.Int32: int,
    dbus.Int64: long,
    dbus.ObjectPath: str,
    dbus.Signature: str,
    dbus.String: unicode,
    dbus.Struct: tuple,
    dbus.UInt16: int,
    dbus.UInt32: int,
    dbus.UInt64: long,
    dbus.UTF8String: unicode
    }

def strip_dbus(instance):
    base = dbus_type_mapping.get(type(instance), None)
    if base == dict or isinstance(instance, dict):
        return dict([(strip_dbus(k), strip_dbus(v)) for k, v in instance.iteritems()])
    if base == list or isinstance(instance, list):
        l = [strip_dbus(v) for v in instance]
        l.sort()
        return l
    if base == tuple or isinstance(instance, tuple):
        return tuple([strip_dbus(v) for v in instance])
    if base == None:
        return instance
    if base == unicode:
        # try conversion to normal string
        try:
            return str(instance)
        except UnicodeEncodeError:
            pass
    return base(instance)

def nothrow(fn):
    '''Function decorator which dumps exceptions to stdout. Use for callbacks.'''

    @functools.wraps(fn)
    def wrapper(*a, **b):
        try:
            fn(*a, **b)
        except:
            print traceback.format_exc()

    return wrapper

class ContactsView(dbus.service.Object):
     '''Implements ViewAgent. Logs changes to stdout and maintains+shows the current content.'''

     def __init__(self):
          '''Create ViewAgent with the chosen path.'''
          # A real app would have to ensure that the path is unique for the
          # process. This example only has one ViewAgent and thus can use
          # a fixed path.
          self.path = '/org/syncevolution/search'
          # Currently known contact data, size matches view.
          self.contacts = []

          dbus.service.Object.__init__(self, dbus.SessionBus(), self.path)

     def search(self, filter):
          '''Start a search.'''
          print 'searching: %s' % filter
          self.viewPath = manager.Search(filter, self.path,
                                         timeout=100000)
          # This example uses the ViewControl to read contact data.
          # It does not close the view explicitly when
          # terminating. Instead it relies on the PIM Manager to
          # detect that the client disconnects from D-Bus.
          # Alternatively a client can also remove only its ViewAgent,
          # which will be noticed by the PIM Manager the next time it
          # tries to send a change.
          self.view = dbus.Interface(bus.get_object(manager.bus_name,
                                                    self.viewPath),
                                     'org._01.pim.contacts.ViewControl')

     def read(self, start, count):
         '''Read contact data which was modified or added.'''
         self.view.ReadContacts(start, count,
                                timeout=100000,
                                reply_handler=lambda x: self.ContactsRead(start, x),
                                error_handler=lambda x: self.ReadFailed(start, count, x))

     def dump(self, start, count):
         '''Show content of view. Highlight the contacts in the given range.'''

         for index, contact in enumerate(self.contacts):
             print
             if start == index:
                 # empty line with marker where range starts
                 print '* '
             print '%s %03d %s' % \
                 (start != None and index >= start and index < start + count and '*' or ' ',
                  index,
                  contact == None and '<<reading...>>' or contact.get('full-name', '<<unnamed>>'))
             print '    ', strip_dbus(contact)

     @nothrow
     def ContactsRead(self, start, contacts):
         print 'got contact data [%d, %d)' % (start, start + len(contacts))
         self.contacts[start : start + len(contacts)] = contacts
         self.dump(start, len(contacts))

     @nothrow
     def ReadFailed(self, start, count, error):
         print 'request for contact data [%d, %d) failed: %s' % \
             (start, start + count, error)

     @nothrow
     @dbus.service.method(dbus_interface='org._01.pim.contacts.ViewAgent',
                          in_signature='oii', out_signature='')
     def ContactsModified(self, view, start, count):
         print 'contacts modified: %s, start %d, count %d' % \
             (view, start, count)
         self.contacts[start:start + count] = itertools.repeat(None, count)
         self.dump(start, count)
         self.read(start, count)

     @nothrow
     @dbus.service.method(dbus_interface='org._01.pim.contacts.ViewAgent',
                          in_signature='oii', out_signature='')
     def ContactsAdded(self, view, start, count):
         print 'contacts added: %s, start %d, count %d' % \
             (view, start, count)
         for i in range(0, count):
             self.contacts.insert(start, None)
         self.dump(start, count)
         self.read(start, count)

     @nothrow
     @dbus.service.method(dbus_interface='org._01.pim.contacts.ViewAgent',
                          in_signature='oii', out_signature='')
     def ContactsRemoved(self, view, start, count):
         print 'contacts removed: %s, start %d, count %d' % \
             (view, start, count)
         # Remove obsolete entries.
         del self.contacts[start:start + count]
         self.dump(start, 0)

     @nothrow
     @dbus.service.method(dbus_interface='org._01.pim.contacts.ViewAgent',
                          in_signature='o', out_signature='')
     def Quiesent(self, view):
         print 'view is stable'

peers = strip_dbus(manager.GetAllPeers())
print 'peers: %s' % peers
print 'available databases: %s' % ([''] + ['peer-' + uid for uid in peers.keys()])

address_books = strip_dbus(manager.GetActiveAddressBooks())
if options.address_books:
    print 'active address books %s -> %s' % (address_books, options.address_books)
    manager.SetActiveAddressBooks(options.address_books)
else:
    print 'active address books: %s' % options.address_books

order = strip_dbus(manager.GetSortOrder())
if options.order:
    print 'active sort order %s -> %s' % (order, options.order)
    manager.SetSortOrder(options.order)
else:
    print 'active sort order: %s' % order

if options.search != None:
    view = ContactsView()
    view.search(eval(options.search))
    loop.run()
else:
    print 'no search expression given, quitting'
#! /usr/bin/python -u
# -*- coding: utf-8 -*-
# vim: set fileencoding=utf-8 :#
#
# Copyright (C) 2012 Intel Corporation
#
# 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

# Configure, sync and/or remove a PBAP-capable phone.
# The peer UID is set to the Bluetooth MAC address.
#
# Examples:
# sync.py --bt-mac A0:4E:04:1E:AD:30 --configure
# sync.py --bt-mac A0:4E:04:1E:AD:30 --sync
# sync.py --bt-mac A0:4E:04:1E:AD:30 --remove
# sync.py --bt-mac A0:4E:04:1E:AD:30 --configure --sync --remove

import dbus
import dbus.service
import gobject
from dbus.mainloop.glib import DBusGMainLoop
import functools
import sys
import traceback
import itertools
from optparse import OptionParser

parser = OptionParser()
parser.add_option("-b", "--bt-mac", dest="mac",
                  default=None,
                  help="Set the Bluetooth MAC address and thus UID of the phone peer.",
                  metavar="aa:bb:cc:dd:ee:ff")
parser.add_option("-c", "--configure",
                  action="store_true", default=False,
                  help="Enable configuring the peer.")
parser.add_option("-s", "--sync",
                  action="store_true", default=False,
                  help="Cache data of peer.")
parser.add_option("-r", "--remove",
                  action="store_true", default=False,
                  help="Remove peer configuration and data.")
(options, args) = parser.parse_args()
if options.configure or options.sync or options.remove:
    if not options.mac:
        sys.exit('--bt-mac parameter must be given')

    # Use MAC address as UID of peer, but with underscores instead of colons
    # and all in lower case. See https://bugs.freedesktop.org/show_bug.cgi?id=56436
    uid = options.mac.replace(':', '_').lower()

DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
loop = gobject.MainLoop()

# The example does all calls to D-Bus with a very long timeout.  A
# real app should instead either never time out (because all of the
# calls can, in theory, take arbitrarily long to complete) or be
# prepared to deal with timeouts.
timeout = 100000

# Contact PIM Manager.
manager = dbus.Interface(bus.get_object('org._01.pim.contacts',
                                        '/org/01/pim/contacts'),
                         'org._01.pim.contacts.Manager')

# Simplify the output of values returned via D-Bus by replacing
# types like dbus.Dictionary with a normal Python dictionary
# and by sorting lists (the order of all list entries never matters
# in the contact dictionary).
dbus_type_mapping = {
    dbus.Array: list,
    dbus.Boolean: bool,
    dbus.Byte: int,
    dbus.Dictionary: dict,
    dbus.Double: float,
    dbus.Int16: int,
    dbus.Int32: int,
    dbus.Int64: long,
    dbus.ObjectPath: str,
    dbus.Signature: str,
    dbus.String: unicode,
    dbus.Struct: tuple,
    dbus.UInt16: int,
    dbus.UInt32: int,
    dbus.UInt64: long,
    dbus.UTF8String: unicode
    }

def strip_dbus(instance):
    base = dbus_type_mapping.get(type(instance), None)
    if base == dict or isinstance(instance, dict):
        return dict([(strip_dbus(k), strip_dbus(v)) for k, v in instance.iteritems()])
    if base == list or isinstance(instance, list):
        l = [strip_dbus(v) for v in instance]
        l.sort()
        return l
    if base == tuple or isinstance(instance, tuple):
        return tuple([strip_dbus(v) for v in instance])
    if base == None:
        return instance
    if base == unicode:
        # try conversion to normal string
        try:
            return str(instance)
        except UnicodeEncodeError:
            pass
    return base(instance)


peers = strip_dbus(manager.GetAllPeers())
print 'peers: %s' % peers
print 'available databases: %s' % ([''] + ['peer-' + uid for uid in peers.keys()])

if options.configure:
    peer = {'protocol': 'PBAP',
            'address': options.mac}
    print 'adding peer config %s = %s' % (uid, peer)
    manager.SetPeer(uid, peer)

if options.sync:
    # Give the operation plenty of time to complete...
    print 'syncing peer %s' % uid
    manager.SyncPeer(uid, timeout=100000)

if options.remove:
    print 'removing peer %s' % uid
    manager.RemovePeer(uid)
_______________________________________________
SyncEvolution mailing list
[email protected]
http://lists.syncevolution.org/listinfo/syncevolution

Reply via email to