Detectar lo que se modificó de ABAddressBookRegisterExternalChangeCallback

Estoy usando ABAddressBookRegisterExternalChangeCallback para get los cambios externos en AddressbookBook del usuario. Estoy usando el siguiente código para registrar la callback:

ABAddressBookRef ntificationaddressbook = ABAddressBookCreate(); ABAddressBookRegisterExternalChangeCallback(ntificationaddressbook, MyAddressBookExternalChangeCallback, self); 

y cuando se llama a esta callback, se llama a MyAddressBookExternalChangeCallback con éxito

 void MyAddressBookExternalChangeCallback (ABAddressBookRef ntificationaddressbook,CFDictionaryRef info,void *context) { NSLog(@"Changed Detected......"); } 

Tengo las siguientes preguntas:

  1. ¿Cómo puedo detectar qué contactos se modificaron y qué acción (ADD, Update, Delete) se realizó en ese contacto? Necesito get el recordID de ese contacto. Es posible ?

Lamentablemente, si eso no es posible, ¿cómo las aplicaciones como viber, tango, watsapp obtienen la información de cambio?

  1. Recibo el método de callback llamado solo cuando la aplicación está en segundo plano si la aplicación termina, hay alguna manera de get la notificación de cambios.

Por favor ayuda. Gracias por adelantado.

Encontré la solución para encontrar lo que había cambiado en la libreta de direcciones. No estoy seguro de si es preciso, pero cómo funciona eso.

Crear una notificación de rellamada para la libreta de direcciones:

  ABAddressBookRef ntificationaddressbook = ABAddressBookCreate(); ABAddressBookRegisterExternalChangeCallback(ntificationaddressbook, MyAddressBookExternalChangeCallback, self); 

