BlackBerry
Skip Top Navigation
Developers Partners
North America [change region] Products Solutions Purchasing Support
Developers


IPD File Format

Garry Seifried, Research In Motion

Note: The format of IPD files described in this article may change at any time at the sole discretion of Research In Motion. Research In Motion will not provide technical support on issues related to IPD files and their format.

The Need for Speed

One of the challenges of working with wireless applications is that delivering large amounts of data over the wireless network can be both time-consuming and expensive.

Suppose that you want to put the contact information for an organization on the BlackBerry wireless device. Normally, we only put the most essential data on the device, and wirelessly pull additional less-essential data as may be needed.

Let’s assume, for the sake of this article, that the entire set of contact information is required.

BlackBerry Desktop Manager Backup/Restore

We’ve all used the BlackBerry Desktop Manager to perform backup of our device data - perhaps prior to performing an upgrade to the device software. Periodically, we’ve also needed to use that backup to restore our data. But have you ever had a look inside one of those IPD files?

That’s one nasty looking file!

Using the IPD Format for Bulk Loads

What if the most effective and efficient way of getting a large amount of data onto the device were to use the IPD format? Using this format, we could programmatically create a file that could be used by the desktop manager in a restore operation.

The structure of the IPD file shown above is as follows:

Inter@ctive Pager Backup/Restore File
<Line feed> - 1 byte - value 0A
<Version> - 1 byte - value 02
<Number of databases in file> - 2 bytes
<Database name separator> - 1 byte - value 00
<Database name block#1>
<Database name block#2>
.
<Database name block#n>
<Database data block#1>
<Database data block#2>
.
<Database data block#n>
  

Where each database name block is of the form:

<Database name length> 2 bytes
The length includes the terminating null
<Database name> As long as the name length

And each database data block is of the form:

<Database ID> 2 bytes
Zero-based position in the list of database name blocks
<Record length> 4 bytes
<Database version> 1 byte
<DatabaseRecordHandle> 2 bytes
<Record unique ID> 4 bytes
<Field length #1> 2 bytes
<Field type #1> 1 byte
<Field data #1> As long as the field length
<Field length #m> 2 bytes
<Field type #m> 1 byte
<Field data #m> As long as the field length

Parse that IPD

In manually parsing the file, we can see some structure that matches the binary structure just outlined:

Number of databases: 57
Database[ 0 ] = Content Store
Database[ 1 ] = Service Book
.
Database[ 56 ] = WTLS Options

Record for database 0:
  Database version: 0x01
  Record handle:    0x0001
  Unique ID:        0x24F07B6D
    Field:
      Length: 0x0002
      Type:   0x01
      Data:
        2F 00
    Field:
      Length: 0x0004
      Type:   0x03
      Data:
        21 00 00 20                            /
    Field:
      Length: 0x0007
      Type:   0x05
      Data:
        66 6F 6C 64 65 72 00                            folder

Record for database 0:
  Database version: 0x01
  Record handle:    0x0002
  Unique ID:        0x00000007
    Field:
      Length: 0x0007
      Type:   0x01
      Data:
        2F 68 6F 6D 65 2F 00                            /home/
    Field:
      Length: 0x0004
      Type:   0x03
      Data:
        31 00 00 20
    Field:
      Length: 0x0007
      Type:   0x05
      Data:
        66 6F 6C 64 65 72 00                            folder
  

Limits in the IPD Format

IPD files have a record length restriction of 128K bytes. To get around this limitation, we came up with a record structure that would separate the content into separate records each under the 128K limit.

The Unique ID values map to this record type mapping for the eight record types:

SESSION_TYPE = 1
TRACK_TYPE = 3
SPEAKER_TYPE = 5
SESSION_TRACK_TYPE = 7
TRACK_SESSION_TYPE = 9
KEYNOTE_LIST_TYPE = 10 (0A)
DAY_SESSION_TYPE = 11 (0B)
SESSION_SPEAKER_TYPE = 13 (0D)
This is how the records appear in the file ( fields omitted):
Number of databases: 1
Database[ 0 ] = BBConferenceGuideData
Record for database 0:
  Database version: 0x01
  Record handle:    0x0001
  Unique ID:        0x00000001

