next up previous contents
Next: A record-based conduit Up: KPilot conduit programming tutorial Previous: A very simple conduit:   Contents


A per-file conduit: docconduit

The PalmDoc conduit of KPilot takes a directory of text files and syncronized them with PalmDOC databases on the handheld. These PalmDOC documents can be read with AportisDoc, TealReader, and modified with applications like QED. Optionally, the conduit can also keep local copies of the pdb handheld databases in a local directory.

The conduit just needs to find out if a document has changed either on the handheld or on the pc (or on both sides), and then copy the text either to or from the handheld. The docconduit has a class DOCConverter which does the actual conversion. You only have to set the local path to the text file, and give a pointer to an opened PilotDatabase (either PilotLocalDatabase or PilotSerialDatabase), and then call docconverter.convertPDBtoDOC(); or docconverter.convertDOCtoPDB();. I will not explain this class here, but rather the algorithm to determine the sync direction and the actual calls of the DOCConverter.

The conduit has to find out

To assure a responsive user interface, we will once again use QTimer::singleShot(this, 0, SLOT(whatever())); for each of these steps.

The DOCConduit::exec() function is just the entry point and calls syncNextDB, which will go through all PalmDOC databases on the handheld and determine if any of them has been changed:

/* virtual */ bool DOCConduit::exec() {
  FUNCTIONSETUP;
  readConfig();
  dbnr=0;
  QTimer::singleShot(0, this, SLOT(syncNextDB()));
  return true;
}

syncNextDB then walks through all PalmDoc databases on the handheld and decides if they are supposed to be synced to the PC. The function needsSync (which we will describe later), checks which files have actually changed or were added or deleted and so determines the sync direction. The docSyncInfo is just an internal structure to store all information about the text:

void DOCConduit::syncNextDB() {
  FUNCTIONSETUP;
  DBInfo dbinfo;

  if (eSyncDirection==eSyncPCToPDA || fHandle->findDatabase(NULL, &dbinfo, dbnr, dbtype(), dbcreator() /*, cardno */ ) < 0) {
    // no more databases available, so check for PC->Palm sync
    QTimer::singleShot(0, this, SLOT(syncNextDOC()));
    return;
  }
  dbnr=dbinfo.index+1;
  DEBUGCONDUIT<<"Next Palm database to sync: "<<dbinfo.name<<", Index="<<dbinfo.index<<endl;

  // if creator and/or type don't match, go to next db
  if (!isCorrectDBTypeCreator(dbinfo) || fDBNames.contains(dbinfo.name)) {
    QTimer::singleShot(0, this, SLOT(syncNextDB()));
    return;
  }

  QString docfilename=constructDOCFileName(dbinfo.name);
  QString pdbfilename=constructPDBFileName(dbinfo.name);

  docSyncInfo syncInfo(dbinfo.name, docfilename, pdbfilename, eSyncNone);
  syncInfo.dbinfo=dbinfo;
  needsSync(syncInfo);
  fSyncInfoList.append(syncInfo);
  fDBNames.append(dbinfo.name);
  
  QTimer::singleShot(0, this, SLOT(syncNextDB()));
  return;
}

To go through all .txt files on disk, we use a QStringList::Iterator, again set the fields of the docSyncInfo for each text, and call needsSync to do the actual comparison of the local and handheld text to the versions of the previous sync. If a local copy of the pdb files should be kept, we proceed similar using the slot checkPDBFiles:

void DOCConduit::syncNextDOC() {
  FUNCTIONSETUP;
  
  if (eSyncDirection==eSyncPDAToPC  ) {
    // We don't sync from PC to PDB, so start the conflict resolution and then the actual sync process
    docnames.clear();
    QTimer::singleShot(0, this, SLOT(checkPDBFiles()));
    return;
  }
  
  // if docnames isn't initialized, get a list of all *.txt files in fDOCDir
  if (docnames.isEmpty()/* || dociterator==docnames.end() */) {
    docnames=QDir(fDOCDir, "*.txt").entryList() ;
    dociterator=docnames.begin();
  }
  if (dociterator==docnames.end()) {
    // no more databases available, so start the conflict resolution and then the actual sync proces
    docnames.clear();
    QTimer::singleShot(0, this, SLOT(checkPDBFiles()));
    return;
  }

  QString fn=(*dociterator);

  QDir dr(fDOCDir);
  QFileInfo fl(dr, fn );
  QString docfilename=fl.absFilePath();
  QString pdbfilename;
  dociterator++;
  
  DBInfo dbinfo;
  // Include all "extensions" except the last. This allows full stops inside the database name (e.g. abbreviations)
  // first fill everything with 0, so we won't have a buffer overflow.
  memset(&dbinfo.name[0], 0, 33);
  strncpy(&dbinfo.name[0], fl.baseName(TRUE), 30);

  bool alreadySynced=fDBNames.contains(dbinfo.name);
  if (!alreadySynced) {
    docSyncInfo syncInfo(dbinfo.name, docfilename, pdbfilename, eSyncNone);
    syncInfo.dbinfo=dbinfo;
    needsSync(syncInfo);
    fSyncInfoList.append(syncInfo);
    fDBNames.append(dbinfo.name);
  }
  
  QTimer::singleShot(0, this, SLOT(syncNextDOC()));
  return;
}


/** This slot will only be used if fKeepPDBLocally to check if new doc databases have been copied to the pdb directory.
 *  If so, install it to the handheld and sync it to the PC */
void DOCConduit::checkPDBFiles() {
  FUNCTIONSETUP;
  
  if (fLocalSync || !fKeepPDBLocally || eSyncDirection==eSyncPCToPDA )
  {
    // no more databases available, so check for PC->Palm sync
    QTimer::singleShot(0, this, SLOT(resolve()));
    return;
  }
  
  // Walk through all files in the pdb directory and check if it has already been synced.
  // if docnames isn't initialized, get a list of all *.pdb files in fPDBDir
  if (docnames.isEmpty()/* || dociterator==docnames.end() */) {
    docnames=QDir(fPDBDir, "*.pdb").entryList() ;
    dociterator=docnames.begin();
  }
  if (dociterator==docnames.end()) {
    // no more databases available, so start the conflict resolution and then the actual sync proces
    docnames.clear();
    QTimer::singleShot(0, this, SLOT(resolve()));
    return;
  }

  QString fn=(*dociterator);

  QDir dr(fPDBDir);
  QFileInfo fl(dr, fn );
  QString pdbfilename=fl.absFilePath();
  dociterator++;
  
  //  Get the doc title and check if it has already been synced (in the synced docs list of in fDBNames to be synced)
  // If the doc title doesn't appear in either list, install it to the Handheld, and add it to the list of dbs to be synced.
  QString dbname=fl.baseName(TRUE).left(30);
  if (!fDBNames.contains(dbname) && !fDBListSynced.contains(dbname)) {
    if (fHandle->installFiles(pdbfilename )) {
      DBInfo dbinfo;
      // Include all "extensions" except the last. This allows full stops inside the database name (e.g. abbreviations)
      // first fill everything with 0, so we won't have a buffer overflow.
      memset(&dbinfo.name[0], 0, 33);
      strncpy(&dbinfo.name[0], dbname, 30);

      docSyncInfo syncInfo(dbinfo.name, constructDOCFileName(dbname), pdbfilename, eSyncNone);
      syncInfo.dbinfo=dbinfo;
      needsSync(syncInfo);
      fSyncInfoList.append(syncInfo);
      fDBNames.append(dbinfo.name);
    } else {
      DEBUGCONDUIT<<"Could not install database "<<dbname<<" ("<<pdbfilename<<") to the handheld"<<endl;
    }
  }
  
  QTimer::singleShot(0, this, SLOT(checkPDBFiles()));
}

