Changeset 2053
- Timestamp:
- 05/27/07 04:34:59 (19 months ago)
- Location:
- plugins/moto-sync
- Files:
-
- 2 modified
-
motosync.py (modified) (31 diffs)
-
mototool (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
plugins/moto-sync/motosync.py
r1809 r2053 266 266 exception dates, exception rules, and the event's date/time, 267 267 returns the motorola recurrence type and a list of exceptions. 268 268 269 269 The general approach we take is: if the recursion can be represented 270 270 by the phone's data structure, we convert it, otherwise we ignore the 271 271 rule completely (to avoid the event showing up at incorrect times). 272 272 273 273 FIXME: this code doesn't yet handle all the tricky parts of RRULE 274 274 specifications (such as BYSETPOS) 275 275 """ 276 276 277 277 # XXX FIXME: whole function needs reworkign for new XML format 278 278 return (MOTO_REPEAT_NONE, []) 279 279 280 280 # can't support multiple rules 281 281 if len(rulenodes) != 1: … … 397 397 class PhoneComms: 398 398 """Functions for directly accessing the phone. 399 399 400 400 "device" may be either a path to a local device node, or a bluetooth MAC. 401 401 """ … … 415 415 def connect(self): 416 416 """Connect to the phone and initiate communication. 417 417 418 418 Returns True on success, False if already connected. 419 419 """ … … 445 445 self.__do_cmd('ATE0Q0V1') # echo off, result codes off, verbose results 446 446 447 # use ISO 8859-1 encoding for data values, for easier debugging 448 # FIXME: change to UCS2? 449 self.__do_cmd('AT+CSCS="8859-1"') 447 # use UCS2 encoding for data values 448 # this is an older version of UTF16 where every char is two bytes long 449 # the phone implements it by sending us 2 hex chars per byte, ie. 4 per char 450 self.__do_cmd('AT+CSCS="UCS2"') 450 451 451 452 (maxevs, numevs, namelen, max_except, _) = self.read_event_params() … … 460 461 self.contact_data_len = contactlen 461 462 self.contact_name_len = namelen 462 463 463 464 return True 464 465 … … 518 519 def read_event_params(self): 519 520 """Read calendar/datebook parameters. 520 521 521 522 Returns: maximum number of events, 522 523 number of events currently stored, … … 565 566 exceptions = evdata[-1] 566 567 data = evdata[:-1] 567 placeholders = self.__make_placeholders(data) 568 self.__do_cmd(('AT+MDBW=' + placeholders) % tuple(data)) 568 # HACK: only the name of the event (data[1]) should be unicode 569 for n in range(2, len(data)): 570 if type(data[n]) == types.UnicodeType: 571 data[n] = data[n].encode('ascii') 572 self.__do_cmd('AT+MDBW=' + self.__to_cmd_str(data)) 569 573 for expos in exceptions: 570 574 self.__do_cmd('AT+MDBWE=%d,%d,1' % (pos, expos)) … … 596 600 def read_contact_params(self): 597 601 """Read phonebook parameters. 598 602 599 603 Returns: minimum position, 600 604 maximum position, … … 647 651 """write a single contact to the position specified in the data list""" 648 652 self.close_calendar() 649 placeholders = self.__make_placeholders(data) 650 self.__do_cmd(('AT+MPBW=' + placeholders) % tuple(data)) 653 # HACK: the email/number and birthday (index 1&23) must not be unicode 654 for n in [1, 23]: 655 if len(data) > n and type(data[n]) == types.UnicodeType: 656 data[n] = data[n].encode('ascii') 657 self.__do_cmd('AT+MPBW=' + self.__to_cmd_str(data)) 651 658 652 659 def delete_contact(self, pos): … … 681 688 If it succeeds, return lines as a list; otherwise raise an exception. 682 689 """ 683 cmd = cmd.encode('iso_8859_1')684 690 opensync.trace(opensync.TRACE_SENSITIVE, '--> ' + cmd) 685 691 ret = [] … … 728 734 valparts = [] 729 735 for part in parts: 730 if part[0] == '"': 736 if part == '': 737 valparts.append('') 738 elif part[0] == '"': 731 739 assert(part[-1] == '"') 732 valparts.append(part[1:-1] .decode('iso_8859_1'))740 valparts.append(part[1:-1]) 733 741 elif part[0] == '(': 734 742 # parse a range string like '(1-10,45,50-60)' … … 739 747 ranges = ranges[0] 740 748 valparts.append(ranges) 749 elif len(part) >= 4 and part[0] == '0': 750 # this looks like a UCS2-encoded string 751 valparts.append(part.decode('hex').decode('utf_16_be')) 741 752 else: 742 753 try: … … 748 759 return ret 749 760 750 def __make_placeholders(self, vals): 751 """Return string expandion placeholders ("%s",%d etc) for a write.""" 761 def __to_cmd_str(self, vals): 762 """Convert different typed data to a string that can be used in a phone 763 write command (ie. MPBW or MDBW.""" 752 764 def make_placeholder(val): 753 765 """Return a single placeholder based on the given value's type.""" … … 755 767 if t == types.IntType: 756 768 return '%d' 769 elif t == types.StringType or val == '': 770 return '"%s"' 771 elif t == types.UnicodeType: 772 return '%s' 757 773 else: 758 assert(t == types.StringType or t == types.UnicodeType, 'unexpected type %s' % str(t)) 759 return '"%s"' 760 761 return ','.join(map(make_placeholder, vals)) 774 assert(False, 'unexpected type %s' % str(t)) 775 776 def convert_val(val): 777 """Convert a value to an alternate representation, if needed.""" 778 if type(val) == types.UnicodeType: 779 # convert to the phone's idea of UCS2 780 # FIXME: this breaks in the case of surrogate pairs, but I doubt 781 # they'll turn up in PIM data, and it's not clear what the phone 782 # actually does support as its character set 783 return val.encode('utf_16_be').encode('hex').upper() 784 else: 785 return val 786 787 return ','.join(map(make_placeholder, vals)) % tuple(map(convert_val, vals)) 762 788 763 789 … … 765 791 class PhoneEntry: 766 792 """(abstract) base class representing an event/contact entry 767 793 768 794 data is kept roughly as it is stored in the phone 769 795 """ 770 796 def __init__(self): 771 797 pass 772 798 773 799 def get_objtype(self): 774 800 """return the opensync object type string""" … … 901 927 else: 902 928 dtstart = self.eventdt.strftime(VCAL_DATE) 903 e.setAttribute(' DateValue', 'DATE')929 e.setAttribute('Value', 'DATE') 904 930 appendXMLChild(doc, e, 'Content', dtstart) 905 931 top.appendChild(e) … … 911 937 else: 912 938 dtend = endtime.strftime(VCAL_DATE) 913 e.setAttribute(' DateValue', 'DATE')939 e.setAttribute('Value', 'DATE') 914 940 appendXMLChild(doc, e, 'Content', dtend) 915 941 top.appendChild(e) … … 927 953 e = doc.createElement('RecurrenceRule') 928 954 if self.repeat_type == MOTO_REPEAT_DAILY: 929 appendXMLChild(doc, e, ' Content', 'DAILY')955 appendXMLChild(doc, e, 'Frequency', 'DAILY') 930 956 elif self.repeat_type == MOTO_REPEAT_WEEKLY: 931 appendXMLChild(doc, e, ' Content', 'WEEKLY')957 appendXMLChild(doc, e, 'Frequency', 'WEEKLY') 932 958 elif self.repeat_type == MOTO_REPEAT_MONTHLY_DATE: 933 appendXMLChild(doc, e, ' Content', 'MONTHLY')959 appendXMLChild(doc, e, 'Frequency', 'MONTHLY') 934 960 appendXMLChild(doc, e, 'ByMonthDay', str(self.eventdt.day)) 935 961 elif self.repeat_type == MOTO_REPEAT_MONTHLY_DAY: 936 appendXMLChild(doc, e, ' Content', 'MONTHLY')962 appendXMLChild(doc, e, 'Frequency', 'MONTHLY') 937 963 day = VCAL_DAYS[self.eventdt.weekday()] 938 964 # compute the week number that the event falls in … … 943 969 appendXMLChild(doc, e, 'ByDay', '%d%s' % (weeknum, day)) 944 970 elif self.repeat_type == MOTO_REPEAT_YEARLY: 945 appendXMLChild(doc, e, ' Content', 'YEARLY')971 appendXMLChild(doc, e, 'Frequency', 'YEARLY') 946 972 appendXMLChild(doc, e, 'ByMonth', str(self.eventdt.month)) 947 973 appendXMLChild(doc, e, 'ByMonthDay', str(self.eventdt.day)) … … 976 1002 # generate ExceptionDate nodes for them 977 1003 e = doc.createElement('ExceptionDateTime') 978 e.setAttribute(' DateValue', 'DATE')1004 e.setAttribute('Value', 'DATE') 979 1005 for exnum in self.exceptions: 980 1006 appendXMLChild(doc, e, 'Content', format_time(rrule[exnum], VCAL_DATE)) … … 1019 1045 def __init__(self, data): 1020 1046 """Parse XML event data. 1021 1047 1022 1048 This function raises UnsupportedDataError for: 1023 1049 * events that recur but can't be represented on the phone … … 1065 1091 1066 1092 # for some reason I don't understand, the phone only allows events 1067 # longer than a day if the time flag is set. pander to this by forcing 1093 # longer than a day if the time flag is set. pander to this by forcing 1068 1094 # such events to start at midnight local time 1069 1095 if isinstance(self.eventdt, date) and self.duration > timedelta(1): … … 1250 1276 class PhoneContactMoto(PhoneContact): 1251 1277 """PhoneContact object for contacts created from phone data. 1252 1278 1253 1279 Creates a contact with a single child.""" 1254 1280 def __init__(self, data): … … 1435 1461 return (self.pos, self.contact, self.numtype, self.parent.name, 1436 1462 self.contacttype, self.voicetag, self.ringerid, 0, 1437 int(self.primaryflag), self.parent.categorynum, 1463 int(self.primaryflag), self.parent.categorynum, 1438 1464 self.profile_icon, self.parent.firstlast_enabled, 1439 1465 self.parent.firstlast_index, self.picture_path, … … 1489 1515 class PosAllocator: 1490 1516 """Position-allocator class. 1491 1517 1492 1518 Remembers which positions in a set are used/free, and allocates new ones. 1493 1519 """ … … 1526 1552 class PhoneAccess: 1527 1553 """Grab-bag class of utility functions. 1528 1554 1529 1555 Interfaces between PhoneComms/PhoneEntry classes and SyncClass below. 1530 1556 """ … … 1615 1641 def delete_entry(self, uid): 1616 1642 """Delete an event with the given UID 1617 1643 1618 1644 Returns True on success, False otherwise. 1619 1645 """ … … 1629 1655 def update_entry(self, change, context): 1630 1656 """Update an entry or add a new one, from the OSyncChange object. 1631 1657 1632 1658 Returns True on success, False otherwise. 1633 1659 """ … … 1676 1702 change.uid = self.__generate_uid(entry) 1677 1703 change.hash = self.__gen_hash(entry) 1678 1704 1679 1705 return True 1680 1706 … … 1711 1737 def __uid_to_pos(self, uid): 1712 1738 """Reverse the generate_uid function above. 1713 1739 1714 1740 Also checks that it is one of ours. 1715 1741 """ … … 1718 1744 lastpos = lastpart.rindex('@') 1719 1745 assert(lastpart[lastpos + 1:] == self.serial[-8:], 'Entry not created on this phone') 1720 1746 1721 1747 if objtype == "event": 1722 1748 positions = PhoneEvent.unpack_uid(lastpart[:lastpos]) -
plugins/moto-sync/mototool
r1809 r2053 53 53 """Prompt the user if they are trying to write to the phone.""" 54 54 if options.mode == 'delete' or options.mode == 'restore': 55 print ('WARNING: About to %s all %s entries fromthe phone!'55 print ('WARNING: About to %s all %s entries on the phone!' 56 56 % (options.mode, ' & '.join(options.objtype))) 57 57 print 'Are you sure? [yn] ', … … 67 67 strings.append(str(val)) 68 68 else: 69 strings.append('"%s"' % val.encode('utf7')) 70 assert('"' not in val) 69 if val == '': 70 strings.append('') 71 else: 72 s = val.encode('utf8') 73 # escape \ to \\, " to \" and newline to \n 74 s = s.replace('\\', '\\\\') 75 s = s.replace('"', '\\"') 76 s = s.replace('\n', '\\n') 77 strings.append('"' + s + '"') 71 78 return ','.join([typestr] + strings) + '\n' 72 79 73 80 def unpack_backup(line): 74 81 """reverse the pack_backup function above""" 82 # FIXME: is the unescaping sane with arbitrary utf8 characters? 75 83 if line[-1] == '\n': 76 84 line = line[:-1] … … 79 87 wasquote = False 80 88 inquote = False 89 inescape = False 81 90 for c in line: 82 91 if c == ',' and not inquote: 83 if not wasquote and len(parts) > 0 :92 if not wasquote and len(parts) > 0 and nextpart != '': 84 93 nextpart = int(nextpart) 94 if wasquote: 95 nextpart = nextpart.decode('utf8') 85 96 parts.append(nextpart) 86 97 nextpart = '' 87 98 wasquote = False 99 elif c == '\\' and inquote and not inescape: 100 inescape = True 101 elif inquote and inescape: 102 inescape = False 103 if c == 'n': 104 c = '\n' 105 else: 106 assert(c == '"' or c == '\\') 107 nextpart = nextpart + c 88 108 elif c == '"': 89 109 wasquote = True … … 91 111 else: 92 112 nextpart = nextpart + c 93 if wasquote :94 parts.append(nextpart.decode('utf 7'))113 if wasquote or nextpart == '': 114 parts.append(nextpart.decode('utf8')) 95 115 else: 96 116 parts.append(int(nextpart))