Record for database 0:
  Database version: 0x01
  Record handle:    0x0008
  Unique ID:        0x0000000D
  

What is the relationship to a Session record as defined in this structure versus an instance of a Session record from a database? We used the fields of the IPD record for each occurrence of a session.

Writing the Length Before the Content

Since you need to output the record length before the record content, you must process the content first and then get the content length.

This is done by creating an intermediate file that contains the content. The method that closes the file must also return the content length.

After writing the content length, append the intermediate file content to the final IPD file.

This also applies to the content in a record, such as an image that needs to be processed to determine the length of the image content. There is some extra overhead in processing content twice but it is only done while creating the file where overhead doesn’t impact much.

Little Endian Hex Values

Lengths are stored in little endian format. This means that the low-order byte of the number is stored at the lowest address, and the high-order byte at the highest address (the word is stored ‘little-end-first’).

The little-endian format applies to the following fields:

  • record length
  • db version
  • db handle
  • record type.

The following conversion is used to get the byte values:

//Size as little-endian hex value
byte b1 = Convert.ToByte(size&Byte.MaxValue);
byte b2 = Convert.ToByte(size>>8&Byte.MaxValue);
byte b3 = Convert.ToByte(size>>16&Byte.MaxValue);
byte b4 = Convert.ToByte(size>>24&Byte.MaxValue);
  

After conversion, each byte is written in order:

loader.write(b1);
loader.write(b2);
loader.write(b3);
loader.write(b4);
  

Using the Simulator for Backup/Restore

Prior to BlackBerry Java Development Environment (JDE) v4.0, simulator testing configuration for backup/restore required the installation of a serial loopback driver to be defined for a port (say PORT5) as well as few other steps.

With BlackBerry JDE v4.0, the BlackBerry Device Simulator provides a direct and convenient way for backup/restore testing:

  • Launch the BlackBerry Device Simulator
  • Select the Simulate menu
  • Check the USB Cable Connected.

Application Handling the Restore

Performing Backup/Restore involves the BlackBerry Synchronization API. The Synchronization API provides the following interfaces that need to be implemented:

SyncConverter

  • Converts data between SyncObject-format on the device and a serialized format required on the desktop.

SyncCollection

  • A collection of synchronization objects.

SyncObject

  • An object that can be backed up and restored to the user’s computer.

SyncConverter and SyncCollection Manager

  • SyncConverter and SyncCollection are interfaces, so we chose to implement the interfaces in a DataStoreSyncManager class.

DataStoreSyncManager implements SyncCollection

Since a SyncCollection is a collection of SyncObject used for backup/restore and synchronization, we need to provide implementations for the methods defined in the SyncCollection interface.

Notable methods include:

beginTransaction()

  • Starts a transaction which is required to execute synchronizations that involve a large number of data records

endTransaction()

  • Ends a transaction which is required to execute synchronizations that involve a large number of data records

getSyncConverter()

  • Returns the instance of the DataStoreSyncManager

getSyncName()

  • Returns the database name

getObjectCount()

  • Returns the number of sync objects

getSyncObjects()

  • Returns a SyncObject[]

getSyncObject(int uid)

  • Returns a SyncObject by UID

getSyncVersion()

  • Returns 1

removeAllSyncObjects()

  • Clears the DataStore object and returns true

addSyncObject()

  • Adds a SyncObject to the DataStore by using the UID to identify the data type, and using the DataStore methods for the appropriate data type (e.g. setSessionSpeaker() method)

Since we are performing a bulk-load and not a synchronization, we can provide minimal implementations as shown below for some of the methods. For a backup/restore, we can return false; for Synchronization you must provide a full implementation.

isSyncObectDirty () {return false; }
removeSyncObject () { return false; }
setSyncObjectDirty() {}
clearSyncObjectDirty()  {}
  

DataStoreSyncManager implements SynchConverter