After all databases have been identified, we possibly need to do some conflict resolution in the slot resolve(). The conflict resolution dialog just displays the list of databases and lets the user choose the sync direction for each database. When the user presses Ok, the direction field of each docSyncInfo object is set to the chosen value.

void DOCConduit::resolve() {
  FUNCTIONSETUP;
  
  for (fSyncInfoListIterator=fSyncInfoList.begin(); fSyncInfoListIterator!=fSyncInfoList.end(); fSyncInfoListIterator++) {
    // Walk through each database and apply the conflictResolution option. 
    // the remaining conflicts will be resolved in the resolution dialog
    if ((*fSyncInfoListIterator).direction==eSyncConflict){
      DEBUGCONDUIT<<"We have a conflict for "<<(*fSyncInfoListIterator).handheldDB<<", default="<<eConflictResolution<<endl;
      switch (eConflictResolution)
      {
        case eSyncPDAToPC:
          DEBUGCONDUIT<<"PDA overrides for database "<<(*fSyncInfoListIterator).handheldDB<<endl;
          (*fSyncInfoListIterator).direction = eSyncPDAToPC;
          break;
        case eSyncPCToPDA:
          DEBUGCONDUIT<<"PC overrides for database "<<(*fSyncInfoListIterator).handheldDB<<endl;
          (*fSyncInfoListIterator).direction = eSyncPCToPDA;
          break;
        case eSyncNone:
          DEBUGCONDUIT<<"No sync for database "<<(*fSyncInfoListIterator).handheldDB<<endl;
          (*fSyncInfoListIterator).direction = eSyncNone;
          break;
        case eSyncDelete:
        case eSyncConflict:
        default:
          DEBUGCONDUIT<<"Conflict remains due to default resolution setting for database "<<(*fSyncInfoListIterator).handheldDB<<endl;
          break;
      }
    }
  }
  
  // Show the conflict resolution dialog and ask for the action for each database
  ResolutionDialog*dlg=new ResolutionDialog( 0,  i18n("Conflict Resolution"), &fSyncInfoList , fHandle);
  bool show=fAlwaysUseResolution || (dlg && dlg->hasConflicts);
  if (show) {
    if (!dlg || !dlg->exec() ) {
      KPILOT_DELETE(dlg)
      emit logMessage(i18n("Sync aborted by user."));
      QTimer::singleShot(0, this, SLOT(cleanup()));
      return;
    }
  }
  KPILOT_DELETE(dlg)
  

  // fDBNames will be filled with the names of the databases that are actually synced (not deleted), so I can write the list to the config file
  fDBNames.clear();
  fSyncInfoListIterator=fSyncInfoList.begin();
  QTimer::singleShot(0,this, SLOT(syncDatabases()));
  return;
}

Finally, the actual sync of the databases is done again with QTimer::singleShots in the slot syncDatabases(). Each entry in the list is processed in one pass of syncDatabases, and then syncDatabases is again called using a QTimer::singleShot, until all databases have been synced.

void DOCConduit::syncDatabases() {
  FUNCTIONSETUP;
  if (fSyncInfoListIterator==fSyncInfoList.end()) {
    QTimer::singleShot(0, this, SLOT(cleanup()));
    return;
  }
  
  docSyncInfo sinfo=(*fSyncInfoListIterator);
  fSyncInfoListIterator++;
  
  switch (sinfo.direction) {
    case eSyncConflict:
      DEBUGCONDUIT<<"Entry "<<sinfo.handheldDB<<"( docfilename: "<<sinfo.docfilename<<
        ", pdbfilename: "<<sinfo.pdbfilename<<") had sync direction eSyncConflict!!!"<<endl;
      break;
    case eSyncDelete:
    case eSyncPDAToPC:
    case eSyncPCToPDA:
      emit logMessage(i18n("Syncronizing text \"%1\"").arg(sinfo.handheldDB));
      if (!doSync(sinfo)) {
        // The sync could not be done, so inform the user (the error message should probably issued inside doSync)
        DEBUGCONDUIT<<"There was some error syncing the text \""<<sinfo.handheldDB<<"\" with the file "<<sinfo.docfilename<<endl;
      }
      break;
    case eSyncNone:
//    case eSyncAll:
      break;
  }
  if (sinfo.direction != eSyncDelete) fDBNames.append(sinfo.handheldDB);
  
  QTimer::singleShot(0,this, SLOT(syncDatabases()));
  return;
}

