German has posted 18 posts at DZone. View Full User Profile

Real Android apps leveraging db4o persistence engine (Part 2)

09.08.2010
| 6565 views |
  • submit to reddit

This the second delivery in a series of articles targeted at showing developers how db4o (an open source database that leverages today's object-oriented languages, systems, and mindset) is being used in several Android projects to avoid all the pitfalls and hassles of object-relational mapping while benefiting from an concise and straight forward way to evolve a domain model which, in the end, translates into faster, easier upgrades for users.

There are many benefits to using an object database like db4o, including easier code maintenance, and the ability to create applications based on more complex data models. Unlike in rigid, predefined SQL tables, you can store dynamic, free-form data, which can be changed or amended any time. In addition, db4o allows for data replication, another missing element in Android's software stack.

We'll take a look at the code in these projects to learn how developers leverage object database technology on their apps and also use the opportunity to introduce key concepts about db4o.

On the first article in the series we reviewed some aspects of project DyCaPo, a system that facilitates the ability of drivers and passengers to make one-time ride matches close to their departure time. This time we'll take a look at QuiteSleep, an Android project by César Valiente which is powered by db4o and is both open source and available in the Android market.

QuiteSleep's main task is to silence calls from contacts that have been previously blocked for a given time frame. Once the rejected call is finished QuiteSleep will restore the phone to it's normal operation state (restoring previous volume and vibration settings). Additionally, the app allows you to send a notification to blocked callers via e-mail or SMS.

According to César adding db4o to the project was straight forward: you just add the db4o jar file as any other external library in Eclipse (even for an Android project). Once this is done the full functionality of the db4o database is available to the Android application. During development the db4o version used in the project was db4o-7.12.132.14217. When you download db4o for Java you'll see that there are quite a bunch of jar files. This is because (starting with version 7.7) db4o was splitted so you can add exactly what you need in your project and decrease the library's footprint. If you choose the jar file where the name ends iwith "-all.jar" you'll be addding a version that has everything in it (no other db4o related jar files will be necessary).

In QuiteSleep db4o runs in embedded client/server mode which is a sort of an hybrid between a pure embedded option and networked C/S. When db4o acts as embedded server all communications are internal and fast (no networking takes place) but, as opposed to the simple embedded mode, many local clients can't connect to the database at the same time (ie. different clients can start different transactions on the db). In this applications the server is exposed as a singleton and one or more clients connect to it.

Database Server

The database server is configured following these steps:

1. Server configuration includes activation depth, permissions to allow database version updates, database file name and path, etc.

	/**
* This object is for get only one Singleton object and create ONLY in of
* this class mode.
*/
private static ServerDDBB SINGLETON = null;

/**
* * This object is the server DDBB file properly said
*/
private static ObjectServer server = null;

private ServerDDBB() {
try {
if (QSLog.DEBUG_I)
QSLog.i(CLASS_NAME, "Before to open the DDBB");
ServerConfiguration configuration = Db4oClientServer
.newServerConfiguration();
configuration.common().allowVersionUpdates(true);
configuration.common().activationDepth(DEEP);
server = Db4oClientServer.openServer(configuration,
getDDBBFile(DDBB_FILE), 0);
if (server == null)
if (QSLog.DEBUG_W)
QSLog.w(CLASS_NAME, "Server Null!!!!");
} catch (Exception e) {
if (QSLog.DEBUG_E)
QSLog.e(CLASS_NAME, ExceptionUtils.exceptionTraceToString(e
.toString(), e.getStackTrace()));
}
}


2. The sole instance of the database server is created here when receiving the first database client request. Once created the server instance is accessed as a singleton and will take care of new client requests according to the application's needs.

	/**
* Create the server instance if the server doesn’t create before
*/
private synchronized static void createInstance() {
if (server == null)
SINGLETON = new ServerDDBB();
}

/**
* Get the server DDBB instance, if the singleton object doesn’t crea
* before, we create for use it. * @return The server object
*
* @see ObjectsServer
*/
public static ObjectServer getServer() {
if (SINGLETON == null)
createInstance();
return server;
}


Database Client

The database clients are responsible for doing queries, updates, deletions, etc. QuiteSleep provides a DB client class that rely on companion classes for performing these operations.
In the code below you can see QuiteSleep's DB client class implementation:

	protected ObjectContainer clientDDBB;
protected Selects selects;
protected Inserts inserts;
protected Updates updates;
protected Deletes deletes;

/**
* This function return the ObjectContainer used like as clientDDBB for
* the application.
* @return The ObjectContainer used like as clientDDBB
* @see ObjectContainer
*/
public ObjectContainer getObjectContainer() {
return clientDDBB;
}

/**
* Empty constructor, get de client and use it.
*/
public ClientDDBB() {
try {
synchronized (SEMAPHORE) {
clientDDBB = ServerDDBB.getServer().openClient();
selects = new Selects(clientDDBB);
inserts = new Inserts(clientDDBB);
updates = new Updates(clientDDBB);
deletes = new Deletes(clientDDBB);
}
} catch (Exception e) {
if (QSLog.DEBUG_E)
QSLog.e(CLASS_NAME, ExceptionUtils.exceptionTraceToString(e
.toString(), e.getStackTrace()));
}
}


As you can see in the code, the DB client works with an ObjectContainer that is obtained by calling the DB server (in the static class ServerDDBB). Once we obtain the container we can use it to create the companion objects that perform operations such as Selects, Inserts, Updates and Deletes that allow us to work with the DB.

 As an add-on, we can also instantiate the DB clients by passing an external ObjectContainer or even change the default DB server. These alternatives are not used in the application but are included as goodies for developers.

Once we obtain the DB client it's their responsibility to perform all the common operations of a database (selects, inserts, updates and deletes). Let's take a look at how the object that handles "selects" is created and how to submit a query to obtain all contacts in the database. The procedure for creating objects that handle inserts, updates and deletes is identical.


	private ObjectContainer db;

/**
* Constructor
* @param db
*/
public Selects(ObjectContainer db) {
this.db = db;
}

/**
* Select all Contact objects from the DDBB and return it. * @return All
* Contact objects from the DDBB
*
* @see List
*/
public List selectAllContacts() {
try {
synchronized (SEMAPHORE) {
Query query = db.query();
query.constrain(Contact.class);
// Ordered by name (first A....last Z)
query.descend(DDBBValues.CONTACT_NAME).orderAscending();
List contactList = query.execute();
return contactList;
}
} catch (Exception e) {
if (QSLog.DEBUG_E)
QSLog.e(CLASS_NAME, ExceptionUtils.exceptionTraceToString(e
.toString(), e.getStackTrace()));
return null;
}
}


As you can see we pass the ObjectContainer in the DB client to the class constructor so we can build the queries and obtain the necessary data. the queries used in the code are called SODA queries, one of the three query types that db4o currently supports.

Application Lifecycle

During the application life-cycle there are several points where data is accessed (queries, inserts, updates, etc). Let's see how db4o is used by QuiteSleep during normal operation.

Contact Management

When QuiteSleep boots for the first time it performs what is probably the most important data operation in the app: it synchronizes all the contacts with db4o.

/**
* Sync contacts from the ddbb SQLite to the DB4O ddbb
*/
private void syncContacts() {

try {

if (context != null) {

// int insertContact = 0;
ClientDDBB clientDDBB = new ClientDDBB();

// get all contacts from de ddbb
Cursor contactCursor = context.getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI, null, null,
null, null);

if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "Num contacts: "
+ contactCursor.getCount());

// Whule startCursor has content
while (contactCursor.moveToNext()) {

String contactName = contactCursor
.getString(contactCursor
.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));

String contactId = contactCursor.getString(contactCursor
.getColumnIndex(ContactsContract.Contacts._ID));

String hasPhone = contactCursor
.getString(contactCursor
.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));

// If the used contact has at least one phone number, we
// insert it in the db4o ddbb, else not insert.
if (hasPhone.equals("1")) {

// Create the db4o contact object.
Contact contact = new Contact(contactId, contactName);

// insert the contact object
clientDDBB.getInserts().insertContact(contact);

insertContact++;

// where clausule
String where = ContactsContract.Data.CONTACT_ID
+ " = "
+ contactId
+ " AND "
+ ContactsContract.Data.MIMETYPE
+ " = '"
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
+ "'";

Cursor phonesCursor = context.getContentResolver()
.query(ContactsContract.Data.CONTENT_URI, null,
where, null, null);

if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "count: "
+ phonesCursor.getCount());

List phonesList = new ArrayList(
phonesCursor.getCount());

// While the phonesCursor has content
while (phonesCursor.moveToNext()) {

String phoneNumber = phonesCursor
.getString(phonesCursor
.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));

phonesList.add(phoneNumber);

if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "phone: " + phoneNumber);
}
// close the phones cursor
phonesCursor.close();