This class implemented the convert(..) method that Extracts a SyncObject from the synchronization data and converts a SyncObject into synchronization data.

DataStoreSyncObject implements SyncObject

The application needs to have a class defined that represents the structure of the .IPD database records. For our example, eight record types have been defined in our database, so we effectively have eight instances of SyncObjects.

A SyncObject must have a unique ID, which is a 32-bit value that is contant for the lifetime of the object. For this UID we implement a getUID() method that returns a value for one of the record types. For example, SESSION_TYPE = 1 as defined above.

PersistentStore contains Persistable objects

Since we’ve gone to the trouble of getting the data to the device, we now have to save it to the PersistentStore. To do so, implement a class that extends the Persistable interface. Again, the structure of the DataStore class needs to reflect the structure of the data contained in the .IPD file.

In our example, we have eight different data records defined so we will need a DataStore class capable of managing those eight different data types.

We chose to have byte[][] for Session, Track and Speaker data as the records contained character data.

For SessionTrackMap, TrackSessionMap, DaySessionMap, SessionSpeakerMap we chose int[][] since we had relationships expressed as integer index values.

For KeyNoteSessionList we chose int[] as we only had the Session index value in the list. So we have a DataStore structure on the device that supports the .IPD format.

The implementation of methods to access the particular records and record relationships is up to the designer of the application. In our case, we designed the data to support the application UI so that we had methods for Sessions, SessionSpeakers, KeyNoteSpeakers, Tracks, etc.

Sample Code

Loader.cs
Contains the data retrieval and data output code.

namespace BulkLoader {
  /// <summary>
  /// Summary description for Class1.
  /// </summary>
  class Loader {
    // Constants for record types - session shown here
    public static int SESSION_NUMBER_TYPE = 0;
    public static int SESSION_TYPE = 1;

    public void doLoading() {
      // Create output file and content
      byte dbVersion = 1;
      short dbHandle = 0;

      // Final output written to Loader1
      Loader loader1 = new Loader();
      loader1.createFile("BulkLoad-Output.ipd");

      // Prefix data for every IPD file
      loader1.loadFile("BulkLoad-Prefix1.ipd");

      // Data access provided by DBReader
      DBReader rdr = new DBReader();

      // Intermediate data written to BulkLoad-Data.ipd files
      Loader loader = new Loader();
      loader.createFile("BulkLoad-Data.ipd");
      dbHandle++;

      // An ArrayList of SessionInfo objects turned into SessionContent
      ArrayList list = rdr.GetSessions();
      int listSize = list.Count;
      SessionContent sessions = new SessionContent(listSize);
      for (int i=0;i<listSize;i++) {
        SessionInfo info = (SessionInfo) list[i];
        Session data = new Session(info);
        sessions.AddMember(i, data);
      }

      if (listSize>0) {
        sessions.WriteInfo(loader);
      }

      // Get the size so far and output
      int bytes = loader.closeFile();
      OutputData(loader1, bytes, dbVersion, dbHandle, SESSION_TYPE);
    }

    /**
     * Create output from the data
     */
    private void OutputData(Loader loader, int bytes, byte dbVersion,
      short dbHandle, int recordType) {

    // Database type
    short dbase = 0;
    loader.Write(dbase);

    // Add the size of the Prefix2 portion which is 7 bytes
    bytes+=7;

    // Write the size as little-endian hex values
    byte b1 = Convert.ToByte(bytes&Byte.MaxValue);
    byte b2 = Convert.ToByte((bytes>>8)&Byte.MaxValue);
    byte b3 = Convert.ToByte((bytes>>16)&Byte.MaxValue);
    byte b4 = Convert.ToByte((bytes>>24)&Byte.MaxValue);
    loader.Write(b1);
    loader.Write(b2);
    loader.Write(b3);
    loader.Write(b4);

    // DBVersion - append as little-endian hex values
    b1 = Convert.ToByte(dbVersion&Byte.MaxValue);
    loader.Write(b1);

    // DBHandle - append as little-endian hex values
    b1 = Convert.ToByte(dbHandle&Byte.MaxValue);
    b2 = Convert.ToByte((dbHandle>>8)&Byte.MaxValue);
    loader.Write(b1);
    loader.Write(b2);

    // RecordType - append as little-endian hex values
    b1 = Convert.ToByte(recordType&Byte.MaxValue);
    b2 = Convert.ToByte((recordType>>8)&Byte.MaxValue);
    b3 = Convert.ToByte((recordType>>16)&Byte.MaxValue);
    b4 = Convert.ToByte((recordType>>24)&Byte.MaxValue);
    loader.Write(b1);
    loader.Write(b2);
    loader.Write(b3);
    loader.Write(b4);

    // Data Content
    loader.loadFile("BulkLoad-Data.ipd");
  }
}
  

Content.cs
Contains definitions for the IPD file content: Session, Tracks, Speakers.
Session items are shown here.

using System;
using System.Collections;
using System.IO;
using System.Text;

namespace BulkLoader {