Que agregue el siguiente código cuando se produce la notificación: –

 void MyAddressBookExternalChangeCallback (ABAddressBookRef ntificationaddressbook,CFDictionaryRef info,void *context) { NSTimeInterval timeStampone = [[NSDate date] timeIntervalSince1970]; NSNumber *timeStamponeobj= [NSNumber numberWithDouble: timeStampone]; NSLog(@"Start Process"); NSUserDefaults *userdefs = [[NSUserDefaults alloc]init]; NSMutableArray *arraytemp = [[NSMutableArray alloc]init]; CFArrayRef peopleRefs = ABAddressBookCopyArrayOfAllPeopleInSource(ntificationaddressbook, kABSourceTypeLocal); NSMutableArray *changedrecords = [[NSMutableArray alloc]init]; ABAddressBookRevert(ntificationaddressbook); CFIndex count = CFArrayGetCount(peopleRefs); for (CFIndex i=0; i < count; i++) { ABRecordRef ref = CFArrayGetValueAtIndex(peopleRefs, i); NSDate* datemod = ( NSDate *)(ABRecordCopyValue(ref, kABPersonModificationDateProperty)); NSDate *lastopened = [userdefs valueForKey:@"LastOpenedDate"]; NSTimeInterval datemodifiedtime =[datemod timeIntervalSince1970]; NSNumber *modifieddatenumber= [NSNumber numberWithDouble: datemodifiedtime]; NSTimeInterval lastopeddate = [lastopened timeIntervalSince1970]; NSNumber *lastopenednumber= [NSNumber numberWithDouble: lastopeddate]; if ([modifieddatenumber intValue]>[lastopenednumber intValue]) { ABRecordRef aSource = CFArrayGetValueAtIndex(peopleRefs,i); int recordid = ABRecordGetRecordID(aSource); [changedrecords addObject:[NSString stringWithFormat:@"%d",recordid]]; } } NSString *arraystring= [changedrecords componentsJoinedByString:@","]; [userdefs setValue:arraystring forKey:@"ArrayOfChangedPeopleString"]; [userdefs setValue:changedrecords forKey:@"ArrayOfChangedPeople"]; //----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- if ([arraystring isEqualToString:@"(null)"]) { arraystring = [arraystring stringByReplacingOccurrencesOfString:@"(null)" withString:@""]; } NSLog(@"arraychangedstring= %@ and length = %d",arraystring,arraystring.length); NSLog(@"Must send data to the server here for this changed people array= %@",changedrecords); if (arraystring.length>2) { NSLog(@"inside if"); ABAddressBookRef addressBook = ABAddressBookCreate(); CFArrayRef allSources = ABAddressBookCopyArrayOfAllPeople( addressBook ); [userdefs setValue:@"" forKey:@"ArrayOfChangedPeopleString"]; [userdefs setValue:@"" forKey:@"ArrayOfChangedPeople"]; for (int i = 0; i<[changedrecords count]; i++) { NSString *recordid= [changedrecords objectAtIndex:i]; int nPeople = [recordid intValue]; [IntoochUtil current_function_name:@"Inside getcontactdetails - Start"]; if(nPeople != 0){ // for (CFIndex i = nPeople_start; i < (nPeople_end); i++) // { // NSString *reocrdid = [NSString stringWithFormat: @"Record %ld",i]; // NSLog(@"Recordid= %@",reocrdid); NSMutableDictionary *dict = [[NSMutableDictionary alloc]init]; //common field NSString *first_name =nil; NSString *mid_name = nil ; NSString *last_name =nil ; NSString *basic_email =nil ; NSString *basic_mobile =nil ; NSString *other_phone=nil; NSString *pager_phone= nil; //personal filed NSString *Home_email =nil ; NSString*home_mobile =nil ; NSString*home_address =nil; //business filed NSString *work_email =nil ; NSString *company_name =nil ; NSString *job_title =nil ; NSString*work_mobile =nil ; NSString *iphone = nil; NSString *main = nil; NSString *work_address =nil ; NSString *home_fax, *work_fax, *other_fax =nil; NSString *birthday = nil; // NSString *blogs = nil; NSString *record_id; ABRecordRef aSource = ABAddressBookGetPersonWithRecordID(addressBook, nPeople); // Fetch all groups included in the current source // Getting Record id from address book int recordid = ABRecordGetRecordID(aSource); record_id = [NSString stringWithFormat:@"%d",recordid]; // Getting Names from address book // CFStringRef firstName = ABRecordCopyValue(aSource, kABPersonFirstNameProperty); first_name= ABRecordCopyValue(aSource, kABPersonFirstNameProperty); // first_name=[NSString stringWithFormat:@"%@",firstName]; // CFRelease(firstName); // NSLog(@"First name= %@",first_name); NSMutableDictionary *dict_name = [[NSMutableDictionary alloc]init]; if (!([first_name isEqualToString:@"(null)"]|| first_name == nil || first_name.length ==0)) { [dict_name setValue:first_name forKey:@"FirstName"]; } [first_name release]; mid_name= ABRecordCopyValue(aSource, kABPersonMiddleNameProperty); // NSLog(@"mid name = %@",mid_name); // CFRelease(midname); if (!([mid_name isEqualToString:@"(null)"]|| mid_name == nil || mid_name.length ==0)) { [dict_name setValue:mid_name forKey:@"MiddleName"]; } [mid_name release]; last_name = ABRecordCopyValue(aSource, kABPersonLastNameProperty); // NSLog(@"Last Name= %@",last_name); // CFRelease(lastName); if (!([last_name isEqualToString:@"(null)"]|| last_name == nil || last_name.length ==0)) { [dict_name setValue:last_name forKey:@"LastName"]; } [last_name release]; NSString *dictNamestring = [NSString stringWithFormat:@"%@",dict_name]; if (!([dictNamestring isEqualToString:@"(null)"]|| dictNamestring == nil || dictNamestring.length <4)) { [dict setValue:dict_name forKey:@"Names"]; } // [dict_name release]; // Getting Phone numbers from address book NSMutableDictionary *dict_phone = [[NSMutableDictionary alloc]init]; ABMultiValueRef phones =(NSString*)ABRecordCopyValue(aSource, kABPersonPhoneProperty); NSString* mobileLabel; for(CFIndex i = 0; i < ABMultiValueGetCount(phones); i++) { mobileLabel = (NSString*)ABMultiValueCopyLabelAtIndex(phones, i); if([mobileLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]) { home_mobile = (NSString*)ABMultiValueCopyValueAtIndex(phones, i) ; [dict_phone setValue:home_mobile forKey:@"Mobile"]; } else if ([mobileLabel isEqualToString:(NSString*)kABHomeLabel]) { basic_mobile = (NSString*)ABMultiValueCopyValueAtIndex(phones, i); [dict_phone setValue:basic_mobile forKey:@"Home"]; } else if([mobileLabel isEqualToString:(NSString *)kABWorkLabel]) { work_mobile = (NSString*)ABMultiValueCopyValueAtIndex(phones, i); [dict_phone setValue:work_mobile forKey:@"Work"]; } else if([mobileLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) { iphone = (NSString*)ABMultiValueCopyValueAtIndex(phones, i); [dict_phone setValue:iphone forKey:@"iPhone"]; } else if([mobileLabel isEqualToString:(NSString *)kABPersonPhoneMainLabel]) { main = (NSString*)ABMultiValueCopyValueAtIndex(phones, i); [dict_phone setValue:main forKey:@"Main"]; } else if ([mobileLabel isEqualToString:(NSString*)kABOtherLabel]) { other_phone = (NSString*)ABMultiValueCopyValueAtIndex(phones, i); [dict_phone setValue:other_phone forKey:@"OtherPhone"]; } else if ([mobileLabel isEqualToString:(NSString*)kABPersonPhonePagerLabel]) { pager_phone = (NSString*)ABMultiValueCopyValueAtIndex(phones, i); [dict_phone setValue:pager_phone forKey:@"Pager"]; } else { NSLog(@"%@ label is not processed",mobileLabel); } } CFRelease(phones); NSString *dictphonestring = [NSString stringWithFormat:@"%@",dict_phone]; if (!([dictphonestring isEqualToString:@"(null)"]|| dictphonestring == nil || dictphonestring.length <4)) { [dict setValue:dict_phone forKey:@"PhoneNos"]; } // [dict_phone release]; // Getting Fax numbers from address book NSMutableDictionary *dict_fax = [[NSMutableDictionary alloc]init]; ABMultiValueRef faxes =(NSString*)ABRecordCopyValue(aSource, kABPersonPhoneProperty); NSString* FaxLabel; for(CFIndex i = 0; i < ABMultiValueGetCount(faxes); i++) { FaxLabel = (NSString*)ABMultiValueCopyLabelAtIndex(faxes, i); if([FaxLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]) { home_fax = (NSString*)ABMultiValueCopyValueAtIndex(faxes, i) ; [dict_fax setValue:home_fax forKey:@"Home"]; } if ([FaxLabel isEqualToString:(NSString*)kABPersonPhoneWorkFAXLabel]) { work_fax = (NSString*)ABMultiValueCopyValueAtIndex(faxes, i); [dict_fax setValue:work_fax forKey:@"Work"]; } if([FaxLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) { other_fax = (NSString*)ABMultiValueCopyValueAtIndex(faxes, i); [dict_fax setValue:other_fax forKey:@"Other"]; } } CFRelease(faxes); NSString *dictfaxstring = [NSString stringWithFormat:@"%@",dict_fax]; if (!([dictfaxstring isEqualToString:@"(null)"]|| dictfaxstring == nil || dictfaxstring.length <4)) { [dict setValue:dict_fax forKey:@"FaxNos"]; } [dict_fax release]; // Getting emails from address book ABMultiValueRef email = ABRecordCopyValue(aSource, kABPersonEmailProperty); NSMutableDictionary *dict_email = [[NSMutableDictionary alloc]init]; NSString* email_lbl; for(CFIndex i = 0; i < ABMultiValueGetCount(email); i++) { email_lbl = (NSString*)ABMultiValueCopyLabelAtIndex(email, i); if([email_lbl isEqualToString:(NSString *)kABHomeLabel]) { Home_email = [(NSString*)ABMultiValueCopyValueAtIndex(email, i)retain]; [dict_email setValue:Home_email forKey:@"Home"]; } else if([email_lbl isEqualToString:(NSString *)kABWorkLabel]) { work_email = [(NSString*)ABMultiValueCopyValueAtIndex(email, i)retain]; [dict_email setValue:work_email forKey:@"Work"]; } else if([email_lbl isEqualToString:(NSString *)kABOtherLabel]) { basic_email = [(NSString*)ABMultiValueCopyValueAtIndex(email, i)retain]; [dict_email setValue:basic_email forKey:@"Other"]; } else { NSLog(@"%@ label is processed as other",email_lbl); basic_email = [(NSString*)ABMultiValueCopyValueAtIndex(email, i)retain]; [dict_email setValue:basic_email forKey:@"Other"]; } } CFRelease(email); NSString *dictemailstring = [NSString stringWithFormat:@"%@",dict_email]; if (!([dictemailstring isEqualToString:@"(null)"]|| dictemailstring == nil || dictemailstring.length <4)) { [dict setValue:dict_email forKey:@"Emails"]; } // [dict_email release]; company_name = ABRecordCopyValue(aSource, kABPersonOrganizationProperty); // NSLog(@"Company name= %@",company_name); job_title = ABRecordCopyValue(aSource, kABPersonJobTitleProperty); // NSLog(@"Job title= %@",job_title); NSDate *birthdate = ABRecordCopyValue(aSource, kABPersonBirthdayProperty); NSString *birthdate_string = [NSString stringWithFormat:@"%@",birthdate]; if (!([birthdate_string isEqualToString:@"(null)"]|| birthdate_string == nil || birthdate_string.length ==0)) { NSDateComponents *components = [[NSCalendar currentCalendar] components:NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit fromDate:birthdate]; NSInteger day = [components day]; NSInteger month = [components month]; NSInteger year = [components year]; birthday = [NSString stringWithFormat:@"%d-%d-%d",day,month,year]; NSLog(@"birthday= %@",birthday); } [birthdate release]; // Getting addressess from address book ABMultiValueRef address =(NSString*)ABRecordCopyValue(aSource, kABPersonAddressProperty); NSDictionary *dict_address = [[NSMutableDictionary alloc]init]; NSString* addLabel; for(CFIndex i = 0; i < ABMultiValueGetCount(address); i++) { addLabel=(NSString*)ABMultiValueCopyLabelAtIndex(address, i); if ([addLabel isEqualToString:(NSString*)kABWorkLabel]) { NSMutableDictionary *dict_work_add = [(NSMutableDictionary *)ABMultiValueCopyValueAtIndex(address, i)retain]; work_address=[NSString stringWithFormat:@"%@ %@ %@ %@ %@",[dict_work_add objectForKey:@"Street"],[dict_work_add objectForKey:@"City"],[dict_work_add objectForKey:@"State"],[dict_work_add objectForKey:@"Country"],[dict_work_add objectForKey:@"ZIP"]] ; work_address = [work_address stringByReplacingOccurrencesOfString:@"(null)" withString:@""]; work_address = [work_address stringByReplacingOccurrencesOfString:@"\n" withString:@""]; // NSLog(@"work address = [%@] and length= %d",work_address,work_address.length); if (!([work_address isEqualToString:@"(null)"]|| work_address == nil || work_address.length ==0)) { [dict_address setValue:work_address forKey:@"Work"]; } } if ([addLabel isEqualToString:(NSString*)kABHomeLabel]) { NSMutableDictionary *dict_home_add = [(NSMutableDictionary *)ABMultiValueCopyValueAtIndex(address, i)retain]; home_address = [NSString stringWithFormat:@"%@ %@ %@ %@ %@",[dict_home_add objectForKey:@"Street"],[dict_home_add objectForKey:@"City"],[dict_home_add objectForKey:@"State"],[dict_home_add objectForKey:@"Country"],[dict_home_add objectForKey:@"ZIP"]]; home_address = [home_address stringByReplacingOccurrencesOfString:@"(null)" withString:@""]; home_address = [home_address stringByReplacingOccurrencesOfString:@"\n" withString:@""]; // NSLog(@"home address = [%@] and length= %d",home_address,home_address.length); if (!([home_address isEqualToString:@"(null)"]|| home_address == nil || home_address.length ==0)) { [dict_address setValue:home_address forKey:@"Home"]; } } } CFRelease(address); NSString *dictaddrssstring = [NSString stringWithFormat:@"%@",dict_address]; if (!([dictaddrssstring isEqualToString:@"(null)"]|| dictaddrssstring == nil || dictaddrssstring.length <4)) { [dict setValue:dict_address forKey:@"Addresses"]; } [dict_address release]; // Getting Social Networks from address book ABMultiValueRef social =(NSString*)ABRecordCopyValue(aSource, kABPersonSocialProfileProperty); NSMutableDictionary *dict_social = [[NSMutableDictionary alloc]init]; if (social) { for (int i = 0 ; i < ABMultiValueGetCount(social); i++) { NSDictionary *socialDict = (NSDictionary *)ABMultiValueCopyValueAtIndex(social, i); NSString *twitterid; if ([socialDict[@"service"] isEqualToString:(NSString*)kABPersonSocialProfileServiceTwitter]) { twitterid = (NSString*)socialDict[@"username"]; [dict_social setValue:twitterid forKey:@"Twitter"]; } NSString *facebookid; if ([socialDict[@"service"] isEqualToString:(NSString*)kABPersonSocialProfileServiceFacebook]) { facebookid = (NSString*)socialDict[@"username"]; [dict_social setValue:facebookid forKey:@"Facebook"]; } NSString *linkedinid; if ([socialDict[@"service"] isEqualToString:(NSString*)kABPersonSocialProfileServiceLinkedIn]) { linkedinid = (NSString*)socialDict[@"username"]; [dict_social setValue:linkedinid forKey:@"LinkedIn"]; } NSString *gamecenterid; if ([socialDict[@"service"] isEqualToString:(NSString*)kABPersonSocialProfileServiceGameCenter]) { gamecenterid = (NSString*)socialDict[@"username"]; [dict_social setValue:gamecenterid forKey:@"GameCenter"]; } NSString *myspaceid; if ([socialDict[@"service"] isEqualToString:(NSString*)kABPersonSocialProfileServiceMyspace]) { myspaceid = (NSString*)socialDict[@"username"]; [dict_social setValue:myspaceid forKey:@"MySpace"]; } NSString *flickerid; if ([socialDict[@"service"] isEqualToString:(NSString*)kABPersonSocialProfileServiceFlickr]) { flickerid = (NSString*)socialDict[@"username"]; [dict_social setValue:flickerid forKey:@"Flicker"]; } [socialDict release]; } CFRelease(social); } NSString *dictsocialstring = [NSString stringWithFormat:@"%@",dict_social]; if (!([dictsocialstring isEqualToString:@"(null)"]|| dictsocialstring == nil || dictsocialstring.length <4)) { [dict setValue:dict_social forKey:@"SocialNetworks"]; } [dict_social release]; // Getting Other Fields from address book NSMutableDictionary *dict_others = [[NSMutableDictionary alloc]init]; if (!([company_name isEqualToString:@"(null)"]||company_name == nil || company_name.length ==0)) { [dict_others setValue:company_name forKey:@"CompanyName"]; } if (!([job_title isEqualToString:@"(null)"]||job_title == nil || job_title.length ==0)) { [dict_others setValue:job_title forKey:@"JobTitle"]; } if (!([birthday isEqualToString:@"1-1-2001"]||[birthday isEqualToString:@"(null)"]||birthday == nil || birthday.length ==0)) { [dict_others setValue:birthday forKey:@"BirthDay"]; } NSString *dictothersstring = [NSString stringWithFormat:@"%@",dict_others]; if (!([dictothersstring isEqualToString:@"(null)"]|| dictothersstring == nil || dictothersstring.length <4)) { [dict setValue:dict_others forKey:@"OtherInfo"]; } [dict_others release]; [dict setValue:record_id forKey:@"RecordID"]; //This NSLog(@"Value for Dict is========= %@ =========",dict); NSString *namedictionstring = [NSString stringWithFormat:@"%@",dict_name]; NSString *emaildictstring = [NSString stringWithFormat:@"%@",dict_email]; NSString *phonedictstring = [NSString stringWithFormat:@"%@",dict_phone]; // NSLog(@"name dict length= %d email dict length= %d phone dict length = %d",namedictionstring.length,emaildictstring.length,phonedictstring.length); // [array_addressbook addObject:dict]; if (phonedictstring.length >3) { [arraytemp addObject:dict]; } else { if (emaildictstring.length>3 && namedictionstring.length) { [arraytemp addObject:dict] ; } else { NSLog(@"Contact is not valid= %@",dict_name); } } [dict_name release]; [dict_phone release]; [dict_email release]; [dict release]; [company_name release]; [job_title release]; /* CFRelease(firstName); CFRelease(midname); CFRelease(lastName); CFRelease(jobtitle); CFRelease(birthday_c); CFRelease(companyname);*/ // } } } NSString *json_array_string = [arraytemp JSONRepresentation]; [IntoochUtil current_function_name:@"Inside getcontactdetails - End"]; NSLog(@"Json String To send= %@",json_array_string); if (allSources) { CFRelease(allSources); } if (addressBook) { CFRelease(addressBook); } } else { NSLog(@"This is empty.... nothing to perform"); NSMutableArray *arrayofdeletedpeoples= [[NSMutableArray alloc]init]; ABAddressBookRef addressbook = ABAddressBookCreate(); CFArrayRef allPeoples= ABAddressBookCopyArrayOfAllPeople(addressbook); NSMutableArray *arrayofrecordIDCallback = [[NSMutableArray alloc]init]; int count = CFArrayGetCount(allPeoples); for (int i =0; i<count; i++) { ABRecordRef aSource = CFArrayGetValueAtIndex(allPeoples,i); NSString * recordid = [NSString stringWithFormat:@"%d",ABRecordGetRecordID(aSource)]; [arrayofrecordIDCallback addObject:recordid]; } NSLog(@"arrayofrecordIDCallback = %@ and count= %d",arrayofrecordIDCallback,[arrayofrecordIDCallback count]); NSMutableArray *arrayofallrecordids = [userdefs valueForKey:@"AllRecordIDSBackGround"]; NSLog(@"arrayofallrecordids= %@ and count= %d",arrayofallrecordids,[arrayofallrecordids count]); for (int i=0 ; i< [arrayofallrecordids count]; i++) { if ([arrayofrecordIDCallback containsObject:[arrayofallrecordids objectAtIndex:i]]) { } else { [arrayofdeletedpeoples addObject:[arrayofallrecordids objectAtIndex:i]]; } } NSLog(@"Array of deleted peoples= %@",arrayofdeletedpeoples); [arrayofdeletedpeoples release]; [arrayofallrecordids release]; [arrayofrecordIDCallback release]; if (allPeoples) { CFRelease(allPeoples); } if (addressbook) { CFRelease(addressbook); } } //----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [changedrecords release]; [userdefs release]; [arraytemp release]; NSLog(@"Ends Process"); NSTimeInterval timeStamptwo = [[NSDate date] timeIntervalSince1970]; NSNumber *timeStamptwoObj = [NSNumber numberWithDouble: timeStamptwo]; NSLog(@"Time Difference= %d",[timeStamptwoObj intValue]-[timeStamponeobj intValue]); } 

Sí, es cierto que no puede recibir notifications específicas en los cambios en la agenda telefónica. Deberá sincronizar sus datos cuando se active la notificación de callback.

 void MyAddressBookExternalChangeCallback (ABAddressBookRef ntificationaddressbook,CFDictionaryRef info,void *context) { NSLog(@"Changed Detected......"); // synch data } 

Las aplicaciones como viber, tango, watsapp también hacen lo mismo pero en hilo de background .

Si la aplicación termina, deberá sincronizar datos cuando la aplicación didFinishLaunchingWithOptions como en didFinishLaunchingWithOptions .

Hasta donde sé, no se puede saber cuál es la diferencia: yo mismo me topé con este problema.

Mi solución fue:

Tenía una libreta de direcciones local basada en Core Data, y cada vez que recibía esta notificación, ejecutaba un hilo de background que sincronizaba los datos y notificaba al final todos los cambios.

Durante la primera vez, estoy descargando todos los datos en la database local usando sqlite, luego en un cambio externo estoy descargando nuevos datos de la database de contactos en dummy db en sqlite, y luego ejecutando una gran consulta de comparación larga según lo que ha cambiado. En nuestro caso, el foco principal estaba en los numbers de teléfono y el nombre. Una vez que obtenga el cambio, actualícelo en su database de contactos en sqlite. Creo que incluso WhatsApp está haciendo algo similar. Comprobé el time empleado para reflejar los cambios de WhatsApp y nuestra aplicación es casi similar. En realidad, mi aplicación es 2 segundos más rápida que WhatsApp en términos de reflejar los cambios. Espero que esto te ayude.