// create the phones object asociated to contacts
createPhoneObjects(contact, phonesList, clientDDBB);

// Get all mail from the contact
Cursor mailCursor = context
.getContentResolver()
.query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Email.CONTACT_ID
+ " = " + contactId, null, null);

if (mailCursor.getCount() > 0) {

List mailsList = new ArrayList(
mailCursor.getColumnCount());
// While the emailsCursor has content
while (mailCursor.moveToNext()) {

// This would allow you get several email
// addresses
String emailAddress = mailCursor
.getString(mailCursor
.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA));

if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "email: "
+ emailAddress);
mailsList.add(emailAddress);
}

// Create the mail objects asociated to every
// contact
createMailObjects(contact, mailsList, clientDDBB);
}
// close the email cursor
mailCursor.close();

}// end hasPhone

// create the Schedule object if not have been created
// previously
createSchedule(clientDDBB);

// Commit the transaction
clientDDBB.commit();

}// end contact cursor
}// end context!=null
else
// return -1;
insertContact = -1;

} catch (Exception e) {
if (QSLog.DEBUG_E)
QSLog.e(CLASS_NAME, ExceptionUtils.exceptionTraceToString(e
.toString(), e.getStackTrace()));
insertContact = -1;
}
}


The app retrieves, via a content provider, the user's contact list and filters out all the contacts that have no associated phone number (they are not useful for QuiteSleep). The contacts with phone numbers are then stored in db4o. Once this is done db4o becomes the only data access point for the application.