  /**
   * MapContent defines the Record Group: Length, Type, length value that prefixes every record.
   */
  public abstract class MapContent {
    short length;
    protected byte type;     // Record type
    protected int[] members; // Used for Record length

    public MapContent() {}


    public void SetContentLength(int len) {
      length = Convert.ToInt16(len);
    }

    public void AddContentMember(int pos, int item) {
      if (members != null) {
        members[pos] = item;
      }
    }

    /// <summary>
    /// Our objects need to know how to write themselves
    /// </summary>
    /// <param name="loader"></param>
    protected void WriteContentMember(Loader loader) {
      loader.Write(length);
      loader.Write(type);
      int j = members.Length;

      for (int i=0; i<j;i++) {
        loader.Write((int) members.GetValue(i));
      }
    }
  }

  /**
   * SessionContent, Session and SessionInfo manage Sessions.
   */
  public class SessionContent : MapContent {
    Session[] info;

    /// <summary>
    /// Ctor defines the size of Session[]
    /// </summary>
    public SessionContent(int num) {
      type = Convert.ToByte(Loader.SESSION_NUMBER_TYPE);
      info = new Session[num];
      members = new int[1];
    }


    /// <summary>
    /// Add an item to the list
    /// </summary>
    public void AddMember(int pos, Session data) {
      info[pos] = data;
    }

    /// <summary>
    /// Our objects need to know how to write themselves
    /// </summary>
    /// <param name="loader"></param>
    public void WriteInfo(Loader loader) {
      // Content setup
      SetContentLength(4);
      AddContentMember(0, info.Length);
      WriteContentMember(loader);
      IEnumerator e = info.GetEnumerator();

      while (e.MoveNext()) {
        Session data = (Session) e.Current;
        data.WriteInfo(loader);
      }
    }
  }

  /// <summary>
  /// Session provides the infoType, length and SessionInfo
  /// </summary>
  public class Session {
    short infoLength;
    byte infoType = Convert.ToByte(Loader.SESSION_TYPE);
    SessionInfo info;

    public Session(SessionInfo data) {
      info = data;
      infoLength = Convert.ToInt16(info.Length());
    }

    /// <summary>
    /// Our objects need to know how to write themselves
    /// </summary>
    /// <param name="loader"></param>
    public void WriteInfo(Loader loader) {
      loader.Write(infoLength);
      loader.Write(infoType);
      info.WriteInfo(loader);
    }
  }

  /// <summary>
  /// SessionInfo contains the details of a session
  /// </summary>
  public class SessionInfo {
    long start;
    long duration;
    byte keynote;
    string info;

    public SessionInfo(long start, long duration, byte keynote, string info) {
      this.start = start;
      this.duration = duration;
      this.keynote = keynote;
      this.info = info;
    }

    /// <summary>
    /// The length is the size of the SessionInfo object
    /// </summary>
    /// <returns></returns>
    public short Length() {
      return Convert.ToInt16(8 + 8 + 1 + info.Length);
    }