The actual sync is done by the function doSync(docSyncInfo&), which first checks for deletion of the database as a special case. Otherwise, it uses the DOCConverter class to copy the text file to or from the handheld, and then recalculates the md5 checksum of the text file on disk and stores it in KPilot's config.

bool DOCConduit::doSync(docSyncInfo &sinfo) {
  bool res=false;
  
  if (sinfo.direction==eSyncDelete) {
    if (!sinfo.docfilename.isEmpty()) {
      if (!QFile::remove(sinfo.docfilename)) {
        kdWarning()<<i18n("Unable to delete the text file \"%1\" on the PC").arg(sinfo.docfilename)<<endl;
      }
      QString bmkfilename = sinfo.docfilename;
      if (bmkfilename.endsWith(".txt")){
        bmkfilename.remove(bmkfilename.length()-4, 4);
      }
      bmkfilename+=PDBBMK_SUFFIX;
      if (!QFile::remove(bmkfilename)) {
        DEBUGCONDUIT<<"Could not remove bookmarks file "<<bmkfilename<<" for database "<<sinfo.handheldDB<<endl;
      }
    }
    if (!sinfo.pdbfilename.isEmpty() && fKeepPDBLocally) {
      PilotLocalDatabase*database=new PilotLocalDatabase(fPDBDir, sinfo.dbinfo.name, false);
      if (database) {
        if ( database->deleteDatabase() !=0 ) {
          kdWarning()<<i18n("Unable to delete database \"%1\" on the PC").arg(sinfo.dbinfo.name)<<endl;
        }
        KPILOT_DELETE(database);
      }
    }
    if (!fLocalSync) {
      PilotDatabase *database=new PilotSerialDatabase(pilotSocket(), sinfo.dbinfo.name);
      if ( database->deleteDatabase() !=0 ) {
        kdWarning()<<i18n("Unable to delete database \"%1\" from the handheld").arg(sinfo.dbinfo.name)<<endl;
      }
      KPILOT_DELETE(database);
    }
    return true;
  }
  // preSyncAction should initialize the custom databases/files for the
  // specific action chosen for this db and return a pointer to a docDBInfo
  // instance which points either to a local database or a database on the handheld.
  PilotDatabase *database = preSyncAction(sinfo);

  if (database && ( !database->isDBOpen() ) ) {
    DEBUGCONDUIT<<"Database "<<sinfo.dbinfo.name<<" does not yet exist. Creating it:"<<endl;
    if (!database->createDatabase(dbcreator(), dbtype()) ) {
      DEBUGCONDUIT<<"Failed"<<endl;
    }
  }

  if (database && database->isDBOpen()) {
    DOCConverter docconverter;
    connect(&docconverter, SIGNAL(logError(const QString &)), SIGNAL(logError(const QString &)));
    connect(&docconverter, SIGNAL(logMessage(const QString &)), SIGNAL(logMessage(const QString &)));

    docconverter.setDOCpath(fDOCDir, sinfo.docfilename);
    docconverter.setPDB(database);
    docconverter.setBookmarkTypes(fBookmarks);
    docconverter.setCompress(fCompress);

    switch (sinfo.direction) {
      case eSyncPDAToPC:
        res = docconverter.convertPDBtoDOC();
        break;
      case eSyncPCToPDA:
        res = docconverter.convertDOCtoPDB();
        break;
      default:
        break;
    }
    
    // Now calculate the md5 checksum of the PC text and write it to the config file
    {
      KConfigGroupSaver g(fConfig, DOCConduitFactory::fGroup);
      KMD5 docmd5;
      QFile docfile(docconverter.docFilename());
      if (docfile.open(IO_ReadOnly)) {
        docmd5.update(docfile);
        QString thisDigest(docmd5.hexDigest().data());
        fConfig->writeEntry(docconverter.docFilename(), thisDigest);
        fConfig->sync();
        DEBUGCONDUIT<<"MD5 Checksum of the text "<<sinfo.docfilename<<" is "<<thisDigest<<endl;
      } else {
        DEBUGCONDUIT<<"couldn't open file "<<docconverter.docFilename()<<" for reading!!!"<<endl;
      }
    }
    
    if (!postSyncAction(database, sinfo, res)) 
      emit logError(i18n("Unable to install the locally created PalmDOC %1 to the handheld.").arg(sinfo.dbinfo.name));
    if (!res)
      emit logError(i18n("Conversion of PalmDOC \"%1\" failed.").arg(sinfo.dbinfo.name));
//    disconnect(&docconverter, SIGNAL(logError(const QString &)), SIGNAL(logError(const QString &)));
//    disconnect(&docconverter, SIGNAL(logMessage(const QString &)), SIGNAL(logMessage(const QString &)));
//    KPILOT_DELETE(database);
  }
  else
  {
    emit logError(i18n("Unable to open or create the database %1").arg(sinfo.dbinfo.name));
  }
  return res;
}