This sync operation is performed in the background with the help of Java threads. Android dialogs are used to notify the user that the process is taking place while preventing user interaction (the process is critical to allow the app to work properly).

The sync process can later be performed by the user on-demand via the app's settings. During the sync process a series of objects are created that will be made persistent in db4o and participate in data access operations:

Contact
This class represents a typical contact (with a full name and a unique contact id). It also has a boolean attribute that shows whether this caller is in a blocked state in the application.

Phone
This class represents a phone number associated with a contact. It has a direct reference to a contact object described above. It also provides a boolean attribute to indicate whether this phone number is enabled to receive SMS notifications by the application.

Mail
This class represents e-mail addresses belonging to contacts. It also references a contact object directly (the owner of the e-mail data). It also provides a boolean attribute to indicate whether this e-mail is enabled to receive e-mail notifications by the application.

These objects are manipulated during operations such as adding blocked contacts, editing contact information and unblocking contacts.

During the contact blocking operation a new kind of object is created in the database:

Banned
This object is created when we mark a contact as blocked. Internally, this operation relates a Contact object with a Banned object and a Schedule object (the later specifies exactly when a contact is supposed to be blocked as explained below).

Every time we remove the blocking state for a contact the Banned object is deleted from the database while the Contact and Schedule objects remain untouched (no cascade deletion is used).

public int deleteAllBanned() {

try {

Query query = db.query();
query.constrain(Banned.class);
List bannedList = query.execute();
int deletes = 0;

if (bannedList != null)
for (int i = 0; i < bannedList.size(); i++) {
Banned banned = bannedList.get(i);
db.delete(banned);
deletes++;
}

return deletes;

} catch (Exception e) {
if (QSLog.DEBUG_E)
QSLog.e(CLASS_NAME, ExceptionUtils.exceptionTraceToString(e
.toString(), e.getStackTrace()));
return -1;
}
}


Time Management

Once the app creates all the objects associated with contacts (objects of types Contact, Phone, Mail, etc) and establishes the blocked state for them, the next step is to determine the active time frame for blocked contacts.

In order to support that the app uses 3 parameters:
  1. startTime y startFormatTime (time for applying the blocking state), with types Date and String respectively.
  2. endTime y endFormatTime (time for the removing the blocking state), also with types Date and String respectively.
  3. monday, tuesday, wednesday, thursday, friday, saturday y sunday (days of the week), all boolean values.

When any of these parameters is defined by the user, a Schedule object is created based on this information and saved in the database. The Schedule object is created only once and is never deleted (only updated when the schedule details are changed by the user).

As we mentioned before Scheduled objects are referenced by Banned objects.

Service Management

Once all the contact related data together with blocking information has been processed the app configures the notifications service (both SMS and e-mail) and other minor services. when this is the dome the main service loop is started and the app is ready to be used.

At this point the Settings object is created and stored in the database with parameters such as:

  • quiteSleepServiceState: true when QuiteSleep is up and running and has been activated
  • E-mail and SMS configuration attributes: such as recipient, body, subject, etc together with a boolean value that determines whether each type of notification is active

Event History

In this phase the user can access information about the recorded activities while the application was running. The available information consists of the contact name, phone number of the originating call, call date and time, number of messages (SMS, e-mail) that were sent.