    /// <summary>
    /// Our objects need to know how to write themselves
    /// </summary>
    /// <param name="loader"></param>
    public void WriteInfo(Loader loader) {
      loader.Write(start);
      loader.Write(duration);
      loader.Write(keynote);
      loader.Write(info);
    }
  }
  

Loader.cs
This class is responsible for output of data using a BinaryWriter.
Methods exist to write various data types: char, int, byte, short, long, String.

using System;
using System.IO;
namespace BulkLoader {

/// <summary>
/// Loader manages Binary files.
/// </summary>
public class Loader {
  // Our writer
  BinaryWriter writer = null;

  // Output length counter
  int bytes = 0;

  public Loader() {}

  /// <summary>
  /// Create BinaryWriter from a filename
  /// </summary>
  /// <param name="fileName"></param>
  public void createFile(String fileName) {
    try {
      writer =  new BinaryWriter(File.Open(fileName, FileMode.Create));
    } catch (Exception e) {
      Console.Out.WriteLine("Exception: " + e.Message);
    }
  }

  /// <summary>
  /// Load a file
  /// </summary>
  /// <param name="fileName"></param>
  public void loadFile(String fileName) {
    if (fileName.Length==0)  {
      return;
    }

    loadFile(fileName, false);
  }

  /// <summary>
  /// Load a file, counting the bytes of the file
  /// </summary>
  /// <param name="fileName"></param>
  /// <param name="countBytes"></param>
  public void loadFile(String fileName, bool countBytes) {
    if (fileName.Length==0)  {
      return;
    }

    BinaryReader rdr = null;

    Try {
      rdr =  new BinaryReader(File.OpenRead(fileName));
      byte b;

      for (;;){
        b = rdr.ReadByte();
        writer.Write(b);

        if (countBytes){
          bytes++;
        }
      }
    } catch (EndOfStreamException) {
      ; // do nothing
    }
    finally {
      if (rdr != null) rdr.Close();
    }
  }

  /// <summary>
  /// Close an intermediate file and return the file size
  /// </summary>
  /// <returns>file size</returns>
  public int closeFile() {
    if (writer != null) {
      try {
        writer.Close();
      } catch (Exception e) {
        Console.Out.WriteLine("Exception: " + e.Message);
      }
    }
    return bytes;
  }

  /// <summary>
  /// Write a char - 1 byte
  /// </summary>
  /// <param name="val"></param>
  public void Write(char val) {
    writer.Write(val);
    bytes+=1;
  }

  /// <summary>
  /// Write an int - 4 bytes
  /// </summary>
  /// <param name="val"></param>
  public void Write(int val) {
    writer.Write(val);
    bytes+=4;
  }

  /// <summary>
  /// Write a short - 2 bytes
  /// </summary>
  /// <param name="val"></param>
  public void Write(short val) {
    writer.Write(val);
    bytes+=2;
  }

  /// <summary>
  /// Write a byte - 1 byte
  /// </summary>
  /// <param name="val"></param>
  public void Write(byte val) {
    writer.Write(val);
    bytes+=1;
  }

  /// <summary>
  /// Write a long - 8 bytes
  /// </summary>
  /// <param name="val"></param>
  public void Write(long val) {
    writer.Write(val);
    bytes+=8;
  }

  /// <summary>
  /// Write a String - string length
  /// </summary>
  /// <param name="val"></param>
  public void Write(string val) {
    Write(val, val.Length);
  }

  /// <summary>
  /// Write a String - string length
  /// </summary>
  /// <param name="val", "len" ></param>
  public void Write(string val, int len) {
    StringReader rdr = new StringReader(val);

    for (int i=0;i<len;i++) {
      int c = rdr.Read();
      writer.Write(Convert.ToByte(c));
      bytes++;
    }
  }
  


Please email your comments, suggestions and editorial submissions to


Top |  Table of Contents |  Journal in PDF Format |  Legal Disclaimer
 
     
 Home | Products | Solutions | Purchasing | Support | Developers | Worldwide | News | About Us | Contact Us | Site Map
 Legal | Copyright © 2008 Research In Motion Limited, unless otherwise noted.