After the sync is done, just call cleanup and emit the syncDone signal:

void DOCConduit::cleanup() {
  FUNCTIONSETUP;
  
  KConfigGroupSaver g(fConfig, DOCConduitFactory::fGroup);
  fConfig->writeEntry(DOCConduitFactory::fDOCList, fDBNames);
  fConfig->sync();

  emit syncDone(this);
}

The worst part about the conduit is to find out which side has been changed (and how), and what needs to be done about this. The function needsSync does exactly this. If the database was not included in the last sync, it is new, so it will be synced from the side where it was added.

First, we find out, how each of the two sides have changed. If the database was already included, check if it was changed using the function textChanged to compare the md5 checksum of the current text on disk with the checksum of the last sync (stored in kpilot's config). The handheld side is a bit trickier: A PalmDOC on the handheld contains of a header record, several text records, and finally several bookmark records. Each of these records can have the dirty flag set, so we first get the number of text records from the header record. Then we search for the index of the first changed record (i.e. dirty flag set) after the header record. If no text record (but a bookmark record) was changed, a config setting determines if the PalmDOC should still be considered as changed.

Finally, from the status of the two sides, determine the sync direction:

bool DOCConduit::needsSync(docSyncInfo &sinfo)
{
  FUNCTIONSETUP;
  sinfo.direction = eSyncNone;
  
  PilotDatabase*docdb=openDOCDatabase(sinfo.dbinfo.name);
  if (!fDBListSynced.contains(sinfo.handheldDB)) {
    // the database wasn't included on last sync, so it has to be new.
    DEBUGCONDUIT<<"Database "<<sinfo.dbinfo.name<<" wasn't included in the previous sync!"<<endl;

    if (QFile::exists(sinfo.docfilename)) sinfo.fPCStatus=eStatNew;
    else sinfo.fPCStatus=eStatDoesntExist;
    if (docdb && docdb->isDBOpen()) sinfo.fPalmStatus=eStatNew;
    else sinfo.fPalmStatus=eStatDoesntExist;
    KPILOT_DELETE(docdb);
    
    if (sinfo.fPCStatus==eStatNew && sinfo.fPalmStatus==eStatNew) {
      sinfo.direction=eSyncConflict;
      return true;
    };
    if (sinfo.fPCStatus==eStatNew) {
      sinfo.direction=eSyncPCToPDA;
      return true;
    }
    if (sinfo.fPalmStatus==eStatNew) {
      sinfo.direction=eSyncPCToPDA;
      return true;
    }
    return true;
  }
  
  // Text was included in the last sync, so if one side doesn't exist, it was deleted and needs to be deleted from the other side, too
  if (!QFile::exists(sinfo.docfilename)) sinfo.fPCStatus=eStatDeleted;
  else if(textChanged(sinfo.docfilename)) {
    sinfo.fPCStatus=eStatChanged;
    DEBUGCONDUIT<<"PC side has changed!"<<endl;
    // TODO: Check for changed bookmarks on the PC side
  } else {
    DEBUGCONDUIT<<"PC side has NOT changed!"<<endl;
  }
  if (!docdb || !docdb->isDBOpen()) sinfo.fPalmStatus=eStatDeleted;
  else {
    PilotRecord *firstRec = docdb->readRecordByIndex(0);
    PilotDOCHead docHeader(firstRec);
    KPILOT_DELETE(firstRec);

    int storyRecs = docHeader.numRecords;

    // determine the index of the next modified record (does it lie beyond the actual text records?)
    int modRecInd=-1;
    PilotRecord*modRec=docdb->readNextModifiedRec(&modRecInd);
    DEBUGCONDUIT<<"Index of first changed record: "<<modRecInd<<endl;
    
    KPILOT_DELETE(modRec);
    // if the header record was changed, find out which is the first changed real document record:
    if (modRecInd==0) {
      modRec=docdb->readNextModifiedRec(&modRecInd);
      DEBUGCONDUIT<<"Reread Index of first changed records: "<<modRecInd<<endl;
      KPILOT_DELETE(modRec);
    }
  
    // The record index starts with 0, so only a negative number means no modified record was found
    if (modRecInd >= 0) {
//      sinfo.fPalmStatus=eStatBookmarksChanged;
      DEBUGCONDUIT<<"Handheld side has changed!"<<endl;
      if ((!fIgnoreBmkChangesOnly) || (modRecInd <= storyRecs)) 
        sinfo.fPalmStatus=eStatChanged;
      DEBUGCONDUIT<<"PalmStatus="<<sinfo.fPalmStatus<<", condition="<<((!fIgnoreBmkChangesOnly) || (modRecInd <= storyRecs))<<endl;
    } else {
      DEBUGCONDUIT<<"Handheld side has NOT changed!"<<endl;
    }
  }
  KPILOT_DELETE(docdb);

  if (sinfo.fPCStatus == eStatNone && sinfo.fPalmStatus==eStatNone) {
    DEBUGCONDUIT<<"Nothing has changed, not need for a sync."<<endl;
    return false;
  }
  // if either is deleted, and the other is not changed, delete
  if ( ((sinfo.fPCStatus == eStatDeleted) && (sinfo.fPalmStatus!=eStatChanged)) ||
       ((sinfo.fPalmStatus == eStatDeleted) && (sinfo.fPCStatus!=eStatChanged)) ) {
    DEBUGCONDUIT<<"Database was deleted on one side and not changed on the other -> Delete it."<<endl;
    sinfo.direction=eSyncDelete;
    return true;
  }
  
  // eStatDeleted (and both not changed) have already been treated, for all 
  // other values in combination with eStatNone, just copy the texts.
  if (sinfo.fPCStatus==eStatNone) {
    DEBUGCONDUIT<<"PC side has changed!"<<endl;
    sinfo.direction=eSyncPDAToPC;
    return true;
  }

  if (sinfo.fPalmStatus==eStatNone) {
    sinfo.direction=eSyncPCToPDA;
    return true;
  }
  
  // All other cases (deleted,changed), (changed, deleted), (changed,changed) create a conflict:
  sinfo.direction=eSyncConflict;
  return true;
}

These code pieces from the docconduit are supposed to give you an insight into how to structure a conduit.


next up previous contents
Next: A record-based conduit Up: KPilot conduit programming tutorial Previous: A very simple conduit:   Contents
Reinhold Kainhofer 2003-01-13