diff --git a/android/build.gradle b/android/build.gradle index f9b739cb..4f0d56c7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,8 +9,14 @@ buildscript { } } -apply plugin: 'com.android.library' + def isNewArchitectureEnabled() { + return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" +} +apply plugin: 'com.android.library' +if (isNewArchitectureEnabled()) { + apply plugin: 'com.facebook.react' +} def DEFAULT_MIN_SDK_VERSION = 21 def DEFAULT_COMPILE_SDK_VERSION = 29 def DEFAULT_TARGET_SDK_VERSION = 29 @@ -30,6 +36,20 @@ android { targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION versionCode 1 versionName "1.0" + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) + } + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + "src/newarch", + // This is needed to build Kotlin project with NewArch enabled + "${project.buildDir}/generated/source/codegen/java" + ] + } else { + java.srcDirs += ["src/oldarch"] + } + } } lintOptions { abortOnError false @@ -43,3 +63,10 @@ repositories { dependencies { implementation 'com.facebook.react:react-native:+' } +if (isNewArchitectureEnabled()) { + react { + jsRootDir = file("../src/") + libraryName = "react-native-contacts" + codegenJavaPackageName = "com.rt2zz.reactnativecontacts" + } +} \ No newline at end of file diff --git a/android/src/main/java/com/rt2zz/reactnativecontacts/ContactsProvider.java b/android/src/main/java/com/rt2zz/reactnativecontacts/ContactsProvider.java index 1ead282e..927b6c8d 100644 --- a/android/src/main/java/com/rt2zz/reactnativecontacts/ContactsProvider.java +++ b/android/src/main/java/com/rt2zz/reactnativecontacts/ContactsProvider.java @@ -1,6 +1,7 @@ package com.rt2zz.reactnativecontacts; import android.content.ContentResolver; +import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; @@ -32,56 +33,63 @@ public class ContactsProvider { public static final int ID_FOR_PROFILE_CONTACT = -1; - private static final List JUST_ME_PROJECTION = new ArrayList() {{ - add((ContactsContract.Data._ID)); - add(ContactsContract.Data.CONTACT_ID); - add(ContactsContract.Data.RAW_CONTACT_ID); - add(ContactsContract.Data.LOOKUP_KEY); - add(ContactsContract.Data.STARRED); - add(ContactsContract.Contacts.Data.MIMETYPE); - add(ContactsContract.Profile.DISPLAY_NAME); - add(Contactables.PHOTO_URI); - add(StructuredName.DISPLAY_NAME); - add(StructuredName.GIVEN_NAME); - add(StructuredName.MIDDLE_NAME); - add(StructuredName.FAMILY_NAME); - add(StructuredName.PREFIX); - add(StructuredName.SUFFIX); - add(Phone.NUMBER); - add(Phone.NORMALIZED_NUMBER); - add(Phone.TYPE); - add(Phone.LABEL); - add(Email.DATA); - add(Email.ADDRESS); - add(Email.TYPE); - add(Email.LABEL); - add(Organization.COMPANY); - add(Organization.TITLE); - add(Organization.DEPARTMENT); - add(StructuredPostal.FORMATTED_ADDRESS); - add(StructuredPostal.TYPE); - add(StructuredPostal.LABEL); - add(StructuredPostal.STREET); - add(StructuredPostal.POBOX); - add(StructuredPostal.NEIGHBORHOOD); - add(StructuredPostal.CITY); - add(StructuredPostal.REGION); - add(StructuredPostal.POSTCODE); - add(StructuredPostal.COUNTRY); - add(Note.NOTE); - add(Website.URL); - add(Im.DATA); - add(Event.START_DATE); - add(Event.TYPE); - }}; - - private static final List FULL_PROJECTION = new ArrayList() {{ - addAll(JUST_ME_PROJECTION); - }}; - - private static final List PHOTO_PROJECTION = new ArrayList() {{ - add(Contactables.PHOTO_URI); - }}; + public static final String NAME = "RCTContacts"; + private static final List JUST_ME_PROJECTION = new ArrayList() { + { + add((ContactsContract.Data._ID)); + add(ContactsContract.Data.CONTACT_ID); + add(ContactsContract.Data.RAW_CONTACT_ID); + add(ContactsContract.Data.LOOKUP_KEY); + add(ContactsContract.Data.STARRED); + add(ContactsContract.Contacts.Data.MIMETYPE); + add(ContactsContract.Profile.DISPLAY_NAME); + add(Contactables.PHOTO_URI); + add(StructuredName.DISPLAY_NAME); + add(StructuredName.GIVEN_NAME); + add(StructuredName.MIDDLE_NAME); + add(StructuredName.FAMILY_NAME); + add(StructuredName.PREFIX); + add(StructuredName.SUFFIX); + add(Phone.NUMBER); + add(Phone.NORMALIZED_NUMBER); + add(Phone.TYPE); + add(Phone.LABEL); + add(Email.DATA); + add(Email.ADDRESS); + add(Email.TYPE); + add(Email.LABEL); + add(Organization.COMPANY); + add(Organization.TITLE); + add(Organization.DEPARTMENT); + add(StructuredPostal.FORMATTED_ADDRESS); + add(StructuredPostal.TYPE); + add(StructuredPostal.LABEL); + add(StructuredPostal.STREET); + add(StructuredPostal.POBOX); + add(StructuredPostal.NEIGHBORHOOD); + add(StructuredPostal.CITY); + add(StructuredPostal.REGION); + add(StructuredPostal.POSTCODE); + add(StructuredPostal.COUNTRY); + add(Note.NOTE); + add(Website.URL); + add(Im.DATA); + add(Event.START_DATE); + add(Event.TYPE); + } + }; + + private static final List FULL_PROJECTION = new ArrayList() { + { + addAll(JUST_ME_PROJECTION); + } + }; + + private static final List PHOTO_PROJECTION = new ArrayList() { + { + add(Contactables.PHOTO_URI); + } + }; private final ContentResolver contentResolver; @@ -97,9 +105,8 @@ public WritableArray getContactsMatchingString(String searchString) { FULL_PROJECTION.toArray(new String[FULL_PROJECTION.size()]), ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " LIKE ? OR " + Organization.COMPANY + " LIKE ?", - new String[]{"%" + searchString + "%", "%" + searchString + "%"}, - null - ); + new String[] { "%" + searchString + "%", "%" + searchString + "%" }, + null); try { matchingContacts = loadContactsFrom(cursor); @@ -117,7 +124,6 @@ public WritableArray getContactsMatchingString(String searchString) { return contacts; } - public WritableArray getContactsByPhoneNumber(String phoneNumber) { Map matchingContacts; { @@ -126,9 +132,8 @@ public WritableArray getContactsByPhoneNumber(String phoneNumber) { FULL_PROJECTION.toArray(new String[FULL_PROJECTION.size()]), ContactsContract.CommonDataKinds.Phone.NUMBER + " LIKE ? OR " + ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER + " LIKE ?", - new String[]{"%" + phoneNumber + "%", "%" + phoneNumber + "%"}, - null - ); + new String[] { "%" + phoneNumber + "%", "%" + phoneNumber + "%" }, + null); try { matchingContacts = loadContactsFrom(cursor); @@ -153,9 +158,8 @@ public WritableArray getContactsByEmailAddress(String emailAddress) { ContactsContract.Data.CONTENT_URI, FULL_PROJECTION.toArray(new String[FULL_PROJECTION.size()]), ContactsContract.CommonDataKinds.Email.ADDRESS + " LIKE ?", - new String[]{"%" + emailAddress + "%"}, - null - ); + new String[] { "%" + emailAddress + "%" }, + null); try { matchingContacts = loadContactsFrom(cursor); @@ -173,16 +177,17 @@ public WritableArray getContactsByEmailAddress(String emailAddress) { return contacts; } - public WritableMap getContactByRawId(String contactRawId) { + public WritableMap getContactByRawId(String contactRawId) { // Get Contact Id from Raw Contact Id - String[] projections = new String[]{ContactsContract.RawContacts.CONTACT_ID}; + String[] projections = new String[] { ContactsContract.RawContacts.CONTACT_ID }; String select = ContactsContract.RawContacts._ID + "= ?"; - String[] selectionArgs = new String[]{contactRawId}; - Cursor rawCursor = contentResolver.query(ContactsContract.RawContacts.CONTENT_URI, projections, select, selectionArgs, null); + String[] selectionArgs = new String[] { contactRawId }; + Cursor rawCursor = contentResolver.query(ContactsContract.RawContacts.CONTENT_URI, projections, select, + selectionArgs, null); String contactId = null; if (rawCursor.getCount() == 0) { - /*contact id not found */ + /* contact id not found */ } if (cursorMoveToNext(rawCursor)) { @@ -197,7 +202,7 @@ public WritableMap getContactByRawId(String contactRawId) { rawCursor.close(); - //Now that we have the real contact id, fetch information + // Now that we have the real contact id, fetch information return getContactById(contactId); } @@ -206,12 +211,11 @@ public WritableMap getContactById(String contactId) { Map matchingContacts; { Cursor cursor = contentResolver.query( - ContactsContract.Data.CONTENT_URI, - FULL_PROJECTION.toArray(new String[FULL_PROJECTION.size()]), - ContactsContract.RawContacts.CONTACT_ID + " = ?", - new String[]{contactId}, - null - ); + ContactsContract.Data.CONTENT_URI, + FULL_PROJECTION.toArray(new String[FULL_PROJECTION.size()]), + ContactsContract.RawContacts.CONTACT_ID + " = ?", + new String[] { contactId }, + null); try { matchingContacts = loadContactsFrom(cursor); @@ -222,17 +226,17 @@ public WritableMap getContactById(String contactId) { } } - if(matchingContacts.values().size() > 0) { + if (matchingContacts.values().size() > 0) { return matchingContacts.values().iterator().next().toMap(); } - return null; + return null; } public Integer getContactsCount() { - Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); + Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); int count = cursor.getCount(); - + return count; } @@ -240,12 +244,12 @@ public WritableArray getContacts() { Map justMe; { Cursor cursor = contentResolver.query( - Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI, ContactsContract.Contacts.Data.CONTENT_DIRECTORY), + Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI, + ContactsContract.Contacts.Data.CONTENT_DIRECTORY), JUST_ME_PROJECTION.toArray(new String[JUST_ME_PROJECTION.size()]), null, null, - null - ); + null); try { justMe = loadContactsFrom(cursor); @@ -262,27 +266,26 @@ public WritableArray getContacts() { ContactsContract.Data.CONTENT_URI, FULL_PROJECTION.toArray(new String[FULL_PROJECTION.size()]), ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=? OR " - + ContactsContract.Data.MIMETYPE + "=?", - new String[]{ - Email.CONTENT_ITEM_TYPE, - Phone.CONTENT_ITEM_TYPE, - StructuredName.CONTENT_ITEM_TYPE, - Organization.CONTENT_ITEM_TYPE, - StructuredPostal.CONTENT_ITEM_TYPE, - Note.CONTENT_ITEM_TYPE, - Website.CONTENT_ITEM_TYPE, - Im.CONTENT_ITEM_TYPE, - Event.CONTENT_ITEM_TYPE, + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=?", + new String[] { + Email.CONTENT_ITEM_TYPE, + Phone.CONTENT_ITEM_TYPE, + StructuredName.CONTENT_ITEM_TYPE, + Organization.CONTENT_ITEM_TYPE, + StructuredPostal.CONTENT_ITEM_TYPE, + Note.CONTENT_ITEM_TYPE, + Website.CONTENT_ITEM_TYPE, + Im.CONTENT_ITEM_TYPE, + Event.CONTENT_ITEM_TYPE, }, - null - ); + null); try { everyoneElse = loadContactsFrom(cursor); @@ -307,7 +310,7 @@ public WritableArray getContacts() { private Boolean cursorMoveToNext(Cursor cursor) { try { return cursor.moveToNext(); - } catch(RuntimeException error) { + } catch (RuntimeException error) { return false; } } @@ -328,22 +331,22 @@ private Map loadContactsFrom(Cursor cursor) { if (columnIndexContactId != -1) { contactId = cursor.getString(columnIndexContactId); } else { - //todo - double check this, it may not be necessary any more - contactId = String.valueOf(ID_FOR_PROFILE_CONTACT);//no contact id for 'ME' user + // todo - double check this, it may not be necessary any more + contactId = String.valueOf(ID_FOR_PROFILE_CONTACT);// no contact id for 'ME' user } if (columnIndexId != -1) { id = cursor.getString(columnIndexId); } else { - //todo - double check this, it may not be necessary any more - id = String.valueOf(ID_FOR_PROFILE_CONTACT);//no contact id for 'ME' user + // todo - double check this, it may not be necessary any more + id = String.valueOf(ID_FOR_PROFILE_CONTACT);// no contact id for 'ME' user } if (columnIndexRawContactId != -1) { rawContactId = cursor.getString(columnIndexRawContactId); } else { - //todo - double check this, it may not be necessary any more - rawContactId = String.valueOf(ID_FOR_PROFILE_CONTACT);//no contact id for 'ME' user + // todo - double check this, it may not be necessary any more + rawContactId = String.valueOf(ID_FOR_PROFILE_CONTACT);// no contact id for 'ME' user } if (!map.containsKey(contactId)) { @@ -369,7 +372,7 @@ private Map loadContactsFrom(Cursor cursor) { } } - switch(mimeType) { + switch (mimeType) { case StructuredName.CONTENT_ITEM_TYPE: contact.givenName = cursor.getString(cursor.getColumnIndex(StructuredName.GIVEN_NAME)); if (cursor.getString(cursor.getColumnIndex(StructuredName.MIDDLE_NAME)) != null) { @@ -394,7 +397,8 @@ private Map loadContactsFrom(Cursor cursor) { int labelIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LABEL); if (labelIndex >= 0) { String typeLabel = cursor.getString(labelIndex); - label = ContactsContract.CommonDataKinds.Phone..getTypeLabel(Resources.getSystem(), phoneType, typeLabel).toString(); + label = ContactsContract.CommonDataKinds.Phone + .getTypeLabel(Resources.getSystem(), phoneType, typeLabel).toString(); } else { label = "other"; } @@ -527,7 +531,8 @@ private Map loadContactsFrom(Cursor cursor) { int eventType = cursor.getInt(cursor.getColumnIndex(Event.TYPE)); if (eventType == Event.TYPE_BIRTHDAY) { try { - String birthday = cursor.getString(cursor.getColumnIndex(Event.START_DATE)).replace("--", ""); + String birthday = cursor.getString(cursor.getColumnIndex(Event.START_DATE)).replace("--", + ""); String[] yearMonthDay = birthday.split("-"); List yearMonthDayList = Arrays.asList(yearMonthDay); @@ -568,9 +573,8 @@ public String getPhotoUriFromContactId(String contactId) { ContactsContract.Data.CONTENT_URI, PHOTO_PROJECTION.toArray(new String[PHOTO_PROJECTION.size()]), ContactsContract.RawContacts.CONTACT_ID + " = ?", - new String[]{contactId}, - null - ); + new String[] { contactId }, + null); try { if (cursor != null && cursorMoveToNext(cursor)) { String rawPhotoURI = cursor.getString(cursor.getColumnIndex(Contactables.PHOTO_URI)); @@ -598,7 +602,7 @@ private static class Contact { private String company = ""; private String jobTitle = ""; private String department = ""; - private String note =""; + private String note = ""; private List urls = new ArrayList<>(); private List instantMessengers = new ArrayList<>(); private boolean hasPhoto = false; @@ -609,7 +613,6 @@ private static class Contact { private List postalAddresses = new ArrayList<>(); private Birthday birthday; - public Contact(String contactId) { this.contactId = contactId; } diff --git a/android/src/main/java/com/rt2zz/reactnativecontacts/ReactNativeContacts.java b/android/src/main/java/com/rt2zz/reactnativecontacts/ReactNativeContacts.java index 6a9f6658..c5e07ec7 100644 --- a/android/src/main/java/com/rt2zz/reactnativecontacts/ReactNativeContacts.java +++ b/android/src/main/java/com/rt2zz/reactnativecontacts/ReactNativeContacts.java @@ -1,38 +1,54 @@ package com.rt2zz.reactnativecontacts; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.facebook.react.ReactPackage; +import com.facebook.react.TurboReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.bridge.JavaScriptModule; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; -public class ReactNativeContacts implements ReactPackage { +public class ReactNativeContacts extends TurboReactPackage { + @Nullable @Override - public List createNativeModules( - ReactApplicationContext reactContext) { - List modules = new ArrayList<>(); - - modules.add(new ContactsManager(reactContext)); - return modules; - } - - public List> createJSModules() { - return Collections.emptyList(); + public NativeModule getModule(@NonNull String name, @NonNull ReactApplicationContext reactContext) { + if (name.equals(ContactsProvider.NAME)) { + return new ContactsManager(reactContext); + } else { + return null; + } } @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Arrays.asList(); + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + final Map moduleInfos = new HashMap<>(); + boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + moduleInfos.put( + ContactsProvider.NAME, + new ReactModuleInfo( + ContactsProvider.NAME, + ContactsProvider.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + )); + return moduleInfos; + }; } - public static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - ContactsManager.onRequestPermissionsResult(requestCode, permissions, grantResults); - } } diff --git a/android/src/newarch/com/rt2zz/reactnativecontacts/ContactsManager.java b/android/src/newarch/com/rt2zz/reactnativecontacts/ContactsManager.java new file mode 100644 index 00000000..20d70136 --- /dev/null +++ b/android/src/newarch/com/rt2zz/reactnativecontacts/ContactsManager.java @@ -0,0 +1,1388 @@ +package com.rt2zz.reactnativecontacts; + +import android.Manifest; +import android.app.Activity; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.RawContacts; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; + +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.rt2zz.reactnativecontacts.ContactsProvider; +import com.rt2zz.reactnativecontacts.NativeContactsSpec; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Hashtable; + +public class ContactsManager extends NativeContactsSpec implements ActivityEventListener { + + private static final String PERMISSION_DENIED = "denied"; + private static final String PERMISSION_AUTHORIZED = "authorized"; + private static final String PERMISSION_READ_CONTACTS = Manifest.permission.READ_CONTACTS; + private static final int PERMISSION_REQUEST_CODE = 888; + + private static final int REQUEST_OPEN_CONTACT_FORM = 52941; + private static final int REQUEST_OPEN_EXISTING_CONTACT = 52942; + + private static Promise updateContactPromise; + private static Promise requestPromise; + + public ContactsManager(ReactApplicationContext reactContext) { + super(reactContext); + reactContext.addActivityEventListener(this); + } + + /* + * Returns all contactable records on phone + * queries CommonDataKinds.Contactables to get phones and emails + */ + @Override + public void getAll(Promise promise) { + getAllContacts(promise); + } + + /** + * Introduced for iOS compatibility. Same as getAll + * + * @param promise promise + */ + @Override + public void getAllWithoutPhotos(Promise promise) { + getAllContacts(promise); + } + + /** + * Retrieves contacts. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. + */ + private void getAllContacts(final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableArray contacts = contactsProvider.getContacts(); + promise.resolve(contacts); + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + @Override + public void getCount(final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + + ContactsProvider contactsProvider = new ContactsProvider(cr); + try { + Integer contacts = contactsProvider.getContactsCount(); + promise.resolve(contacts); + } catch (Exception e) { + promise.reject(e); + } + + return null; + + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Retrieves contacts matching String. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. + * + * @param searchString String to match + */ + @Override + public void getContactsMatchingString(final String searchString, final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableArray contacts = contactsProvider.getContactsMatchingString(searchString); + + promise.resolve(contacts); + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Retrieves contacts matching a phone number. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. + * + * @param phoneNumber phone number to match + */ + @Override + public void getContactsByPhoneNumber(final String phoneNumber, final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableArray contacts = contactsProvider.getContactsByPhoneNumber(phoneNumber); + + promise.resolve(contacts); + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Retrieves contacts matching an email address. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. + * + * @param emailAddress email address to match + */ + @Override + public void getContactsByEmailAddress(final String emailAddress, final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableArray contacts = contactsProvider.getContactsByEmailAddress(emailAddress); + + promise.resolve(contacts); + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Retrieves thumbnailPath for contact, or null if not + * available. + * + * @param contactId contact identifier, recordID + */ + @Override + public void getPhotoForId(final String contactId, final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + ContactsProvider contactsProvider = new ContactsProvider(cr); + String photoUri = contactsProvider.getPhotoUriFromContactId(contactId); + + promise.resolve(photoUri); + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Retrieves contact for contact, or null if not + * available. + * + * @param contactId contact identifier, recordID + */ + @Override + public void getContactById(final String contactId, final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableMap contact = contactsProvider.getContactById(contactId); + + promise.resolve(contact); + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + @Override + public void writePhotoToPath(final String contactId, final String file, final Promise promise) { + AsyncTask myAsyncTask = new AsyncTask() { + @Override + protected Void doInBackground(final Void... params) { + Context context = getReactApplicationContext(); + ContentResolver cr = context.getContentResolver(); + + Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(contactId)); + InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(cr, uri); + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + BitmapFactory.decodeStream(inputStream).compress(Bitmap.CompressFormat.PNG, 100, outputStream); + promise.resolve(true); + } catch (FileNotFoundException e) { + promise.reject(e.toString()); + } finally { + try { + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + }; + myAsyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + private Bitmap getThumbnailBitmap(String thumbnailPath) { + // Thumbnail from absolute path + Bitmap photo = BitmapFactory.decodeFile(thumbnailPath); + + if (photo == null) { + // Try to find the thumbnail from assets + AssetManager assetManager = getReactApplicationContext().getAssets(); + InputStream inputStream = null; + try { + inputStream = assetManager.open(thumbnailPath); + photo = BitmapFactory.decodeStream(inputStream); + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return photo; + } + + /* + * Start open contact form + */ + @Override + public void openContactForm(ReadableMap contact, Promise promise) { + + String givenName = contact.hasKey("givenName") ? contact.getString("givenName") : null; + String middleName = contact.hasKey("middleName") ? contact.getString("middleName") : null; + String displayName = contact.hasKey("displayName") ? contact.getString("displayName") : null; + String familyName = contact.hasKey("familyName") ? contact.getString("familyName") : null; + String prefix = contact.hasKey("prefix") ? contact.getString("prefix") : null; + String suffix = contact.hasKey("suffix") ? contact.getString("suffix") : null; + String company = contact.hasKey("company") ? contact.getString("company") : null; + String jobTitle = contact.hasKey("jobTitle") ? contact.getString("jobTitle") : null; + String department = contact.hasKey("department") ? contact.getString("department") : null; + String note = contact.hasKey("note") ? contact.getString("note") : null; + String thumbnailPath = contact.hasKey("thumbnailPath") ? contact.getString("thumbnailPath") : null; + + ReadableArray phoneNumbers = contact.hasKey("phoneNumbers") ? contact.getArray("phoneNumbers") : null; + int numOfPhones = 0; + String[] phones = null; + String[] phonesLabels = null; + Integer[] phonesLabelsTypes = null; + if (phoneNumbers != null) { + numOfPhones = phoneNumbers.size(); + phones = new String[numOfPhones]; + phonesLabels = new String[numOfPhones]; + phonesLabelsTypes = new Integer[numOfPhones]; + for (int i = 0; i < numOfPhones; i++) { + phones[i] = phoneNumbers.getMap(i).getString("number"); + String label = phoneNumbers.getMap(i).getString("label"); + phonesLabels[i] = label; + phonesLabelsTypes[i] = mapStringToPhoneType(label); + } + } + + ReadableArray urlAddresses = contact.hasKey("urlAddresses") ? contact.getArray("urlAddresses") : null; + int numOfUrls = 0; + String[] urls = null; + if (urlAddresses != null) { + numOfUrls = urlAddresses.size(); + urls = new String[numOfUrls]; + for (int i = 0; i < numOfUrls; i++) { + urls[i] = urlAddresses.getMap(i).getString("url"); + } + } + + ReadableArray emailAddresses = contact.hasKey("emailAddresses") ? contact.getArray("emailAddresses") : null; + int numOfEmails = 0; + String[] emails = null; + Integer[] emailsLabels = null; + if (emailAddresses != null) { + numOfEmails = emailAddresses.size(); + emails = new String[numOfEmails]; + emailsLabels = new Integer[numOfEmails]; + for (int i = 0; i < numOfEmails; i++) { + emails[i] = emailAddresses.getMap(i).getString("email"); + String label = emailAddresses.getMap(i).getString("label"); + emailsLabels[i] = mapStringToEmailType(label); + } + } + + ReadableArray postalAddresses = contact.hasKey("postalAddresses") ? contact.getArray("postalAddresses") : null; + int numOfPostalAddresses = 0; + String[] postalAddressesStreet = null; + String[] postalAddressesCity = null; + String[] postalAddressesState = null; + String[] postalAddressesRegion = null; + String[] postalAddressesPostCode = null; + String[] postalAddressesCountry = null; + String[] postalAddressesFormattedAddress = null; + String[] postalAddressesLabel = null; + Integer[] postalAddressesType = null; + + if (postalAddresses != null) { + numOfPostalAddresses = postalAddresses.size(); + postalAddressesStreet = new String[numOfPostalAddresses]; + postalAddressesCity = new String[numOfPostalAddresses]; + postalAddressesState = new String[numOfPostalAddresses]; + postalAddressesRegion = new String[numOfPostalAddresses]; + postalAddressesPostCode = new String[numOfPostalAddresses]; + postalAddressesCountry = new String[numOfPostalAddresses]; + postalAddressesFormattedAddress = new String[numOfPostalAddresses]; + postalAddressesLabel = new String[numOfPostalAddresses]; + postalAddressesType = new Integer[numOfPostalAddresses]; + for (int i = 0; i < numOfPostalAddresses; i++) { + postalAddressesStreet[i] = postalAddresses.getMap(i).getString("street"); + postalAddressesCity[i] = postalAddresses.getMap(i).getString("city"); + postalAddressesState[i] = postalAddresses.getMap(i).getString("state"); + postalAddressesRegion[i] = postalAddresses.getMap(i).getString("region"); + postalAddressesPostCode[i] = postalAddresses.getMap(i).getString("postCode"); + postalAddressesCountry[i] = postalAddresses.getMap(i).getString("country"); + postalAddressesFormattedAddress[i] = postalAddresses.getMap(i).getString("formattedAddress"); + postalAddressesLabel[i] = postalAddresses.getMap(i).getString("label"); + postalAddressesType[i] = mapStringToPostalAddressType(postalAddresses.getMap(i).getString("label")); + } + } + + ReadableArray imAddresses = contact.hasKey("imAddresses") ? contact.getArray("imAddresses") : null; + int numOfIMAddresses = 0; + String[] imAccounts = null; + String[] imProtocols = null; + if (imAddresses != null) { + numOfIMAddresses = imAddresses.size(); + imAccounts = new String[numOfIMAddresses]; + imProtocols = new String[numOfIMAddresses]; + for (int i = 0; i < numOfIMAddresses; i++) { + imAccounts[i] = imAddresses.getMap(i).getString("username"); + imProtocols[i] = imAddresses.getMap(i).getString("service"); + } + } + + ArrayList contactData = new ArrayList<>(); + + ContentValues name = new ContentValues(); + name.put(ContactsContract.Contacts.Data.MIMETYPE, CommonDataKinds.Identity.CONTENT_ITEM_TYPE); + name.put(StructuredName.GIVEN_NAME, givenName); + name.put(StructuredName.FAMILY_NAME, familyName); + name.put(StructuredName.MIDDLE_NAME, middleName); + name.put(StructuredName.PREFIX, prefix); + name.put(StructuredName.SUFFIX, suffix); + contactData.add(name); + + ContentValues organization = new ContentValues(); + organization.put(ContactsContract.Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); + organization.put(Organization.COMPANY, company); + organization.put(Organization.TITLE, jobTitle); + organization.put(Organization.DEPARTMENT, department); + contactData.add(organization); + + for (int i = 0; i < numOfUrls; i++) { + ContentValues url = new ContentValues(); + url.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Website.CONTENT_ITEM_TYPE); + url.put(CommonDataKinds.Website.URL, urls[i]); + contactData.add(url); + } + + for (int i = 0; i < numOfEmails; i++) { + ContentValues email = new ContentValues(); + email.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE); + email.put(CommonDataKinds.Email.TYPE, emailsLabels[i]); + email.put(CommonDataKinds.Email.ADDRESS, emails[i]); + contactData.add(email); + } + + for (int i = 0; i < numOfPhones; i++) { + ContentValues phone = new ContentValues(); + phone.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE); + phone.put(CommonDataKinds.Phone.TYPE, phonesLabelsTypes[i]); + phone.put(CommonDataKinds.Phone.LABEL, phonesLabels[i]); + phone.put(CommonDataKinds.Phone.NUMBER, phones[i]); + + contactData.add(phone); + } + + for (int i = 0; i < numOfPostalAddresses; i++) { + ContentValues structuredPostal = new ContentValues(); + structuredPostal.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE); + structuredPostal.put(CommonDataKinds.StructuredPostal.STREET, postalAddressesStreet[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.CITY, postalAddressesCity[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.REGION, postalAddressesRegion[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.COUNTRY, postalAddressesCountry[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.POSTCODE, postalAddressesPostCode[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, + postalAddressesFormattedAddress[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.LABEL, postalAddressesLabel[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.TYPE, postalAddressesType[i]); + // No state column in StructuredPostal + // structuredPostal.put(CommonDataKinds.StructuredPostal.???, + // postalAddressesState[i]); + contactData.add(structuredPostal); + } + + for (int i = 0; i < numOfIMAddresses; i++) { + ContentValues imAddress = new ContentValues(); + imAddress.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Im.CONTENT_ITEM_TYPE); + imAddress.put(CommonDataKinds.Im.DATA, imAccounts[i]); + imAddress.put(CommonDataKinds.Im.TYPE, CommonDataKinds.Im.TYPE_HOME); + imAddress.put(CommonDataKinds.Im.PROTOCOL, CommonDataKinds.Im.PROTOCOL_CUSTOM); + imAddress.put(CommonDataKinds.Im.CUSTOM_PROTOCOL, imProtocols[i]); + contactData.add(imAddress); + } + + if (note != null) { + ContentValues structuredNote = new ContentValues(); + structuredNote.put(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); + structuredNote.put(Note.NOTE, note); + contactData.add(structuredNote); + } + + if (thumbnailPath != null && !thumbnailPath.isEmpty()) { + Bitmap photo = getThumbnailBitmap(thumbnailPath); + + if (photo != null) { + ContentValues thumbnail = new ContentValues(); + thumbnail.put(ContactsContract.Data.RAW_CONTACT_ID, 0); + thumbnail.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); + thumbnail.put(CommonDataKinds.Photo.PHOTO, toByteArray(photo)); + thumbnail.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE); + contactData.add(thumbnail); + } + } + + Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI); + intent.putExtra(ContactsContract.Intents.Insert.NAME, displayName); + intent.putExtra("finishActivityOnSaveCompleted", true); + intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData); + + updateContactPromise = promise; + getReactApplicationContext().startActivityForResult(intent, REQUEST_OPEN_CONTACT_FORM, Bundle.EMPTY); + } + + /* + * Open contact in native app + */ + @Override + public void openExistingContact(ReadableMap contact, Promise promise) { + + String recordID = contact.hasKey("recordID") ? contact.getString("recordID") : null; + + try { + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, recordID); + Intent intent = new Intent(Intent.ACTION_EDIT); + intent.setDataAndType(uri, ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + + updateContactPromise = promise; + getReactApplicationContext().startActivityForResult(intent, REQUEST_OPEN_EXISTING_CONTACT, Bundle.EMPTY); + + } catch (Exception e) { + promise.reject(e.toString()); + } + } + + /* + * View contact in native app + */ + @Override + public void viewExistingContact(ReadableMap contact, Promise promise) { + + String recordID = contact.hasKey("recordID") ? contact.getString("recordID") : null; + + try { + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, recordID); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(uri, ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + + updateContactPromise = promise; + getReactApplicationContext().startActivityForResult(intent, REQUEST_OPEN_EXISTING_CONTACT, Bundle.EMPTY); + + } catch (Exception e) { + promise.reject(e.toString()); + } + } + + /* + * Edit contact in native app + */ + @Override + public void editExistingContact(ReadableMap contact, Promise promise) { + + String recordID = contact.hasKey("recordID") ? contact.getString("recordID") : null; + + try { + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, recordID); + + ReadableArray phoneNumbers = contact.hasKey("phoneNumbers") ? contact.getArray("phoneNumbers") : null; + int numOfPhones = 0; + String[] phones = null; + Integer[] phonesLabels = null; + if (phoneNumbers != null) { + numOfPhones = phoneNumbers.size(); + phones = new String[numOfPhones]; + phonesLabels = new Integer[numOfPhones]; + for (int i = 0; i < numOfPhones; i++) { + phones[i] = phoneNumbers.getMap(i).getString("number"); + String label = phoneNumbers.getMap(i).getString("label"); + phonesLabels[i] = mapStringToPhoneType(label); + } + } + + ArrayList contactData = new ArrayList<>(); + for (int i = 0; i < numOfPhones; i++) { + ContentValues phone = new ContentValues(); + phone.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE); + phone.put(CommonDataKinds.Phone.TYPE, phonesLabels[i]); + phone.put(CommonDataKinds.Phone.NUMBER, phones[i]); + contactData.add(phone); + } + + Intent intent = new Intent(Intent.ACTION_EDIT); + intent.setDataAndType(uri, ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData); + + updateContactPromise = promise; + getReactApplicationContext().startActivityForResult(intent, REQUEST_OPEN_EXISTING_CONTACT, Bundle.EMPTY); + + } catch (Exception e) { + promise.reject(e.toString()); + } + } + + /* + * Adds contact to phone's addressbook + */ + @Override + public void addContact(ReadableMap contact, Promise promise) { + if (contact == null) { + promise.reject("New contact cannot be null."); + return; + } + String givenName = contact.hasKey("givenName") ? contact.getString("givenName") : null; + String middleName = contact.hasKey("middleName") ? contact.getString("middleName") : null; + String familyName = contact.hasKey("familyName") ? contact.getString("familyName") : null; + String prefix = contact.hasKey("prefix") ? contact.getString("prefix") : null; + String suffix = contact.hasKey("suffix") ? contact.getString("suffix") : null; + String company = contact.hasKey("company") ? contact.getString("company") : null; + String jobTitle = contact.hasKey("jobTitle") ? contact.getString("jobTitle") : null; + String department = contact.hasKey("department") ? contact.getString("department") : null; + String note = contact.hasKey("note") ? contact.getString("note") : null; + String thumbnailPath = contact.hasKey("thumbnailPath") ? contact.getString("thumbnailPath") : null; + + ReadableArray phoneNumbers = contact.hasKey("phoneNumbers") ? contact.getArray("phoneNumbers") : null; + int numOfPhones = 0; + String[] phones = null; + Integer[] phonesTypes = null; + String[] phonesLabels = null; + if (phoneNumbers != null) { + numOfPhones = phoneNumbers.size(); + phones = new String[numOfPhones]; + phonesTypes = new Integer[numOfPhones]; + phonesLabels = new String[numOfPhones]; + for (int i = 0; i < numOfPhones; i++) { + phones[i] = phoneNumbers.getMap(i).getString("number"); + String label = phoneNumbers.getMap(i).getString("label"); + phonesTypes[i] = mapStringToPhoneType(label); + phonesLabels[i] = label; + } + } + + ReadableArray urlAddresses = contact.hasKey("urlAddresses") ? contact.getArray("urlAddresses") : null; + int numOfUrls = 0; + String[] urls = null; + if (urlAddresses != null) { + numOfUrls = urlAddresses.size(); + urls = new String[numOfUrls]; + for (int i = 0; i < numOfUrls; i++) { + urls[i] = urlAddresses.getMap(i).getString("url"); + } + } + + ReadableArray emailAddresses = contact.hasKey("emailAddresses") ? contact.getArray("emailAddresses") : null; + int numOfEmails = 0; + String[] emails = null; + Integer[] emailsTypes = null; + String[] emailsLabels = null; + if (emailAddresses != null) { + numOfEmails = emailAddresses.size(); + emails = new String[numOfEmails]; + emailsTypes = new Integer[numOfEmails]; + emailsLabels = new String[numOfEmails]; + for (int i = 0; i < numOfEmails; i++) { + emails[i] = emailAddresses.getMap(i).getString("email"); + String label = emailAddresses.getMap(i).getString("label"); + emailsTypes[i] = mapStringToEmailType(label); + emailsLabels[i] = label; + } + } + + ReadableArray imAddresses = contact.hasKey("imAddresses") ? contact.getArray("imAddresses") : null; + int numOfIMAddresses = 0; + String[] imAccounts = null; + String[] imProtocols = null; + if (imAddresses != null) { + numOfIMAddresses = imAddresses.size(); + imAccounts = new String[numOfIMAddresses]; + imProtocols = new String[numOfIMAddresses]; + for (int i = 0; i < numOfIMAddresses; i++) { + imAccounts[i] = imAddresses.getMap(i).getString("username"); + imProtocols[i] = imAddresses.getMap(i).getString("service"); + } + } + + ArrayList ops = new ArrayList(); + + ContentProviderOperation.Builder op = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) + .withValue(RawContacts.ACCOUNT_TYPE, null) + .withValue(RawContacts.ACCOUNT_NAME, null); + ops.add(op.build()); + + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + // .withValue(StructuredName.DISPLAY_NAME, name) + .withValue(StructuredName.GIVEN_NAME, givenName) + .withValue(StructuredName.MIDDLE_NAME, middleName) + .withValue(StructuredName.FAMILY_NAME, familyName) + .withValue(StructuredName.PREFIX, prefix) + .withValue(StructuredName.SUFFIX, suffix); + ops.add(op.build()); + + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) + .withValue(Note.NOTE, note); + ops.add(op.build()); + + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE) + .withValue(Organization.COMPANY, company) + .withValue(Organization.TITLE, jobTitle) + .withValue(Organization.DEPARTMENT, department); + ops.add(op.build()); + + // TODO not sure where to allow yields + op.withYieldAllowed(true); + + for (int i = 0; i < numOfPhones; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Phone.NUMBER, phones[i]) + .withValue(CommonDataKinds.Phone.TYPE, phonesTypes[i]) + .withValue(CommonDataKinds.Phone.LABEL, phonesLabels[i]); + ops.add(op.build()); + } + + for (int i = 0; i < numOfUrls; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Website.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Website.URL, urls[i]); + ops.add(op.build()); + } + + for (int i = 0; i < numOfEmails; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Email.ADDRESS, emails[i]) + .withValue(CommonDataKinds.Email.TYPE, emailsTypes[i]) + .withValue(CommonDataKinds.Email.LABEL, emailsLabels[i]); + ops.add(op.build()); + } + + if (thumbnailPath != null && !thumbnailPath.isEmpty()) { + Bitmap photo = getThumbnailBitmap(thumbnailPath); + + if (photo != null) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, + CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Photo.PHOTO, toByteArray(photo)) + .build()); + } + } + + ReadableArray postalAddresses = contact.hasKey("postalAddresses") ? contact.getArray("postalAddresses") : null; + if (postalAddresses != null) { + for (int i = 0; i < postalAddresses.size(); i++) { + ReadableMap address = postalAddresses.getMap(i); + + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.StructuredPostal.TYPE, + mapStringToPostalAddressType(address.getString("label"))) + .withValue(CommonDataKinds.StructuredPostal.LABEL, address.getString("label")) + .withValue(CommonDataKinds.StructuredPostal.STREET, address.getString("street")) + .withValue(CommonDataKinds.StructuredPostal.CITY, address.getString("city")) + .withValue(CommonDataKinds.StructuredPostal.REGION, address.getString("state")) + .withValue(CommonDataKinds.StructuredPostal.POSTCODE, address.getString("postCode")) + .withValue(CommonDataKinds.StructuredPostal.COUNTRY, address.getString("country")); + + ops.add(op.build()); + } + } + + for (int i = 0; i < numOfIMAddresses; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Im.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Im.DATA, imAccounts[i]) + .withValue(CommonDataKinds.Im.TYPE, CommonDataKinds.Im.TYPE_HOME) + .withValue(CommonDataKinds.Im.PROTOCOL, CommonDataKinds.Im.PROTOCOL_CUSTOM) + .withValue(CommonDataKinds.Im.CUSTOM_PROTOCOL, imProtocols[i]); + ops.add(op.build()); + } + + Context ctx = getReactApplicationContext(); + try { + ContentResolver cr = ctx.getContentResolver(); + ContentProviderResult[] result = cr.applyBatch(ContactsContract.AUTHORITY, ops); + + if (result != null && result.length > 0) { + + String rawId = String.valueOf(ContentUris.parseId(result[0].uri)); + + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableMap newlyAddedContact = contactsProvider.getContactByRawId(rawId); + + promise.resolve(newlyAddedContact); // success + } + } catch (Exception e) { + promise.reject(e.toString()); + } + } + + public byte[] toByteArray(Bitmap bitmap) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream); + return stream.toByteArray(); + } + + /* + * Update contact to phone's addressbook + */ + @Override + public void updateContact(ReadableMap contact, Promise promise) { + + String recordID = contact.hasKey("recordID") ? contact.getString("recordID") : null; + String rawContactId = contact.hasKey("rawContactId") ? contact.getString("rawContactId") : null; + + if (rawContactId == null || recordID == null) { + promise.reject("Invalid recordId or rawContactId"); + return; + } + + String givenName = contact.hasKey("givenName") ? contact.getString("givenName") : null; + String middleName = contact.hasKey("middleName") ? contact.getString("middleName") : null; + String familyName = contact.hasKey("familyName") ? contact.getString("familyName") : null; + String prefix = contact.hasKey("prefix") ? contact.getString("prefix") : null; + String suffix = contact.hasKey("suffix") ? contact.getString("suffix") : null; + String company = contact.hasKey("company") ? contact.getString("company") : null; + String jobTitle = contact.hasKey("jobTitle") ? contact.getString("jobTitle") : null; + String department = contact.hasKey("department") ? contact.getString("department") : null; + String note = contact.hasKey("note") ? contact.getString("note") : null; + String thumbnailPath = contact.hasKey("thumbnailPath") ? contact.getString("thumbnailPath") : null; + + ReadableArray phoneNumbers = contact.hasKey("phoneNumbers") ? contact.getArray("phoneNumbers") : null; + int numOfPhones = 0; + String[] phones = null; + Integer[] phonesTypes = null; + String[] phonesLabels = null; + String[] phoneIds = null; + if (phoneNumbers != null) { + numOfPhones = phoneNumbers.size(); + phones = new String[numOfPhones]; + phonesTypes = new Integer[numOfPhones]; + phonesLabels = new String[numOfPhones]; + phoneIds = new String[numOfPhones]; + for (int i = 0; i < numOfPhones; i++) { + ReadableMap phoneMap = phoneNumbers.getMap(i); + String phoneNumber = phoneMap.getString("number"); + String phoneLabel = phoneMap.getString("label"); + String phoneId = phoneMap.hasKey("id") ? phoneMap.getString("id") : null; + phones[i] = phoneNumber; + phonesTypes[i] = mapStringToPhoneType(phoneLabel); + phonesLabels[i] = phoneLabel; + phoneIds[i] = phoneId; + } + } + + ReadableArray urlAddresses = contact.hasKey("urlAddresses") ? contact.getArray("urlAddresses") : null; + int numOfUrls = 0; + String[] urls = null; + String[] urlIds = null; + + if (urlAddresses != null) { + numOfUrls = urlAddresses.size(); + urls = new String[numOfUrls]; + urlIds = new String[numOfUrls]; + for (int i = 0; i < numOfUrls; i++) { + ReadableMap urlMap = urlAddresses.getMap(i); + urls[i] = urlMap.getString("url"); + urlIds[i] = urlMap.hasKey("id") ? urlMap.getString("id") : null; + } + } + + ReadableArray emailAddresses = contact.hasKey("emailAddresses") ? contact.getArray("emailAddresses") : null; + int numOfEmails = 0; + String[] emails = null; + Integer[] emailsTypes = null; + String[] emailsLabels = null; + String[] emailIds = null; + + if (emailAddresses != null) { + numOfEmails = emailAddresses.size(); + emails = new String[numOfEmails]; + emailIds = new String[numOfEmails]; + emailsTypes = new Integer[numOfEmails]; + emailsLabels = new String[numOfEmails]; + for (int i = 0; i < numOfEmails; i++) { + ReadableMap emailMap = emailAddresses.getMap(i); + emails[i] = emailMap.getString("email"); + String label = emailMap.getString("label"); + emailsTypes[i] = mapStringToEmailType(label); + emailsLabels[i] = label; + emailIds[i] = emailMap.hasKey("id") ? emailMap.getString("id") : null; + } + } + + ReadableArray postalAddresses = contact.hasKey("postalAddresses") ? contact.getArray("postalAddresses") : null; + int numOfPostalAddresses = 0; + String[] postalAddressesStreet = null; + String[] postalAddressesCity = null; + String[] postalAddressesState = null; + String[] postalAddressesRegion = null; + String[] postalAddressesPostCode = null; + String[] postalAddressesCountry = null; + Integer[] postalAddressesType = null; + String[] postalAddressesLabel = null; + if (postalAddresses != null) { + numOfPostalAddresses = postalAddresses.size(); + postalAddressesStreet = new String[numOfPostalAddresses]; + postalAddressesCity = new String[numOfPostalAddresses]; + postalAddressesState = new String[numOfPostalAddresses]; + postalAddressesRegion = new String[numOfPostalAddresses]; + postalAddressesPostCode = new String[numOfPostalAddresses]; + postalAddressesCountry = new String[numOfPostalAddresses]; + postalAddressesType = new Integer[numOfPostalAddresses]; + postalAddressesLabel = new String[numOfPostalAddresses]; + for (int i = 0; i < numOfPostalAddresses; i++) { + String postalLabel = getValueFromKey(postalAddresses.getMap(i), "label"); + postalAddressesStreet[i] = getValueFromKey(postalAddresses.getMap(i), "street"); + postalAddressesCity[i] = getValueFromKey(postalAddresses.getMap(i), "city"); + postalAddressesState[i] = getValueFromKey(postalAddresses.getMap(i), "state"); + postalAddressesRegion[i] = getValueFromKey(postalAddresses.getMap(i), "region"); + postalAddressesPostCode[i] = getValueFromKey(postalAddresses.getMap(i), "postCode"); + postalAddressesCountry[i] = getValueFromKey(postalAddresses.getMap(i), "country"); + postalAddressesType[i] = mapStringToPostalAddressType(postalLabel); + postalAddressesLabel[i] = postalLabel; + } + } + + ReadableArray imAddresses = contact.hasKey("imAddresses") ? contact.getArray("imAddresses") : null; + int numOfIMAddresses = 0; + String[] imAccounts = null; + String[] imProtocols = null; + String[] imAddressIds = null; + + if (imAddresses != null) { + numOfIMAddresses = imAddresses.size(); + imAccounts = new String[numOfIMAddresses]; + imProtocols = new String[numOfIMAddresses]; + imAddressIds = new String[numOfIMAddresses]; + for (int i = 0; i < numOfIMAddresses; i++) { + ReadableMap imAddressMap = imAddresses.getMap(i); + imAccounts[i] = imAddressMap.getString("username"); + imProtocols[i] = imAddressMap.getString("service"); + imAddressIds[i] = imAddressMap.hasKey("id") ? imAddressMap.getString("id") : null; + } + } + + ArrayList ops = new ArrayList(); + + ContentProviderOperation.Builder op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection(ContactsContract.Data.CONTACT_ID + "=?", new String[] { String.valueOf(recordID) }) + .withValue(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + .withValue(StructuredName.GIVEN_NAME, givenName) + .withValue(StructuredName.MIDDLE_NAME, middleName) + .withValue(StructuredName.FAMILY_NAME, familyName) + .withValue(StructuredName.PREFIX, prefix) + .withValue(StructuredName.SUFFIX, suffix); + ops.add(op.build()); + + op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + " = ?", + new String[] { String.valueOf(recordID), Organization.CONTENT_ITEM_TYPE }) + .withValue(Organization.COMPANY, company) + .withValue(Organization.TITLE, jobTitle) + .withValue(Organization.DEPARTMENT, department); + ops.add(op.build()); + + op.withYieldAllowed(true); + + if (phoneNumbers != null) { + // remove existing phoneNumbers first + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.Phone.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); + ops.add(op.build()); + + // add passed phonenumbers + for (int i = 0; i < numOfPhones; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Phone.NUMBER, phones[i]) + .withValue(CommonDataKinds.Phone.TYPE, phonesTypes[i]) + .withValue(CommonDataKinds.Phone.LABEL, phonesLabels[i]); + ops.add(op.build()); + } + } + + for (int i = 0; i < numOfUrls; i++) { + if (urlIds[i] == null) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Website.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Website.URL, urls[i]); + } else { + op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection(ContactsContract.Data._ID + "=?", new String[] { String.valueOf(urlIds[i]) }) + .withValue(CommonDataKinds.Website.URL, urls[i]); + } + ops.add(op.build()); + } + + if (emailAddresses != null) { + // remove existing emails first + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.Email.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); + ops.add(op.build()); + + // add passed email addresses + for (int i = 0; i < numOfEmails; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Email.ADDRESS, emails[i]) + .withValue(CommonDataKinds.Email.TYPE, emailsTypes[i]) + .withValue(CommonDataKinds.Email.LABEL, emailsLabels[i]); + ops.add(op.build()); + } + } + + // remove existing note first + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(Note.CONTENT_ITEM_TYPE), String.valueOf(rawContactId) }); + ops.add(op.build()); + + if (note != null) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) + .withValue(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) + .withValue(Note.NOTE, note); + ops.add(op.build()); + } + + if (thumbnailPath != null && !thumbnailPath.isEmpty()) { + Bitmap photo = getThumbnailBitmap(thumbnailPath); + + if (photo != null) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, + CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Photo.PHOTO, toByteArray(photo)) + .build()); + } + } + + if (postalAddresses != null) { + // remove existing addresses + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); + ops.add(op.build()); + + for (int i = 0; i < numOfPostalAddresses; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.StructuredPostal.TYPE, postalAddressesType[i]) + .withValue(CommonDataKinds.StructuredPostal.LABEL, postalAddressesLabel[i]) + .withValue(CommonDataKinds.StructuredPostal.STREET, postalAddressesStreet[i]) + .withValue(CommonDataKinds.StructuredPostal.CITY, postalAddressesCity[i]) + .withValue(CommonDataKinds.StructuredPostal.REGION, postalAddressesState[i]) + .withValue(CommonDataKinds.StructuredPostal.POSTCODE, postalAddressesPostCode[i]) + .withValue(CommonDataKinds.StructuredPostal.COUNTRY, postalAddressesCountry[i]); + ops.add(op.build()); + } + } + + if (imAddresses != null) { + // remove existing IM addresses + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.Im.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); + ops.add(op.build()); + + // add passed IM addresses + for (int i = 0; i < numOfIMAddresses; i++) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Im.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Im.DATA, imAccounts[i]) + .withValue(CommonDataKinds.Im.TYPE, CommonDataKinds.Im.TYPE_HOME) + .withValue(CommonDataKinds.Im.PROTOCOL, CommonDataKinds.Im.PROTOCOL_CUSTOM) + .withValue(CommonDataKinds.Im.CUSTOM_PROTOCOL, imProtocols[i]); + ops.add(op.build()); + } + } + + Context ctx = getReactApplicationContext(); + try { + ContentResolver cr = ctx.getContentResolver(); + ContentProviderResult[] result = cr.applyBatch(ContactsContract.AUTHORITY, ops); + + if (result != null && result.length > 0) { + + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableMap updatedContact = contactsProvider.getContactById(recordID); + + promise.resolve(updatedContact); // success + } + } catch (Exception e) { + promise.reject(e.toString()); + } + } + + /* + * Update contact to phone's addressbook + */ + @Override + public void deleteContact(ReadableMap contact, Promise promise) { + + String recordID = contact.hasKey("recordID") ? contact.getString("recordID") : null; + + try { + Context ctx = getReactApplicationContext(); + + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, recordID); + ContentResolver cr = ctx.getContentResolver(); + int deleted = cr.delete(uri, null, null); + + if (deleted > 0) + promise.resolve(recordID); // success + else + promise.resolve(null); // something was wrong + + } catch (Exception e) { + promise.reject(e.toString()); + } + } + + /* + * Check permission + */ + @Override + public void checkPermission(Promise promise) { + promise.resolve(isPermissionGranted()); + } + + /* + * Request permission + */ + @Override + public void requestPermission(Promise promise) { + requestReadContactsPermission(promise); + } + + /* + * Enable note usage + */ + @Override + public void iosEnableNotesUsage(boolean enabled) { + // this method is only needed for iOS + } + + private void requestReadContactsPermission(Promise promise) { + Activity currentActivity = getCurrentActivity(); + if (currentActivity == null) { + promise.reject(PERMISSION_DENIED); + return; + } + + if (isPermissionGranted().equals(PERMISSION_AUTHORIZED)) { + promise.resolve(PERMISSION_AUTHORIZED); + return; + } + + requestPromise = promise; + ActivityCompat.requestPermissions(currentActivity, new String[] { PERMISSION_READ_CONTACTS }, + PERMISSION_REQUEST_CODE); + } + + protected static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestPromise == null) { + return; + } + + if (requestCode != PERMISSION_REQUEST_CODE) { + requestPromise.resolve(PERMISSION_DENIED); + return; + } + + Hashtable results = new Hashtable<>(); + for (int i = 0; i < permissions.length; i++) { + results.put(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED); + } + + if (results.containsKey(PERMISSION_READ_CONTACTS) && results.get(PERMISSION_READ_CONTACTS)) { + requestPromise.resolve(PERMISSION_AUTHORIZED); + } else { + requestPromise.resolve(PERMISSION_DENIED); + } + + requestPromise = null; + } + + /* + * Get string value from key + */ + private String getValueFromKey(ReadableMap item, String key) { + return item.hasKey(key) ? item.getString(key) : ""; + } + + /* + * Check if READ_CONTACTS permission is granted + */ + private String isPermissionGranted() { + // return -1 for denied and 1 + int res = getReactApplicationContext().checkSelfPermission(PERMISSION_READ_CONTACTS); + return (res == PackageManager.PERMISSION_GRANTED) ? PERMISSION_AUTHORIZED : PERMISSION_DENIED; + } + + /* + * TODO support all phone types + * http://developer.android.com/reference/android/provider/ContactsContract. + * CommonDataKinds.Phone.html + */ + private int mapStringToPhoneType(String label) { + int phoneType; + switch (label) { + case "home": + phoneType = CommonDataKinds.Phone.TYPE_HOME; + break; + case "work": + phoneType = CommonDataKinds.Phone.TYPE_WORK; + break; + case "mobile": + phoneType = CommonDataKinds.Phone.TYPE_MOBILE; + break; + case "main": + phoneType = CommonDataKinds.Phone.TYPE_MAIN; + break; + case "work fax": + phoneType = CommonDataKinds.Phone.TYPE_FAX_WORK; + break; + case "home fax": + phoneType = CommonDataKinds.Phone.TYPE_FAX_HOME; + break; + case "pager": + phoneType = CommonDataKinds.Phone.TYPE_PAGER; + break; + case "work_pager": + phoneType = CommonDataKinds.Phone.TYPE_WORK_PAGER; + break; + case "work_mobile": + phoneType = CommonDataKinds.Phone.TYPE_WORK_MOBILE; + break; + case "other": + phoneType = CommonDataKinds.Phone.TYPE_OTHER; + break; + case "cell": + phoneType = CommonDataKinds.Phone.TYPE_MOBILE; + break; + default: + phoneType = CommonDataKinds.Phone.TYPE_CUSTOM; + break; + } + return phoneType; + } + + /* + * TODO support TYPE_CUSTOM + * http://developer.android.com/reference/android/provider/ContactsContract. + * CommonDataKinds.Email.html + */ + private int mapStringToEmailType(String label) { + int emailType; + switch (label) { + case "home": + emailType = CommonDataKinds.Email.TYPE_HOME; + break; + case "work": + emailType = CommonDataKinds.Email.TYPE_WORK; + break; + case "mobile": + emailType = CommonDataKinds.Email.TYPE_MOBILE; + break; + case "other": + emailType = CommonDataKinds.Email.TYPE_OTHER; + break; + case "personal": + emailType = CommonDataKinds.Email.TYPE_HOME; + break; + default: + emailType = CommonDataKinds.Email.TYPE_CUSTOM; + break; + } + return emailType; + } + + private int mapStringToPostalAddressType(String label) { + int postalAddressType; + switch (label) { + case "home": + postalAddressType = CommonDataKinds.StructuredPostal.TYPE_HOME; + break; + case "work": + postalAddressType = CommonDataKinds.StructuredPostal.TYPE_WORK; + break; + default: + postalAddressType = CommonDataKinds.StructuredPostal.TYPE_CUSTOM; + break; + } + return postalAddressType; + } + + @Override + public String getName() { + return ContactsProvider.NAME; + } + + /* + * Required for ActivityEventListener + */ + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_OPEN_CONTACT_FORM && requestCode != REQUEST_OPEN_EXISTING_CONTACT) { + return; + } + + if (updateContactPromise == null) { + return; + } + + if (resultCode != Activity.RESULT_OK) { + updateContactPromise.resolve(null); // user probably pressed cancel + updateContactPromise = null; + return; + } + + if (data == null) { + updateContactPromise.reject("Error received activity result with no data!"); + updateContactPromise = null; + return; + } + + try { + Uri contactUri = data.getData(); + + if (contactUri == null) { + updateContactPromise.reject("Error wrong data. No content uri found!"); // something was wrong + updateContactPromise = null; + return; + } + + Context ctx = getReactApplicationContext(); + ContentResolver cr = ctx.getContentResolver(); + ContactsProvider contactsProvider = new ContactsProvider(cr); + WritableMap newlyModifiedContact = contactsProvider.getContactById(contactUri.getLastPathSegment()); + + updateContactPromise.resolve(newlyModifiedContact); // success + } catch (Exception e) { + updateContactPromise.reject(e.getMessage()); + } + updateContactPromise = null; + } + + /* + * Required for ActivityEventListener + */ + @Override + public void onNewIntent(Intent intent) { + } + +} diff --git a/android/src/main/java/com/rt2zz/reactnativecontacts/ContactsManager.java b/android/src/oldarch/com/rt2zz/reactnativecontacts/ContactsManager.java similarity index 91% rename from android/src/main/java/com/rt2zz/reactnativecontacts/ContactsManager.java rename to android/src/oldarch/com/rt2zz/reactnativecontacts/ContactsManager.java index 97994b04..6dc4273a 100644 --- a/android/src/main/java/com/rt2zz/reactnativecontacts/ContactsManager.java +++ b/android/src/oldarch/com/rt2zz/reactnativecontacts/ContactsManager.java @@ -72,7 +72,7 @@ public void getAll(Promise promise) { } /** - * Introduced for iOS compatibility. Same as getAll + * Introduced for iOS compatibility. Same as getAll * * @param promise promise */ @@ -83,12 +83,13 @@ public void getAllWithoutPhotos(Promise promise) { /** * Retrieves contacts. - * Uses raw URI when rawUri is true, makes assets copy otherwise. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. */ private void getAllContacts(final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); @@ -103,9 +104,9 @@ protected Void doInBackground(final Void ... params) { @ReactMethod public void getCount(final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); @@ -126,15 +127,16 @@ protected Void doInBackground(final Void ... params) { /** * Retrieves contacts matching String. - * Uses raw URI when rawUri is true, makes assets copy otherwise. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. * * @param searchString String to match */ @ReactMethod public void getContactsMatchingString(final String searchString, final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); ContactsProvider contactsProvider = new ContactsProvider(cr); @@ -149,15 +151,16 @@ protected Void doInBackground(final Void ... params) { /** * Retrieves contacts matching a phone number. - * Uses raw URI when rawUri is true, makes assets copy otherwise. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. * * @param phoneNumber phone number to match */ @ReactMethod public void getContactsByPhoneNumber(final String phoneNumber, final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); ContactsProvider contactsProvider = new ContactsProvider(cr); @@ -172,15 +175,16 @@ protected Void doInBackground(final Void ... params) { /** * Retrieves contacts matching an email address. - * Uses raw URI when rawUri is true, makes assets copy otherwise. + * Uses raw URI when rawUri is true, makes assets copy + * otherwise. * * @param emailAddress email address to match */ @ReactMethod public void getContactsByEmailAddress(final String emailAddress, final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); ContactsProvider contactsProvider = new ContactsProvider(cr); @@ -194,15 +198,16 @@ protected Void doInBackground(final Void ... params) { } /** - * Retrieves thumbnailPath for contact, or null if not available. + * Retrieves thumbnailPath for contact, or null if not + * available. * * @param contactId contact identifier, recordID */ @ReactMethod public void getPhotoForId(final String contactId, final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); ContactsProvider contactsProvider = new ContactsProvider(cr); @@ -216,15 +221,16 @@ protected Void doInBackground(final Void ... params) { } /** - * Retrieves contact for contact, or null if not available. + * Retrieves contact for contact, or null if not + * available. * * @param contactId contact identifier, recordID */ @ReactMethod public void getContactById(final String contactId, final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); ContactsProvider contactsProvider = new ContactsProvider(cr); @@ -239,9 +245,9 @@ protected Void doInBackground(final Void ... params) { @ReactMethod public void writePhotoToPath(final String contactId, final String file, final Promise promise) { - AsyncTask myAsyncTask = new AsyncTask() { + AsyncTask myAsyncTask = new AsyncTask() { @Override - protected Void doInBackground(final Void ... params) { + protected Void doInBackground(final Void... params) { Context context = getReactApplicationContext(); ContentResolver cr = context.getContentResolver(); @@ -279,7 +285,7 @@ private Bitmap getThumbnailBitmap(String thumbnailPath) { if (photo == null) { // Try to find the thumbnail from assets AssetManager assetManager = getReactApplicationContext().getAssets(); - InputStream inputStream = null; + InputStream inputStream = null; try { inputStream = assetManager.open(thumbnailPath); photo = BitmapFactory.decodeStream(inputStream); @@ -455,16 +461,17 @@ public void openContactForm(ReadableMap contact, Promise promise) { structuredPostal.put(CommonDataKinds.StructuredPostal.REGION, postalAddressesRegion[i]); structuredPostal.put(CommonDataKinds.StructuredPostal.COUNTRY, postalAddressesCountry[i]); structuredPostal.put(CommonDataKinds.StructuredPostal.POSTCODE, postalAddressesPostCode[i]); - structuredPostal.put(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, postalAddressesFormattedAddress[i]); + structuredPostal.put(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, + postalAddressesFormattedAddress[i]); structuredPostal.put(CommonDataKinds.StructuredPostal.LABEL, postalAddressesLabel[i]); structuredPostal.put(CommonDataKinds.StructuredPostal.TYPE, postalAddressesType[i]); - //No state column in StructuredPostal - //structuredPostal.put(CommonDataKinds.StructuredPostal.???, postalAddressesState[i]); + // No state column in StructuredPostal + // structuredPostal.put(CommonDataKinds.StructuredPostal.???, + // postalAddressesState[i]); contactData.add(structuredPostal); } - for (int i = 0; i < numOfIMAddresses; i++) - { + for (int i = 0; i < numOfIMAddresses; i++) { ContentValues imAddress = new ContentValues(); imAddress.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Im.CONTENT_ITEM_TYPE); imAddress.put(CommonDataKinds.Im.DATA, imAccounts[i]); @@ -474,22 +481,22 @@ public void openContactForm(ReadableMap contact, Promise promise) { contactData.add(imAddress); } - if(note != null) { + if (note != null) { ContentValues structuredNote = new ContentValues(); structuredNote.put(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); structuredNote.put(ContactsContract.CommonDataKinds.Note.NOTE, note); contactData.add(structuredNote); } - if(thumbnailPath != null && !thumbnailPath.isEmpty()) { + if (thumbnailPath != null && !thumbnailPath.isEmpty()) { Bitmap photo = getThumbnailBitmap(thumbnailPath); - if(photo != null) { + if (photo != null) { ContentValues thumbnail = new ContentValues(); thumbnail.put(ContactsContract.Data.RAW_CONTACT_ID, 0); thumbnail.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); thumbnail.put(ContactsContract.CommonDataKinds.Photo.PHOTO, toByteArray(photo)); - thumbnail.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE ); + thumbnail.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE); contactData.add(thumbnail); } } @@ -525,7 +532,7 @@ public void openExistingContact(ReadableMap contact, Promise promise) { } } - /* + /* * View contact in native app */ @ReactMethod @@ -546,7 +553,7 @@ public void viewExistingContact(ReadableMap contact, Promise promise) { promise.reject(e.toString()); } } - + /* * Edit contact in native app */ @@ -708,7 +715,7 @@ public void addContact(ReadableMap contact, Promise promise) { .withValue(Organization.DEPARTMENT, department); ops.add(op.build()); - //TODO not sure where to allow yields + // TODO not sure where to allow yields op.withYieldAllowed(true); for (int i = 0; i < numOfPhones; i++) { @@ -739,13 +746,14 @@ public void addContact(ReadableMap contact, Promise promise) { ops.add(op.build()); } - if(thumbnailPath != null && !thumbnailPath.isEmpty()) { + if (thumbnailPath != null && !thumbnailPath.isEmpty()) { Bitmap photo = getThumbnailBitmap(thumbnailPath); - if(photo != null) { + if (photo != null) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, toByteArray(photo)) .build()); } @@ -759,7 +767,8 @@ public void addContact(ReadableMap contact, Promise promise) { op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.StructuredPostal.TYPE, mapStringToPostalAddressType(address.getString("label"))) + .withValue(CommonDataKinds.StructuredPostal.TYPE, + mapStringToPostalAddressType(address.getString("label"))) .withValue(CommonDataKinds.StructuredPostal.LABEL, address.getString("label")) .withValue(CommonDataKinds.StructuredPostal.STREET, address.getString("street")) .withValue(CommonDataKinds.StructuredPostal.CITY, address.getString("city")) @@ -771,8 +780,7 @@ public void addContact(ReadableMap contact, Promise promise) { } } - for (int i = 0; i < numOfIMAddresses; i++) - { + for (int i = 0; i < numOfIMAddresses; i++) { op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Im.CONTENT_ITEM_TYPE) @@ -788,7 +796,7 @@ public void addContact(ReadableMap contact, Promise promise) { ContentResolver cr = ctx.getContentResolver(); ContentProviderResult[] result = cr.applyBatch(ContactsContract.AUTHORITY, ops); - if(result != null && result.length > 0) { + if (result != null && result.length > 0) { String rawId = String.valueOf(ContentUris.parseId(result[0].uri)); @@ -919,9 +927,9 @@ public void updateContact(ReadableMap contact, Promise promise) { for (int i = 0; i < numOfPostalAddresses; i++) { String postalLabel = getValueFromKey(postalAddresses.getMap(i), "label"); postalAddressesStreet[i] = getValueFromKey(postalAddresses.getMap(i), "street"); - postalAddressesCity[i] = getValueFromKey(postalAddresses.getMap(i), "city"); + postalAddressesCity[i] = getValueFromKey(postalAddresses.getMap(i), "city"); postalAddressesState[i] = getValueFromKey(postalAddresses.getMap(i), "state"); - postalAddressesRegion[i] = getValueFromKey(postalAddresses.getMap(i),"region"); + postalAddressesRegion[i] = getValueFromKey(postalAddresses.getMap(i), "region"); postalAddressesPostCode[i] = getValueFromKey(postalAddresses.getMap(i), "postCode"); postalAddressesCountry[i] = getValueFromKey(postalAddresses.getMap(i), "country"); postalAddressesType[i] = mapStringToPostalAddressType(postalLabel); @@ -951,7 +959,7 @@ public void updateContact(ReadableMap contact, Promise promise) { ArrayList ops = new ArrayList(); ContentProviderOperation.Builder op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection(ContactsContract.Data.CONTACT_ID + "=?", new String[]{String.valueOf(recordID)}) + .withSelection(ContactsContract.Data.CONTACT_ID + "=?", new String[] { String.valueOf(recordID) }) .withValue(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) .withValue(StructuredName.GIVEN_NAME, givenName) .withValue(StructuredName.MIDDLE_NAME, middleName) @@ -961,7 +969,8 @@ public void updateContact(ReadableMap contact, Promise promise) { ops.add(op.build()); op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + " = ?", new String[]{String.valueOf(recordID), Organization.CONTENT_ITEM_TYPE}) + .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + " = ?", + new String[] { String.valueOf(recordID), Organization.CONTENT_ITEM_TYPE }) .withValue(Organization.COMPANY, company) .withValue(Organization.TITLE, jobTitle) .withValue(Organization.DEPARTMENT, department); @@ -969,14 +978,13 @@ public void updateContact(ReadableMap contact, Promise promise) { op.withYieldAllowed(true); - if (phoneNumbers != null) { // remove existing phoneNumbers first op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection( - ContactsContract.Data.MIMETYPE + "=? AND "+ ContactsContract.Data.RAW_CONTACT_ID + " = ?", - new String[]{String.valueOf(CommonDataKinds.Phone.CONTENT_ITEM_TYPE), String.valueOf(rawContactId)} - ); + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.Phone.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); ops.add(op.build()); // add passed phonenumbers @@ -999,19 +1007,19 @@ public void updateContact(ReadableMap contact, Promise promise) { .withValue(CommonDataKinds.Website.URL, urls[i]); } else { op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection(ContactsContract.Data._ID + "=?", new String[]{String.valueOf(urlIds[i])}) + .withSelection(ContactsContract.Data._ID + "=?", new String[] { String.valueOf(urlIds[i]) }) .withValue(CommonDataKinds.Website.URL, urls[i]); } ops.add(op.build()); } - if (emailAddresses != null){ + if (emailAddresses != null) { // remove existing emails first op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection( - ContactsContract.Data.MIMETYPE + "=? AND "+ ContactsContract.Data.RAW_CONTACT_ID + " = ?", - new String[]{String.valueOf(CommonDataKinds.Email.CONTENT_ITEM_TYPE), String.valueOf(rawContactId)} - ); + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.Email.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); ops.add(op.build()); // add passed email addresses @@ -1029,12 +1037,11 @@ public void updateContact(ReadableMap contact, Promise promise) { // remove existing note first op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection( - ContactsContract.Data.MIMETYPE + "=? AND "+ ContactsContract.Data.RAW_CONTACT_ID + " = ?", - new String[]{String.valueOf(Note.CONTENT_ITEM_TYPE), String.valueOf(rawContactId)} - ); + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(Note.CONTENT_ITEM_TYPE), String.valueOf(rawContactId) }); ops.add(op.build()); - if(note != null) { + if (note != null) { op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValue(ContactsContract.Data.RAW_CONTACT_ID, String.valueOf(rawContactId)) .withValue(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) @@ -1042,25 +1049,26 @@ public void updateContact(ReadableMap contact, Promise promise) { ops.add(op.build()); } - if(thumbnailPath != null && !thumbnailPath.isEmpty()) { + if (thumbnailPath != null && !thumbnailPath.isEmpty()) { Bitmap photo = getThumbnailBitmap(thumbnailPath); - if(photo != null) { + if (photo != null) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, toByteArray(photo)) .build()); } } - if (postalAddresses != null){ - //remove existing addresses - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + if (postalAddresses != null) { + // remove existing addresses + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection( - ContactsContract.Data.MIMETYPE + "=? AND "+ ContactsContract.Data.RAW_CONTACT_ID + " = ?", - new String[]{String.valueOf(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE), String.valueOf(rawContactId)} - ); + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); ops.add(op.build()); for (int i = 0; i < numOfPostalAddresses; i++) { @@ -1078,13 +1086,13 @@ public void updateContact(ReadableMap contact, Promise promise) { } } - if (imAddresses != null){ + if (imAddresses != null) { // remove existing IM addresses op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection( - ContactsContract.Data.MIMETYPE + "=? AND "+ ContactsContract.Data.RAW_CONTACT_ID + " = ?", - new String[]{String.valueOf(CommonDataKinds.Im.CONTENT_ITEM_TYPE), String.valueOf(rawContactId)} - ); + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.Data.RAW_CONTACT_ID + " = ?", + new String[] { String.valueOf(CommonDataKinds.Im.CONTENT_ITEM_TYPE), + String.valueOf(rawContactId) }); ops.add(op.build()); // add passed IM addresses @@ -1105,7 +1113,7 @@ public void updateContact(ReadableMap contact, Promise promise) { ContentResolver cr = ctx.getContentResolver(); ContentProviderResult[] result = cr.applyBatch(ContactsContract.AUTHORITY, ops); - if(result != null && result.length > 0) { + if (result != null && result.length > 0) { ContactsProvider contactsProvider = new ContactsProvider(cr); WritableMap updatedContact = contactsProvider.getContactById(recordID); @@ -1126,21 +1134,22 @@ public void deleteContact(ReadableMap contact, Promise promise) { String recordID = contact.hasKey("recordID") ? contact.getString("recordID") : null; try { - Context ctx = getReactApplicationContext(); + Context ctx = getReactApplicationContext(); - Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI,recordID); - ContentResolver cr = ctx.getContentResolver(); - int deleted = cr.delete(uri,null,null); + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, recordID); + ContentResolver cr = ctx.getContentResolver(); + int deleted = cr.delete(uri, null, null); - if(deleted > 0) - promise.resolve(recordID); // success - else - promise.resolve(null); // something was wrong + if (deleted > 0) + promise.resolve(recordID); // success + else + promise.resolve(null); // something was wrong } catch (Exception e) { promise.reject(e.toString()); } } + /* * Check permission */ @@ -1178,11 +1187,12 @@ private void requestReadContactsPermission(Promise promise) { } requestPromise = promise; - ActivityCompat.requestPermissions(currentActivity, new String[]{PERMISSION_READ_CONTACTS}, PERMISSION_REQUEST_CODE); + ActivityCompat.requestPermissions(currentActivity, new String[] { PERMISSION_READ_CONTACTS }, + PERMISSION_REQUEST_CODE); } protected static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + @NonNull int[] grantResults) { if (requestPromise == null) { return; } @@ -1224,7 +1234,8 @@ private String isPermissionGranted() { /* * TODO support all phone types - * http://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Phone.html + * http://developer.android.com/reference/android/provider/ContactsContract. + * CommonDataKinds.Phone.html */ private int mapStringToPhoneType(String label) { int phoneType; @@ -1271,7 +1282,8 @@ private int mapStringToPhoneType(String label) { /* * TODO support TYPE_CUSTOM - * http://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Email.html + * http://developer.android.com/reference/android/provider/ContactsContract. + * CommonDataKinds.Email.html */ private int mapStringToEmailType(String label) { int emailType; @@ -1316,7 +1328,7 @@ private int mapStringToPostalAddressType(String label) { @Override public String getName() { - return "Contacts"; + return ContactsProvider.NAME; } /* diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index dc0c6823..00000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,411 +0,0 @@ -PODS: - - boost-for-react-native (1.63.0) - - DoubleConversion (1.1.6) - - FBLazyVector (0.64.0) - - FBReactNativeSpec (0.64.0): - - RCT-Folly (= 2020.01.13.00) - - RCTRequired (= 0.64.0) - - RCTTypeSafety (= 0.64.0) - - React-Core (= 0.64.0) - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - glog (0.3.5) - - RCT-Folly (2020.01.13.00): - - boost-for-react-native - - DoubleConversion - - glog - - RCT-Folly/Default (= 2020.01.13.00) - - RCT-Folly/Default (2020.01.13.00): - - boost-for-react-native - - DoubleConversion - - glog - - RCTRequired (0.64.0) - - RCTTypeSafety (0.64.0): - - FBLazyVector (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - RCTRequired (= 0.64.0) - - React-Core (= 0.64.0) - - React (0.64.0): - - React-Core (= 0.64.0) - - React-Core/DevSupport (= 0.64.0) - - React-Core/RCTWebSocket (= 0.64.0) - - React-RCTActionSheet (= 0.64.0) - - React-RCTAnimation (= 0.64.0) - - React-RCTBlob (= 0.64.0) - - React-RCTImage (= 0.64.0) - - React-RCTLinking (= 0.64.0) - - React-RCTNetwork (= 0.64.0) - - React-RCTSettings (= 0.64.0) - - React-RCTText (= 0.64.0) - - React-RCTVibration (= 0.64.0) - - React-callinvoker (0.64.0) - - React-Core (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default (= 0.64.0) - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/CoreModulesHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/Default (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/DevSupport (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default (= 0.64.0) - - React-Core/RCTWebSocket (= 0.64.0) - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-jsinspector (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTActionSheetHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTAnimationHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTBlobHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTImageHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTLinkingHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTNetworkHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTSettingsHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTTextHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTVibrationHeaders (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-Core/RCTWebSocket (0.64.0): - - glog - - RCT-Folly (= 2020.01.13.00) - - React-Core/Default (= 0.64.0) - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsiexecutor (= 0.64.0) - - React-perflogger (= 0.64.0) - - Yoga - - React-CoreModules (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - RCTTypeSafety (= 0.64.0) - - React-Core/CoreModulesHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - React-RCTImage (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-cxxreact (0.64.0): - - boost-for-react-native (= 1.63.0) - - DoubleConversion - - glog - - RCT-Folly (= 2020.01.13.00) - - React-callinvoker (= 0.64.0) - - React-jsi (= 0.64.0) - - React-jsinspector (= 0.64.0) - - React-perflogger (= 0.64.0) - - React-runtimeexecutor (= 0.64.0) - - React-jsi (0.64.0): - - boost-for-react-native (= 1.63.0) - - DoubleConversion - - glog - - RCT-Folly (= 2020.01.13.00) - - React-jsi/Default (= 0.64.0) - - React-jsi/Default (0.64.0): - - boost-for-react-native (= 1.63.0) - - DoubleConversion - - glog - - RCT-Folly (= 2020.01.13.00) - - React-jsiexecutor (0.64.0): - - DoubleConversion - - glog - - RCT-Folly (= 2020.01.13.00) - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-perflogger (= 0.64.0) - - React-jsinspector (0.64.0) - - react-native-contacts (6.0.5): - - React-Core - - React-perflogger (0.64.0) - - React-RCTActionSheet (0.64.0): - - React-Core/RCTActionSheetHeaders (= 0.64.0) - - React-RCTAnimation (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - RCTTypeSafety (= 0.64.0) - - React-Core/RCTAnimationHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-RCTBlob (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - React-Core/RCTBlobHeaders (= 0.64.0) - - React-Core/RCTWebSocket (= 0.64.0) - - React-jsi (= 0.64.0) - - React-RCTNetwork (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-RCTImage (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - RCTTypeSafety (= 0.64.0) - - React-Core/RCTImageHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - React-RCTNetwork (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-RCTLinking (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - React-Core/RCTLinkingHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-RCTNetwork (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - RCTTypeSafety (= 0.64.0) - - React-Core/RCTNetworkHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-RCTSettings (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - RCTTypeSafety (= 0.64.0) - - React-Core/RCTSettingsHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-RCTText (0.64.0): - - React-Core/RCTTextHeaders (= 0.64.0) - - React-RCTVibration (0.64.0): - - FBReactNativeSpec (= 0.64.0) - - RCT-Folly (= 2020.01.13.00) - - React-Core/RCTVibrationHeaders (= 0.64.0) - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (= 0.64.0) - - React-runtimeexecutor (0.64.0): - - React-jsi (= 0.64.0) - - ReactCommon/turbomodule/core (0.64.0): - - DoubleConversion - - glog - - RCT-Folly (= 2020.01.13.00) - - React-callinvoker (= 0.64.0) - - React-Core (= 0.64.0) - - React-cxxreact (= 0.64.0) - - React-jsi (= 0.64.0) - - React-perflogger (= 0.64.0) - - RNGestureHandler (1.10.3): - - React-Core - - Yoga (1.14.0) - -DEPENDENCIES: - - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../node_modules/react-native/`) - - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - - React-Core (from `../node_modules/react-native/`) - - React-Core/DevSupport (from `../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - - react-native-contacts (from `../..`) - - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) - - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) - -SPEC REPOS: - trunk: - - boost-for-react-native - -EXTERNAL SOURCES: - DoubleConversion: - :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" - FBLazyVector: - :path: "../node_modules/react-native/Libraries/FBLazyVector" - FBReactNativeSpec: - :path: "../node_modules/react-native/React/FBReactNativeSpec" - glog: - :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" - RCT-Folly: - :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" - RCTRequired: - :path: "../node_modules/react-native/Libraries/RCTRequired" - RCTTypeSafety: - :path: "../node_modules/react-native/Libraries/TypeSafety" - React: - :path: "../node_modules/react-native/" - React-callinvoker: - :path: "../node_modules/react-native/ReactCommon/callinvoker" - React-Core: - :path: "../node_modules/react-native/" - React-CoreModules: - :path: "../node_modules/react-native/React/CoreModules" - React-cxxreact: - :path: "../node_modules/react-native/ReactCommon/cxxreact" - React-jsi: - :path: "../node_modules/react-native/ReactCommon/jsi" - React-jsiexecutor: - :path: "../node_modules/react-native/ReactCommon/jsiexecutor" - React-jsinspector: - :path: "../node_modules/react-native/ReactCommon/jsinspector" - react-native-contacts: - :path: "../.." - React-perflogger: - :path: "../node_modules/react-native/ReactCommon/reactperflogger" - React-RCTActionSheet: - :path: "../node_modules/react-native/Libraries/ActionSheetIOS" - React-RCTAnimation: - :path: "../node_modules/react-native/Libraries/NativeAnimation" - React-RCTBlob: - :path: "../node_modules/react-native/Libraries/Blob" - React-RCTImage: - :path: "../node_modules/react-native/Libraries/Image" - React-RCTLinking: - :path: "../node_modules/react-native/Libraries/LinkingIOS" - React-RCTNetwork: - :path: "../node_modules/react-native/Libraries/Network" - React-RCTSettings: - :path: "../node_modules/react-native/Libraries/Settings" - React-RCTText: - :path: "../node_modules/react-native/Libraries/Text" - React-RCTVibration: - :path: "../node_modules/react-native/Libraries/Vibration" - React-runtimeexecutor: - :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" - ReactCommon: - :path: "../node_modules/react-native/ReactCommon" - RNGestureHandler: - :path: "../node_modules/react-native-gesture-handler" - Yoga: - :path: "../node_modules/react-native/ReactCommon/yoga" - -SPEC CHECKSUMS: - boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c - DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de - FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5 - FBReactNativeSpec: 00448a501f6ae93b3ea732755754878aaf960f8e - glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62 - RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c - RCTRequired: 2f8cb5b7533219bf4218a045f92768129cf7050a - RCTTypeSafety: 512728b73549e72ad7330b92f3d42936f2a4de5b - React: 98eac01574128a790f0bbbafe2d1a8607291ac24 - React-callinvoker: def3f7fae16192df68d9b69fd4bbb59092ee36bc - React-Core: 70a52aa5dbe9b83befae82038451a7df9fd54c5a - React-CoreModules: 052edef46117862e2570eb3a0f06d81c61d2c4b8 - React-cxxreact: c1dc71b30653cfb4770efdafcbdc0ad6d388baab - React-jsi: 74341196d9547cbcbcfa4b3bbbf03af56431d5a1 - React-jsiexecutor: 06a9c77b56902ae7ffcdd7a4905f664adc5d237b - React-jsinspector: 0ae35a37b20d5e031eb020a69cc5afdbd6406301 - react-native-contacts: 931baebf460125c5a7bbce1c4521a96c69795123 - React-perflogger: 9c547d8f06b9bf00cb447f2b75e8d7f19b7e02af - React-RCTActionSheet: 3080b6e12e0e1a5b313c8c0050699b5c794a1b11 - React-RCTAnimation: 3f96f21a497ae7dabf4d2f150ee43f906aaf516f - React-RCTBlob: 283b8e5025e7f954176bc48164f846909002f3ed - React-RCTImage: 5088a484faac78f2d877e1b79125d3bb1ea94a16 - React-RCTLinking: 5e8fbb3e9a8bc2e4e3eb15b1eb8bda5fcac27b8c - React-RCTNetwork: 38ec277217b1e841d5e6a1fa78da65b9212ccb28 - React-RCTSettings: 242d6e692108c3de4f3bb74b7586a8799e9ab070 - React-RCTText: 8746736ac8eb5a4a74719aa695b7a236a93a83d2 - React-RCTVibration: 0fd6b21751a33cb72fce1a4a33ab9678416d307a - React-runtimeexecutor: cad74a1eaa53ee6e7a3620231939d8fe2c6afcf0 - ReactCommon: cfe2b7fd20e0dbd2d1185cd7d8f99633fbc5ff05 - RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 - Yoga: 8c8436d4171c87504c648ae23b1d81242bdf3bbf - -PODFILE CHECKSUM: 36ea5c349217893f661c1143bf6399f9d00f346e - -COCOAPODS: 1.10.1 diff --git a/index.js b/index.js deleted file mode 100644 index e6ec0496..00000000 --- a/index.js +++ /dev/null @@ -1,2 +0,0 @@ -var ReactNative = require('react-native') -module.exports = ReactNative.NativeModules.Contacts diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..52f58766 --- /dev/null +++ b/index.ts @@ -0,0 +1,113 @@ +import { NativeModules } from "react-native"; +import NativeContacts from "./src/NativeContacts"; +import { Contact } from "./type"; + +const isTurboModuleEnabled = global.__turboModuleProxy != null; +const Contacts = isTurboModuleEnabled ? NativeContacts : NativeModules.Contacts; + +async function getAll(): Promise { + return Contacts.getAll(); +} + +async function getAllWithoutPhotos(): Promise { + return Contacts.getAllWithoutPhotos(); +} + +async function getContactById(contactId: string): Promise { + return Contacts.getContactById(contactId); +} + +async function getCount(): Promise { + return Contacts.getCount(); +} + +async function getPhotoForId(contactId: string): Promise { + return Contacts.getPhotoForId(contactId); +} + +async function addContact(contact: Partial): Promise { + return Contacts.addContact(contact); +} + +async function openContactForm(contact: Partial): Promise { + return Contacts.openContactForm(contact); +} + +async function openExistingContact(contact: Contact): Promise { + return Contacts.openExistingContact(contact); +} + +async function viewExistingContact(contact: { + recordID: string; +}): Promise { + return Contacts.viewExistingContact(contact); +} + +async function editExistingContact(contact: Contact): Promise { + return Contacts.editExistingContact(contact); +} + +async function updateContact( + contact: Partial & { recordID: string } +): Promise { + return Contacts.updateContact(contact); +} + +async function deleteContact(contact: Contact): Promise { + return Contacts.deleteContact(contact); +} + +async function getContactsMatchingString(str: string): Promise { + return Contacts.getContactsMatchingString(str); +} + +async function getContactsByPhoneNumber( + phoneNumber: string +): Promise { + return Contacts.getContactsByPhoneNumber(phoneNumber); +} + +async function getContactsByEmailAddress( + emailAddress: string +): Promise { + return Contacts.getContactsByEmailAddress(emailAddress); +} + +async function checkPermission(): Promise< + "authorized" | "denied" | "undefined" +> { + return Contacts.checkPermission(); +} + +async function requestPermission(): Promise< + "authorized" | "denied" | "undefined" +> { + return Contacts.requestPermission(); +} + +async function writePhotoToPath( + contactId: string, + file: string +): Promise { + return Contacts.writePhotoToPath(contactId, file); +} +export default { + getAll, + getAllWithoutPhotos, + getContactById, + getCount, + getPhotoForId, + addContact, + openContactForm, + openExistingContact, + viewExistingContact, + editExistingContact, + updateContact, + deleteContact, + getContactsMatchingString, + getContactsByPhoneNumber, + getContactsByEmailAddress, + checkPermission, + requestPermission, + writePhotoToPath, +}; diff --git a/ios/RCTContacts/RCTContacts.h b/ios/RCTContacts/RCTContacts.h index 22f49454..2b06f213 100644 --- a/ios/RCTContacts/RCTContacts.h +++ b/ios/RCTContacts/RCTContacts.h @@ -1,7 +1,19 @@ #import #import #import +#import -@interface RCTContacts : NSObject +#ifdef RCT_NEW_ARCH_ENABLED + +#import +@interface RCTContacts: NSObject + +#else + +#import +@interface RCTContacts : NSObject + +#endif @end + diff --git a/ios/RCTContacts/RCTContacts.m b/ios/RCTContacts/RCTContacts.mm similarity index 67% rename from ios/RCTContacts/RCTContacts.m rename to ios/RCTContacts/RCTContacts.mm index 2371aa4b..7275a1aa 100644 --- a/ios/RCTContacts/RCTContacts.m +++ b/ios/RCTContacts/RCTContacts.mm @@ -3,6 +3,11 @@ #import "RCTContacts.h" #import #import +#import + +// #ifdef RCT_NEW_ARCH_ENABLED +// #import "RNContactsSpec.h" +// #endif @implementation RCTContacts { CNContactStore * contactStore; @@ -43,6 +48,13 @@ - (NSDictionary *)constantsToExport }; } +RCT_REMAP_METHOD(getAll, withResolver:(RCTPromiseResolveBlock) resolve + withRejecter:(RCTPromiseRejectBlock) reject) +{ + [self getAll:resolve reject:reject]; +} + + RCT_EXPORT_METHOD(checkPermission:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) __unused reject) { @@ -533,7 +545,7 @@ -(NSString *) getFilePathForThumbnailImage:(CNContact*) contact recordID:(NSStri - (NSString *)getPathForDirectory:(int)directory { - NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES); + NSArray *paths = NSSearchPathForDirectoriesInDomains((NSSearchPathDirectory)directory, NSUserDomainMask, YES); return [paths firstObject]; } @@ -597,7 +609,7 @@ -(NSString *) getFilePathForThumbnailImage:(NSString *)recordID } } --(NSString *) getContact:(NSString *)recordID +-(NSDictionary *) getContact:(NSString *)recordID addressBook:(CNContactStore*)addressBook withThumbnails:(BOOL) withThumbnails { @@ -623,7 +635,7 @@ -(NSString *) getContact:(NSString *)recordID CNContact* contact = [addressBook unifiedContactWithIdentifier:recordID keysToFetch:keysToFetch error:&contactError]; if(!contact) - return [NSNull null]; + return nil; return [self contactToDictionary: contact withThumbnails:withThumbnails]; } @@ -720,7 +732,8 @@ -(NSString *) getContact:(NSString *)recordID currentViewController = currentViewController.presentedViewController; } - UIActivityIndicatorViewStyle *activityIndicatorStyle; + UIActivityIndicatorViewStyle activityIndicatorStyle; + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; UIColor *activityIndicatorBackgroundColor; if (@available(iOS 13, *)) { activityIndicatorStyle = UIActivityIndicatorViewStyleMedium; @@ -792,7 +805,7 @@ -(NSString *) getContact:(NSString *)recordID CNContactViewController *contactViewController = [CNContactViewController viewControllerForContact:contact]; // Add a cancel button which will close the view - contactViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:backTitle == nil ? @"Cancel" : backTitle style:UIBarButtonSystemItemCancel target:self action:@selector(cancelContactForm)]; + contactViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:backTitle == nil ? @"Cancel" : backTitle style:UIBarButtonItemStylePlain target:self action:@selector(cancelContactForm)]; contactViewController.delegate = self; @@ -881,7 +894,7 @@ -(NSString *) getContact:(NSString *)recordID //[controller presentViewController:alert animated:YES completion:nil]; // Add a cancel button which will close the view - controller.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Done" style:UIBarButtonSystemItemCancel target:self action:@selector(doneContactForm)]; + controller.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Done" style:UIBarButtonItemStylePlain target:self action:@selector(doneContactForm)]; controller.delegate = self; controller.allowsEditing = true; @@ -1160,38 +1173,33 @@ + (NSData*) imageData:(NSString*)sourceUri enum { WDASSETURL_PENDINGREADS = 1, WDASSETURL_ALLFINISHED = 0}; -+ (NSData*) loadImageAsset:(NSURL*)assetURL { - //thanks to http://www.codercowboy.com/code-synchronous-alassetlibrary-asset-existence-check/ - - __block NSData *data = nil; - __block NSConditionLock * albumReadLock = [[NSConditionLock alloc] initWithCondition:WDASSETURL_PENDINGREADS]; - //this *MUST* execute on a background thread, ALAssetLibrary tries to use the main thread and will hang if you're on the main thread. - dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - ALAssetsLibrary * assetLibrary = [[ALAssetsLibrary alloc] init]; - [assetLibrary assetForURL:assetURL - resultBlock:^(ALAsset *asset) { - ALAssetRepresentation *rep = [asset defaultRepresentation]; - - Byte *buffer = (Byte*)malloc(rep.size); - NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil]; - data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; - - [albumReadLock lock]; - [albumReadLock unlockWithCondition:WDASSETURL_ALLFINISHED]; - } failureBlock:^(NSError *error) { - RCTLog(@"asset error: %@", [error localizedDescription]); - - [albumReadLock lock]; - [albumReadLock unlockWithCondition:WDASSETURL_ALLFINISHED]; - }]; ++ (NSData *)loadImageAsset:(NSURL *)assetURL { + __block NSData *imageData = nil; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:@[assetURL] options:nil]; + if (result.count > 0) { + PHAsset *asset = result.firstObject; + PHImageManager *imageManager = [PHImageManager defaultManager]; + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; // Load image synchronously + + [imageManager requestImageDataForAsset:asset + options:options + resultHandler:^(NSData * _Nullable data, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) { + imageData = data; + dispatch_semaphore_signal(semaphore); + }]; + } else { + dispatch_semaphore_signal(semaphore); + } }); - - [albumReadLock lockWhenCondition:WDASSETURL_ALLFINISHED]; - [albumReadLock unlock]; - - RCTLog(@"asset lookup finished: %@ %@", [assetURL absoluteString], (data ? @"exists" : @"does not exist")); - - return data; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + + return imageData; } RCT_EXPORT_METHOD(deleteContact:(NSDictionary *)contactData resolver:(RCTPromiseResolveBlock) resolve @@ -1257,4 +1265,472 @@ + (BOOL)requiresMainQueueSetup return YES; } +// Thanks to this guard, we won't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +#endif + +- (void)getAll:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self getAllContacts:resolve reject:reject withThumbnails:true]; +} + + - (void)checkPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNAuthorizationStatus authStatus = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts]; + if (authStatus == CNAuthorizationStatusDenied || authStatus == CNAuthorizationStatusRestricted){ + resolve(@"denied"); + } else if (authStatus == CNAuthorizationStatusAuthorized){ + resolve(@"authorized"); + } else { + resolve(@"undefined"); + } + } + + + - (void)deleteContact:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + if(!contactStore) { + contactStore = [[CNContactStore alloc] init]; + } + + NSString* recordID = [contactData valueForKey:@"recordID"]; + + NSArray *keys = @[CNContactIdentifierKey]; + + + @try { + + CNMutableContact *contact = [[contactStore unifiedContactWithIdentifier:recordID keysToFetch:keys error:nil] mutableCopy]; + NSError *error; + CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; + [saveRequest deleteContact:contact]; + [contactStore executeSaveRequest:saveRequest error:&error]; + + resolve(recordID); + } + @catch (NSException *exception) { + reject(@"Error", [exception reason], nil); + } + } + + + - (void)editExistingContact:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore* contactStore = [self contactsStore:reject]; + if(!contactStore) + return; + + NSError* contactError; + selectedContact = nil; + NSString* recordID = [contactData valueForKey:@"recordID"]; + NSArray * keysToFetch =@[ + CNContactEmailAddressesKey, + CNContactPhoneNumbersKey, + CNContactFamilyNameKey, + CNContactGivenNameKey, + CNContactMiddleNameKey, + CNContactPostalAddressesKey, + CNContactOrganizationNameKey, + CNContactJobTitleKey, + CNContactImageDataAvailableKey, + CNContactThumbnailImageDataKey, + CNContactImageDataKey, + CNContactUrlAddressesKey, + CNContactBirthdayKey, + CNContactIdentifierKey, + [CNContactFormatter descriptorForRequiredKeysForStyle:CNContactFormatterStyleFullName], + [CNContactViewController descriptorForRequiredKeys]]; + + @try { + CNMutableContact* record = [[contactStore unifiedContactWithIdentifier:recordID keysToFetch:keysToFetch error:&contactError] mutableCopy]; + + NSMutableArray *phoneNumbers = [[NSMutableArray alloc]init]; + phoneNumbers = [NSMutableArray arrayWithArray:record.phoneNumbers]; + + for (id phoneData in [contactData valueForKey:@"phoneNumbers"]) { + NSString *number = [phoneData valueForKey:@"number"]; + + CNLabeledValue *contactPhoneNumber = [CNLabeledValue labeledValueWithLabel:CNLabelOther value:[CNPhoneNumber phoneNumberWithStringValue:number]]; + //record.phoneNumbers = @[contactPhoneNumber]; + [phoneNumbers addObject:contactPhoneNumber]; + } + + NSArray *phoneNumbersNew = [[NSArray alloc]init]; + phoneNumbersNew = [NSArray arrayWithArray:phoneNumbers]; + + + record.phoneNumbers = phoneNumbersNew; + + CNSaveRequest *request = [[CNSaveRequest alloc] init]; + [request updateContact:record]; + + selectedContact = record; + + [contactStore executeSaveRequest:request error:nil]; + + CNContactViewController *controller = [CNContactViewController viewControllerForContact:record]; + //controller.title = @"Saved!"; + UIAlertController *alert= [UIAlertController + alertControllerWithTitle:@"Saved!" + message:@"Number added to contact" + preferredStyle:UIAlertControllerStyleAlert]; + //[controller presentViewController:alert animated:YES completion:nil]; + + // Add a cancel button which will close the view + controller.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Done" style:UIBarButtonItemStylePlain target:self action:@selector(doneContactForm)]; + + controller.delegate = self; + controller.allowsEditing = true; + controller.allowsActions = true; + + dispatch_async(dispatch_get_main_queue(), ^{ + UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:controller]; + UIViewController *viewController = (UIViewController*)[[[[UIApplication sharedApplication] delegate] window] rootViewController]; + + //navigation.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName : [UIColor redColor]}; + + while (viewController.presentedViewController) + { + viewController = viewController.presentedViewController; + } + [viewController presentViewController:navigation animated:YES completion:nil]; + [controller presentViewController:alert animated:YES completion:nil]; + + self->updateContactPromise = resolve; + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + + [alert dismissViewControllerAnimated:YES completion:^{ + + //Dismissed + }]; + + }); + } + @catch (NSException *exception) { + reject(@"Error", [exception reason], nil); + } + } + + + - (void)getAllWithoutPhotos:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self getAllContacts:resolve reject:reject withThumbnails:false]; + } + + + - (void)getContactById:(nonnull NSString *)recordID resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore* contactStore = [self contactsStore:reject]; + if(!contactStore) + return; + + CNEntityType entityType = CNEntityTypeContacts; + if([CNContactStore authorizationStatusForEntityType:entityType] == CNAuthorizationStatusNotDetermined) + { + [contactStore requestAccessForEntityType:entityType completionHandler:^(BOOL granted, NSError * _Nullable error) { + if(granted){ + resolve([self getContact:recordID addressBook:contactStore withThumbnails:false]); + } + }]; + } + else if( [CNContactStore authorizationStatusForEntityType:entityType]== CNAuthorizationStatusAuthorized) + { + resolve([self getContact:recordID addressBook:contactStore withThumbnails:false]); + } + } + + + - (void)getContactsByEmailAddress:(NSString *)string resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore *contactStore = [[CNContactStore alloc] init]; + if (!contactStore) + return; + [self getContactsFromAddressBook:contactStore byEmailAddress:string resolve:resolve]; + } + + + - (void)getContactsByPhoneNumber:(NSString *)string resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore *contactStore = [[CNContactStore alloc] init]; + if (!contactStore) + return; + [self getContactsFromAddressBook:contactStore byPhoneNumber:string resolve:resolve]; + } + + + - (void)getContactsMatchingString:(NSString *)string resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore *contactStore = [[CNContactStore alloc] init]; + if (!contactStore) + return; + [self getContactsFromAddressBook:contactStore matchingString:string resolve:resolve]; + } + + + - (void)getCount:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self getAllContactsCount:resolve reject:reject]; + } + + + - (void)getPhotoForId:(nonnull NSString *)recordID resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore* contactStore = [self contactsStore:reject]; + if(!contactStore) + return; + + CNEntityType entityType = CNEntityTypeContacts; + if([CNContactStore authorizationStatusForEntityType:entityType] == CNAuthorizationStatusNotDetermined) + { + [contactStore requestAccessForEntityType:entityType completionHandler:^(BOOL granted, NSError * _Nullable error) { + if(granted){ + resolve([self getFilePathForThumbnailImage:recordID addressBook:contactStore]); + } + }]; + } + else if( [CNContactStore authorizationStatusForEntityType:entityType]== CNAuthorizationStatusAuthorized) + { + resolve([self getFilePathForThumbnailImage:recordID addressBook:contactStore]); + } + } + + +- (void)openContactForm:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNMutableContact * contact = [[CNMutableContact alloc] init]; + + [self updateRecord:contact withData:contactData]; + + CNContactViewController *controller = [CNContactViewController viewControllerForNewContact:contact]; + + + controller.delegate = self; + + dispatch_async(dispatch_get_main_queue(), ^{ + UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:controller]; + UIViewController *viewController = (UIViewController*)[[[[UIApplication sharedApplication] delegate] window] rootViewController]; + while (viewController.presentedViewController) + { + viewController = viewController.presentedViewController; + } + [viewController presentViewController:navigation animated:YES completion:nil]; + + self->updateContactPromise = resolve; + }); +} + + + - (void)openExistingContact:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + if(!contactStore) { + contactStore = [[CNContactStore alloc] init]; + } + + NSString* recordID = [contactData valueForKey:@"recordID"]; + NSString* backTitle = [contactData valueForKey:@"backTitle"]; + + NSArray *keys = @[CNContactIdentifierKey, + CNContactEmailAddressesKey, + CNContactBirthdayKey, + CNContactImageDataKey, + CNContactPhoneNumbersKey, + [CNContactFormatter descriptorForRequiredKeysForStyle:CNContactFormatterStyleFullName], + [CNContactViewController descriptorForRequiredKeys]]; + + @try { + + CNContact *contact = [contactStore unifiedContactWithIdentifier:recordID keysToFetch:keys error:nil]; + + CNContactViewController *contactViewController = [CNContactViewController viewControllerForContact:contact]; + + // Add a cancel button which will close the view + contactViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:backTitle == nil ? @"Cancel" : backTitle style:UIBarButtonItemStyleDone target:self action:@selector(cancelContactForm)]; + contactViewController.delegate = self; + + + dispatch_async(dispatch_get_main_queue(), ^{ + UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:contactViewController]; + + UIViewController *currentViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + + while (currentViewController.presentedViewController) + { + currentViewController = currentViewController.presentedViewController; + } + + UIActivityIndicatorViewStyle activityIndicatorStyle; + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; + UIColor *activityIndicatorBackgroundColor; + if (@available(iOS 13, *)) { + activityIndicatorStyle = UIActivityIndicatorViewStyleMedium; + activityIndicatorBackgroundColor = [UIColor secondarySystemGroupedBackgroundColor]; + } else { + activityIndicatorStyle = UIActivityIndicatorViewStyleGray; + activityIndicatorBackgroundColor = [UIColor whiteColor];; + } + + // Cover the contact view with an activity indicator so we can put it in edit mode without user seeing the transition + UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:activityIndicatorStyle]; + activityIndicatorView.frame = UIApplication.sharedApplication.keyWindow.frame; + [activityIndicatorView startAnimating]; + activityIndicatorView.backgroundColor = activityIndicatorBackgroundColor; + [navigation.view addSubview:activityIndicatorView]; + + [currentViewController presentViewController:navigation animated:YES completion:nil]; + + + // TODO should this 'fake click' method be used? For a brief instance + // Fake click edit button to enter edit mode + // dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // SEL selector = contactViewController.navigationItem.rightBarButtonItem.action; + // NSLog(@"!!!!!!!!!!!!!!!!!! FAKE CLICK!!! %@", NSStringFromSelector(selector)); + // id target = contactViewController.navigationItem.rightBarButtonItem.target; + // [target performSelector:selector]; + // }); + + + // We need to wait for a short while otherwise contactViewController will not respond to the selector (it has not initialized) + [contactViewController performSelector:@selector(toggleEditing:) withObject:nil afterDelay:0.1]; + + // remove the activity indicator after a delay so the underlying transition will have time to complete + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [activityIndicatorView removeFromSuperview]; + }); + + updateContactPromise = resolve; + }); + + } + @catch (NSException *exception) { + reject(@"Error", [exception reason], nil); + } + } + + + - (void)requestPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore* contactStore = [[CNContactStore alloc] init]; + + [contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { + [self checkPermission:resolve rejecter:reject]; + }]; + } + + + - (void)updateContact:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore* contactStore = [self contactsStore:reject]; + if(!contactStore) + return; + + NSError* contactError; + NSString* recordID = [contactData valueForKey:@"recordID"]; + NSMutableArray * keysToFetch = [NSMutableArray arrayWithArray: @[ + CNContactEmailAddressesKey, + CNContactPhoneNumbersKey, + CNContactFamilyNameKey, + CNContactGivenNameKey, + CNContactMiddleNameKey, + CNContactPostalAddressesKey, + CNContactOrganizationNameKey, + CNContactJobTitleKey, + CNContactImageDataAvailableKey, + CNContactThumbnailImageDataKey, + CNContactImageDataKey, + CNContactUrlAddressesKey, + CNContactBirthdayKey, + CNContactInstantMessageAddressesKey + ]]; + if(notesUsageEnabled) { + [keysToFetch addObject: CNContactNoteKey]; + } + + @try { + CNMutableContact* record = [[contactStore unifiedContactWithIdentifier:recordID keysToFetch:keysToFetch error:&contactError] mutableCopy]; + [self updateRecord:record withData:contactData]; + CNSaveRequest *request = [[CNSaveRequest alloc] init]; + [request updateContact:record]; + + [contactStore executeSaveRequest:request error:nil]; + + NSDictionary *contactDict = [self contactToDictionary:record withThumbnails:false]; + + resolve(contactDict); + } + @catch (NSException *exception) { + reject(@"Error", [exception reason], nil); + } + } + + + - (void)viewExistingContact:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + if(!contactStore) { + contactStore = [[CNContactStore alloc] init]; + } + + NSString* recordID = [contactData valueForKey:@"recordID"]; + NSString* backTitle = [contactData valueForKey:@"backTitle"]; + + NSArray *keys = @[CNContactIdentifierKey, + CNContactEmailAddressesKey, + CNContactBirthdayKey, + CNContactImageDataKey, + CNContactPhoneNumbersKey, + [CNContactFormatter descriptorForRequiredKeysForStyle:CNContactFormatterStyleFullName], + [CNContactViewController descriptorForRequiredKeys]]; + + @try { + + CNContact *contact = [contactStore unifiedContactWithIdentifier:recordID keysToFetch:keys error:nil]; + + CNContactViewController *contactViewController = [CNContactViewController viewControllerForContact:contact]; + + // Add a cancel button which will close the view + contactViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:backTitle == nil ? @"Cancel" : backTitle style:UIBarButtonItemStylePlain target:self action:@selector(cancelContactForm)]; + contactViewController.delegate = self; + + + dispatch_async(dispatch_get_main_queue(), ^{ + UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:contactViewController]; + + UIViewController *currentViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + + while (currentViewController.presentedViewController) + { + currentViewController = currentViewController.presentedViewController; + } + + [currentViewController presentViewController:navigation animated:YES completion:nil]; + + updateContactPromise = resolve; + }); + + } + @catch (NSException *exception) { + reject(@"Error", [exception reason], nil); + } + } + + + - (void)writePhotoToPath:(NSString *)contactId file:(NSString *)file resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + reject(@"Error", @"not implemented", nil); + } + +- (void)addContact:(NSDictionary *)contactData resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + CNContactStore* contactStore = [self contactsStore:reject]; + if(!contactStore) + return; + + CNMutableContact * contact = [[CNMutableContact alloc] init]; + + [self updateRecord:contact withData:contactData]; + + @try { + CNSaveRequest *request = [[CNSaveRequest alloc] init]; + [request addContact:contact toContainerWithIdentifier:nil]; + + [contactStore executeSaveRequest:request error:nil]; + + NSDictionary *contactDict = [self contactToDictionary:contact withThumbnails:false]; + + resolve(contactDict); + } + @catch (NSException *exception) { + reject(@"Error", [exception reason], nil); + } +} + @end diff --git a/package.json b/package.json index fd764b7d..bffffc16 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,18 @@ "bugs": { "url": "https://github.com/rt2zz/react-native-contacts/issues" }, + "@react-native-community/bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + "typescript" + ] + }, "homepage": "https://github.com/rt2zz/react-native-contacts", - "main": "index.js", - "types": "index.d.ts", + "main": "index.ts", + "types": "index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, @@ -43,7 +52,13 @@ "devDependencies": { "react-native-cli": "^2.0.1" }, + "codegenConfig": { + "name": "RNContactsSpec", + "type": "modules", + "jsSrcsDir": "src" + }, "peerDependencies": { - "react-native": ">=0.64.0" + "react-native": "*", + "react": "*" } } diff --git a/react-native-contacts.podspec b/react-native-contacts.podspec index c9df084b..8d1c6346 100644 --- a/react-native-contacts.podspec +++ b/react-native-contacts.podspec @@ -1,18 +1,37 @@ require 'json' -package_json = JSON.parse(File.read('package.json')) +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) +folly_version = '2021.07.22.00' +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' Pod::Spec.new do |s| + # This guard prevent to install the dependencies when we run `pod install` in the old architecture. + + s.name = "react-native-contacts" - s.version = package_json["version"] - s.summary = package_json["description"] + s.version = package["version"] + s.summary = package["description"] s.homepage = "https://github.com/geektimecoil/react-native-onesignal" - s.license = package_json["license"] - s.author = { package_json["author"] => package_json["author"] } - s.platform = :ios, "9.0" - s.source = { :git => "https://github.com/rt2zz/react-native-contacts.git" } - s.source_files = 'ios/RCTContacts/*.{h,m}' + s.license = package["license"] + s.author = { package["author"] => package["author"] } + s.platform = :ios, "12.0" + s.source = { :git => "https://github.com/rt2zz/react-native-contacts.git", :tag => "v#{s.version}" } + s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency 'React-Core' + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-Codegen" + s.dependency "RCT-Folly", folly_version + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + install_modules_dependencies(s) + end end diff --git a/src/NativeContacts.ts b/src/NativeContacts.ts new file mode 100644 index 00000000..9b4b76b1 --- /dev/null +++ b/src/NativeContacts.ts @@ -0,0 +1,27 @@ +import type { TurboModule } from "react-native/Libraries/TurboModule/RCTExport"; +import { TurboModuleRegistry } from "react-native"; +import { Contact } from "../type"; + +export interface Spec extends TurboModule { + getAll: () => Promise; + getAllWithoutPhotos: () => Promise; + getContactById: (contactId: string) => Promise; + getCount: () => Promise; + getPhotoForId: (contactId: string) => Promise; + addContact: (contact: Object) => Promise; + openContactForm: (contact: Object) => Promise; + openExistingContact: (contact: Object) => Promise; + viewExistingContact: (contact: { recordID: string }) => Promise; + editExistingContact: (contact: Object) => Promise; + updateContact: (contact: Object) => Promise; + deleteContact: (contact: Object) => Promise; + getContactsMatchingString: (str: string) => Promise; + getContactsByPhoneNumber: (phoneNumber: string) => Promise; + getContactsByEmailAddress: (emailAddress: string) => Promise; + checkPermission: () => Promise; + requestPermission: () => Promise; + writePhotoToPath: (contactId: string, file: string) => Promise; + iosEnableNotesUsage: (enabled: boolean) => void; +} + +export default TurboModuleRegistry.get("RCTContacts"); diff --git a/type.ts b/type.ts new file mode 100644 index 00000000..1e45d83a --- /dev/null +++ b/type.ts @@ -0,0 +1,62 @@ +export interface EmailAddress { + label: string; + email: string; +} + +export interface PhoneNumber { + label: string; + number: string; +} + +export interface PostalAddress { + label: string; + formattedAddress: string; + street: string; + pobox: string; + neighborhood: string; + city: string; + region: string; + state: string; + postCode: string; + country: string; +} + +export interface InstantMessageAddress { + username: string; + service: string; +} + +export interface Birthday { + day: number; + month: number; + year: number; +} + +export interface UrlAddress { + url: string; + label: string; +} + +export interface Contact { + recordID: string; + backTitle: string; + company: string | null; + emailAddresses: EmailAddress[]; + displayName: string; + familyName: string; + givenName: string; + middleName: string; + jobTitle: string; + phoneNumbers: PhoneNumber[]; + hasThumbnail: boolean; + thumbnailPath: string; + isStarred: boolean; + postalAddresses: PostalAddress[]; + prefix: string; + suffix: string; + department: string; + birthday: Birthday; + imAddresses: InstantMessageAddress[]; + urlAddresses: UrlAddress[]; + note: string; +}