In order to support this kind of logging a CallLog object is made persistent in db4o for every blocked call. A CallLog object references a specific contact and phone number combined with the specific date and time of the blocked call.

 

	public void silentIncomingCall() {

try {

/*
* Put the mobile phone in silent mode (sound+vibration) Here i put
* this for silent all incoming call for salomonic decision that
* silent all calls for later if the contact is banned let this
* silent mode and if the contact isn't banned put in normal mode. I
* use this because sometimes a ring second sound when an incoming
* call.
*/
// putRingerModeSilent();

ClientDDBB clientDDBB = new ClientDDBB();

String phoneNumberWhithoutDashes = TokenizerUtils
.tokenizerPhoneNumber(incomingCallNumber, null);

Contact contactBanned = clientDDBB.getSelects()
.selectBannedContactForPhoneNumber(
phoneNumberWhithoutDashes);

// If the contact is in the banned list
if (contactBanned != null) {

if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "Contact: "
+ contactBanned.getContactName() + "\t isBanned: "
+ contactBanned.isBanned());

// create the CallLog object for log calls.
CallLog callLog = new CallLog();

// check if the call is in the interval time
boolean isInInterval = checkSchedule(callLog, clientDDBB);

if (isInInterval) {

// Put the mobile phone in silent mode (sound+vibration)
putRingerModeSilent();

/*
* Check if the mail service is running, if it is true
* create a SendMail object for try to send one or more
* email to the contact with the incoming number
*/
sendMail(incomingCallNumber, callLog, clientDDBB);

/*
* Check if the sms service is running, if it is true create
* a SendSMS object for try to send a SMS to the contact
* with the incoming number
*/
sendSMS(incomingCallNumber, callLog, clientDDBB);

// get the nomOrder for the new CallLog
int numOrder = clientDDBB.getSelects().countCallLog();
if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "CallLog numOrder: " + numOrder);

// Set the parameters and save it
callLog.setPhoneNumber(phoneNumberWhithoutDashes);
callLog.setContact(contactBanned);
callLog.setNumOrder(numOrder + 1);

clientDDBB.getInserts().insertCallLog(callLog);
clientDDBB.commit();
clientDDBB.close();

}
// If the call isn't in the interval time
else {
if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "No est√° en el intervalo");
// putRingerModeNormal();
clientDDBB.close();
}
}
// If the incoming call number isn't of the any banned contact
else {
if (QSLog.DEBUG_D)
QSLog.d(CLASS_NAME, "ContactBanned == NULL!!!!");
// putRingerModeNormal();
clientDDBB.close();
}

} catch (Exception e) {
if (QSLog.DEBUG_E)
QSLog.e(CLASS_NAME, ExceptionUtils.exceptionTraceToString(e
.toString(), e.getStackTrace()));
}
}

 

Application Behavior

Now let's take a look at how the application behaves and how the described objects work together and are made persistent when there's an incoming call.

Incoming Call

When there's an incoming call Android throws a "broadcast intent", a signal that alerts apps the call should be handled. QuiteSleep implements a "broadcast receiver" that listens to this broadcast message. Once the message is intercepted QuiteSleep performs a series of checks such as whether the phone is already muted and if the app itself is in active state.

If these checks are passed a background process is launched via an Android service that checks whether the incoming call number belongs to a blocked contact. This is when the CallLog object is created and stored in the database to later have a record for the call.

If the call was effectively silenced according to the specified day and time frame the independent threads are created to handle notifications (SMS or e-mail). This is performed before making the CallLog object persistent on db4o to keep a record of sent notifications.

Call Termination

Once a call has been silienced a hang-up event (another broadcast intent) will follow which is intercepted by the app via a different broadcast receiver. The wrap-up actions are then the following:

  • Check whether the phone is in mute mode via the app (if it's not this is an unrelated hang up event that shouldn't be handled)
  • Check whether the QuiteSleep service is active
  • Check whether a hang-up is already being processed by the app

If all the checks above are positive a final background thread handles restoring the phone to it's original state to allow for the normal handling of incoming calls.

Conclusion

In QuiteSleep db4o is up to the task of dealing with all persistence operations in the app and makes persistence related code simpler.

In César Valiente's own words:

db4o is an excellent option for Android apps that don't need to use the complexity of relational databases, with a few lines of code you can create a complete data model for your app, using queries, inserts, deletes, updates and index configurations...  and more importantly, the class model in your app is the data model in your object database!!

 Still not using an object database? What are you waiting for? =)

Published at DZone with permission of its author, German Viscuso.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Lucas Godoy replied on Thu, 2010/09/09 - 9:25am

Hi

First of all, I want to say that I never used db4o, but I've seen examples of its usage a lot cleaner than this.
QuiteSleep may get the job done, but I don't think this code is simpler. 143 lines only to sync contacts?
In fact, I think it is as ugly as not using db4o at all.
And I don't like the idea of methods called "d"or "e" in any context.

Just an opinion.
Thanks for the article, anyway.
Cheers.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.