Wednesday, July 3, 2013

Backup, Remove and Restore your Contacts using PhoneGap

A couple of people have had questions on how to do this recently so I thought I would do a write up on it. As well, it illustrates how you avoid using loops with asynchronous code. Although for an even better explanation of that topic you'll want to read Item 64: Use Recursion for Asynchronous Loops from David Herman's book, Effective JavaScript. Chapter 7 on Concurrency is worth the purchase price of the book but I digress...

First a warning. Try all the code out on an emulator first. The methods below will completely wipe the contacts from your device so you'll want to make sure the backup step works first before continuing. You've been warned!

Anyway, if you want to backup the contacts on your device to a file you'd use the following process:
  1. Find all the contacts
  2. Request a file system object
  3. Create a FileEntry object
  4. Create a FileWriter
  5. Write the JSON data to file
The code in which to accomplish those tasks is as follows: Once you see the "backup complete" message in the console you'll have a file called "contacts.bak" in the root directory of your file system. For Android users that will probably be /sdcard and for iOS, etc. it would be in the applications sandbox. If you take a look at the file you will see something like this: If you are seeing what looks like your complete contact database in text format then you are ready to proceed.

Next we will delete all the contacts on the device. The steps are:
  1. Find all the contacts
  2. Recurse through the contacts deleting one at a time.
The code looks like:

This might look a little bit weird at first glance but trust me it'll make sense. You'll notice in deleteAllTheContacts the first thing we do is to create a local function called deleteContacts. This is the method that will actually remove the contacts from the device. Then after the definition of deleteContacts we call navigator.contacts.find(). This call will get an array of all the contacts on the device and call it's success function which is deleteContacts.

Now in deleteContacts we do a check to see if the length of the contacts array is zero. If it is zero then we are done, there are no more contacts left to be deleted. If the contact array length is greater than zero we have more work to do. We'll pop the next Contact object off of the contacts array, which reduces the size of the array by one and we'll call the remove method of the Contact object. The success call back for remove method is the deleteContacts method. Keep reading this paragraph until all of your contacts have been deleted. Boom recursion.

But wait, you are wondering how could this possibly work. Your thinking I've got 7 quintillion contacts and there is no way the call stack can support that many recursive calls. Ah, but you are forgetting that asynchronous calls return immediately so they never eat up the call stack. If you tried doing this with a for loop you would blow up the call stack causing your program to crash if you had enough contacts and even if you didn't kill your app how would you know when all of those async calls to remove were complete without doing a lot of JavaScript gymnastics. Just use the recursion approach.

Finally you'll want to be able to restore the contacts you've previously saved to file. I've broken it down into two separate methods to make it easier to read:
  1. Request local file system
  2. Get the FileEntry
  3. Request the File object
  4. Read the data and parse it to JSON
  5. Recurse through all the contacts and save them to the device

This is pretty much just unrolling the two previous steps of backing up and deleting the contacts. If you've gotten this far you should be able to understand what is going on. Although there are two lines I want to draw your attention to:
contactData.id = null;
contactData.rawId = null;
What I'm doing here is removing the unique ID's from the contact. If you skip this step you will signal the API that you are attempting to modify an existing contact and the save will most probably fail. Hopefully this helps a bunch of folks.

10 comments:

Ahmed Ramadan said...

thank you soooo much :)

Jon Ander Romero Martínez said...

Hi Simon,

im trying to get this approach working in my own phonegap app but im having some issues with a Galaxy S2 terminal.

It seems to work, but when i create or delete more than 150 contacts phonegap gets and Error Code = 0 (Unknown Error i think) and not only my app is stoping, but the contacts native app from android is stoping aswell. I don't know how to solve this.

Any lead would be appreciated :-)

Thanks.

Simon MacDonald said...

@Jon Ander Romero Martínez

Reproduce the issue with "adb logcat" running and it should give you more details with regards to the error. Lemme know what you see.

Jon Ander Romero Martínez said...

Hi again Simon,

This is what I get when the app crashes:

08-12 13:53:05.715: D/dalvikvm(15062): GC_CONCURRENT freed 775K, 14% free 11049K/12807K, paused 1ms+7ms
08-12 13:53:07.480: W/CursorWrapperInner(15062): Cursor finalized without prior close()
08-12 13:53:07.480: W/CursorWrapperInner(15062): Cursor finalized without prior close()
08-12 13:53:07.480: W/CursorWrapperInner(15062): Cursor finalized without prior close()
08-12 13:53:07.480: W/CursorWrapperInner(15062): Cursor finalized without prior close()
08-12 13:53:07.480: D/dalvikvm(15062): GC_EXPLICIT freed 1623K, 27% free 9459K/12807K, paused 1ms+2ms
08-12 13:53:15.310: W/PluginManager(15062): THREAD WARNING: exec() call to Contacts.remove blocked the main thread for 17ms. Plugin should use CordovaInterface.getThreadPool().
08-12 13:53:26.025: W/dalvikvm(15062): threadid=18: thread exiting with uncaught exception (group=0x40c401f8)
08-12 13:53:26.025: E/AndroidRuntime(15062): FATAL EXCEPTION: pool-1-thread-2
08-12 13:53:26.025: E/AndroidRuntime(15062): java.lang.NullPointerException
08-12 13:53:26.025: E/AndroidRuntime(15062): at org.apache.cordova.core.ContactAccessorSdk5.remove(ContactAccessorSdk5.java:1790)
08-12 13:53:26.025: E/AndroidRuntime(15062): at org.apache.cordova.core.ContactManager$3.run(ContactManager.java:109)
08-12 13:53:26.025: E/AndroidRuntime(15062): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
08-12 13:53:26.025: E/AndroidRuntime(15062): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
08-12 13:53:26.025: E/AndroidRuntime(15062): at java.lang.Thread.run(Thread.java:856)
08-12 13:53:48.995: D/OpenGLRenderer(15062): Flushing caches (mode 0)
08-12 13:53:49.680: D/OpenGLRenderer(15062): Flushing caches (mode 1)

Sometimes it crashes with 150+ contacts, sometimes 200+, but always crashes in the Galaxy S2. I don't know if is a terminal only issue o general in android.

I hope that you can see anything there that could help me :-)

Thanks!

Simon MacDonald said...

@Jon Ander Romero Martínez

Ah cool a NullPointerException. What version of PhoneGap are you using so I can look at the correct line.

Jon Ander Romero Martínez said...

Hi,

I'm using phonegap 3.0.

Simon MacDonald said...

@Jon Ander Romero Martínez

Well that is pretty weird, the cursor is null. Open the ContactAccessorSdk5.java file in your project then go to line 1790 and change it from:

if (cursor.getCount() == 1) {

to:

if (cursor != null && cursor.getCount() == 1) {

and it should guard against the NullPointerException and keep the app from crashing. Let me know how it goes.

Jon Ander Romero Martínez said...

Hi Simon,

I'm getting a new error and I think it has something to do with the opened cursors and a memory issue.

08-13 09:49:32.930: E/CursorWindow(2391): Could not allocate CursorWindow '/data/data/com.android.providers.contacts/databases/contacts2.db' of size 2097152 due to error -12.
08-13 09:49:32.935: E/JavaBinder(2391): *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
08-13 09:49:32.935: E/JavaBinder(2391): android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=758 (# cursors opened by pid 4836=502) (# cursors opened by pid 4940=256)
08-13 09:49:32.935: E/JavaBinder(2391): at android.database.CursorWindow.(CursorWindow.java:104)
08-13 09:49:32.935: E/JavaBinder(2391): at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
08-13 09:49:32.935: E/JavaBinder(2391): at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:162)
08-13 09:49:32.935: E/JavaBinder(2391): at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:156)
08-13 09:49:32.935: E/JavaBinder(2391): at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:161)
08-13 09:49:32.935: E/JavaBinder(2391): at android.database.AbstractCursor.moveToNext(AbstractCursor.java:209)
08-13 09:49:32.935: E/JavaBinder(2391): at com.android.providers.contacts.ContactsProvider2.lookupContactIdByRawContactIds(ContactsProvider2.java:9728)
08-13 09:49:32.935: E/JavaBinder(2391): at com.android.providers.contacts.ContactsProvider2.lookupContactIdByLookupKey(ContactsProvider2.java:9610)
08-13 09:49:32.935: E/JavaBinder(2391): at com.android.providers.contacts.ContactsProvider2.deleteInTransaction(ContactsProvider2.java:5020)
08-13 09:49:32.935: E/JavaBinder(2391): at com.android.providers.contacts.AbstractContactsProvider.delete(AbstractContactsProvider.java:123)
08-13 09:49:32.935: E/JavaBinder(2391): at com.android.providers.contacts.ContactsProvider2.delete(ContactsProvider2.java:2656)
08-13 09:49:32.935: E/JavaBinder(2391): at android.content.ContentProvider$Transport.delete(ContentProvider.java:213)
08-13 09:49:32.935: E/JavaBinder(2391): at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:192)
08-13 09:49:32.935: E/JavaBinder(2391): at android.os.Binder.execTransact(Binder.java:338)
08-13 09:49:32.935: E/JavaBinder(2391): at dalvik.system.NativeStart.run(Native Method)

Thank you.

Jon Ander Romero Martínez said...

Hi again Simon,

I have been doing some tests and after closing all the cursors in the ContactAccessorSdk5.java file everything is working. In fact, the contacts are deleting faster than before.

Thank you very much for your help.

Simon MacDonald said...

@Jon Ander Romero Martínez

Awesome, glad to hear it is working. Can you share your code changes so we can get them into the repo and fix it for everyone?