diff --git a/.gitignore b/.gitignore index f06235c..380cdfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.gradle/ diff --git a/android/.gradle/5.6.4/executionHistory/executionHistory.lock b/android/.gradle/5.6.4/executionHistory/executionHistory.lock deleted file mode 100644 index aae6757..0000000 Binary files a/android/.gradle/5.6.4/executionHistory/executionHistory.lock and /dev/null differ diff --git a/android/.gradle/5.6.4/fileChanges/last-build.bin b/android/.gradle/5.6.4/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/android/.gradle/5.6.4/fileChanges/last-build.bin and /dev/null differ diff --git a/android/.gradle/5.6.4/fileHashes/fileHashes.bin b/android/.gradle/5.6.4/fileHashes/fileHashes.bin deleted file mode 100644 index 49a2475..0000000 Binary files a/android/.gradle/5.6.4/fileHashes/fileHashes.bin and /dev/null differ diff --git a/android/.gradle/5.6.4/fileHashes/fileHashes.lock b/android/.gradle/5.6.4/fileHashes/fileHashes.lock deleted file mode 100644 index 66a5bb8..0000000 Binary files a/android/.gradle/5.6.4/fileHashes/fileHashes.lock and /dev/null differ diff --git a/android/.gradle/5.6.4/gc.properties b/android/.gradle/5.6.4/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 69916da..0000000 Binary files a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/android/.gradle/buildOutputCleanup/cache.properties b/android/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index dc44c0c..0000000 --- a/android/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Fri May 01 00:10:19 EDT 2020 -gradle.version=5.6.4 diff --git a/android/.gradle/buildOutputCleanup/outputFiles.bin b/android/.gradle/buildOutputCleanup/outputFiles.bin deleted file mode 100644 index 0536072..0000000 Binary files a/android/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ diff --git a/android/.gradle/vcs-1/gc.properties b/android/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android/src/main/java/com/teamhive/capacitor/ContactDataExtractorVisitor.java b/android/src/main/java/com/teamhive/capacitor/ContactDataExtractorVisitor.java new file mode 100644 index 0000000..04fd8ca --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/ContactDataExtractorVisitor.java @@ -0,0 +1,42 @@ +package com.teamhive.capacitor; + +import android.database.Cursor; +import android.provider.ContactsContract; +import com.getcapacitor.JSArray; +import com.getcapacitor.JSObject; +import com.teamhive.capacitor.contentQuery.ContentQueryService; +import com.teamhive.capacitor.utils.Visitor; + +import java.util.Map; + +public class ContactDataExtractorVisitor implements Visitor { + + private Map projectionMap; + + private JSArray phoneNumbers = new JSArray(); + private JSArray emailAddresses = new JSArray(); + + public ContactDataExtractorVisitor(Map projectionMap) { + this.projectionMap = projectionMap; + } + + @Override + public void visit(Cursor cursor) { + JSObject currentDataRecord = ContentQueryService.extractDataFromResultSet(cursor, projectionMap); + String currentMimeType = currentDataRecord.getString(PluginContactFields.MIME_TYPE); + + if (ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(currentMimeType)) { + emailAddresses.put(currentDataRecord.getString(ContactsContract.Contacts.Data.DATA1)); + } else if (ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(currentMimeType)) { + phoneNumbers.put(currentDataRecord.getString(ContactsContract.Contacts.Data.DATA1)); + } + } + + public JSArray getPhoneNumbers() { + return phoneNumbers; + } + + public JSArray getEmailAddresses() { + return emailAddresses; + } +} diff --git a/android/src/main/java/com/teamhive/capacitor/ContactExtractorVisitor.java b/android/src/main/java/com/teamhive/capacitor/ContactExtractorVisitor.java new file mode 100644 index 0000000..e11d626 --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/ContactExtractorVisitor.java @@ -0,0 +1,31 @@ +package com.teamhive.capacitor; + +import android.database.Cursor; +import com.getcapacitor.JSObject; +import com.teamhive.capacitor.contentQuery.ContentQueryService; +import com.teamhive.capacitor.utils.Visitor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ContactExtractorVisitor implements Visitor { + + private Map projectionMap; + + private List contacts = new ArrayList<>(); + + public ContactExtractorVisitor(Map projectionMap) { + this.projectionMap = projectionMap; + } + + @Override + public void visit(Cursor cursor) { + JSObject contact = ContentQueryService.extractDataFromResultSet(cursor, projectionMap); + contacts.add(contact); + } + + public List getContacts() { + return contacts; + } +} diff --git a/android/src/main/java/com/teamhive/capacitor/ContactPicker.java b/android/src/main/java/com/teamhive/capacitor/ContactPicker.java index 3153d95..800bf21 100644 --- a/android/src/main/java/com/teamhive/capacitor/ContactPicker.java +++ b/android/src/main/java/com/teamhive/capacitor/ContactPicker.java @@ -1,23 +1,22 @@ package com.teamhive.capacitor; -import com.getcapacitor.JSArray; -import com.getcapacitor.JSObject; -import com.getcapacitor.NativePlugin; -import com.getcapacitor.Plugin; -import com.getcapacitor.PluginCall; -import com.getcapacitor.PluginMethod; - import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; -import android.database.Cursor; +import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; +import com.getcapacitor.*; +import com.teamhive.capacitor.contentQuery.ContentQuery; +import com.teamhive.capacitor.contentQuery.ContentQueryService; +import com.teamhive.capacitor.utils.Utils; +import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; @NativePlugin( - permissions={ Manifest.permission.READ_CONTACTS }, + permissions = {Manifest.permission.READ_CONTACTS}, requestCodes = { ContactPicker.REQUEST_OPEN_CODE, ContactPicker.REQUEST_FETCH_CODE, @@ -25,19 +24,18 @@ } ) public class ContactPicker extends Plugin { + // Request codes protected static final int REQUEST_OPEN_CODE = 11222; protected static final int REQUEST_FETCH_CODE = 10012; protected static final int REQUEST_PERMISSIONS_CODE = 10312; - private static final String[] CONTACT_FIELDS_PROJECTION; - private static final Map CONTACT_FIELDS_MAP = new HashMap(); + // Messages + public static final String ERROR_READ_CONTACT = "Unable to read contact data."; + public static final String ERROR_NO_PERMISSION = "User denied permission"; - static { - CONTACT_FIELDS_MAP.put(CommonDataKinds.Phone.DISPLAY_NAME, "displayName"); - CONTACT_FIELDS_MAP.put(CommonDataKinds.Email.ADDRESS, "emailAddress"); - CONTACT_FIELDS_PROJECTION = CONTACT_FIELDS_MAP.keySet().toArray(new String[]{}); - } + // Queries + public static final String CONTACT_DATA_SELECT_CLAUSE = ContactsContract.Data.LOOKUP_KEY + " = ? AND " + ContactsContract.Data.MIMETYPE + " IN('" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "', '" + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "')"; @PluginMethod() public void open(PluginCall call) { @@ -48,8 +46,7 @@ public void open(PluginCall call) { return; } saveCall(call); - Intent contactPickerIntent = new Intent(Intent.ACTION_PICK); - contactPickerIntent.setType(CommonDataKinds.Email.CONTENT_TYPE); + Intent contactPickerIntent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); startActivityForResult(call, contactPickerIntent, REQUEST_OPEN_CODE); } @@ -67,9 +64,9 @@ protected void handleRequestPermissionsResult(int requestCode, String[] permissi return; } - for (int result: grantResults) { + for (int result : grantResults) { if (result == PackageManager.PERMISSION_DENIED) { - savedCall.error("User denied permission"); + savedCall.error(ERROR_NO_PERMISSION); return; } } @@ -89,41 +86,79 @@ protected void handleOnActivityResult(int requestCode, int resultCode, Intent in return; } if (requestCode == REQUEST_OPEN_CODE) { - Cursor cursor = null; try { - cursor = getContext().getContentResolver().query(intent.getData(), CONTACT_FIELDS_PROJECTION, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - JSObject tempContact = new JSObject(); - try { - for (Map.Entry entry : CONTACT_FIELDS_MAP.entrySet()) { - int columnIndex = cursor.getColumnIndex(entry.getKey()); - tempContact.put(entry.getValue(), cursor.getString(columnIndex)); - } - } catch (Exception e) { - e.printStackTrace(); - } - JSObject contact = new JSObject(); - - JSArray emailAddresses = new JSArray(); - emailAddresses.put(tempContact.getString("emailAddress")); - - String displayName = tempContact.getString("displayName"); - contact.put("emailAddresses", emailAddresses); - contact.put("givenName", displayName.split(" ")[0]); - contact.put("familyName", displayName.split(" ")[1]); - - JSObject result = new JSObject(); - result.put("value", contact); - savedCall.success(result); - } - } catch (Exception e) { + JSObject contact = readContactData(intent, savedCall); + savedCall.success(Utils.wrapIntoResult(contact)); + } catch (IOException e) { + savedCall.error(ERROR_READ_CONTACT, e); + } + } + } + + private JSObject readContactData(Intent intent, PluginCall savedCall) throws IOException { + final Map projectionMap = getContactProjectionMap(); + ContentQuery contactQuery = new ContentQuery.Builder() + .withUri(intent.getData()) + .withProjection(projectionMap) + .build(); + + try (ContentQueryService.VisitableCursorWrapper contactVcw = ContentQueryService.query(getContext(), contactQuery)) { + + ContactExtractorVisitor contactExtractor = new ContactExtractorVisitor(projectionMap); + contactVcw.accept(contactExtractor); + List contacts = contactExtractor.getContacts(); + + if (contacts.size() == 0) { + return null; + } else { + JSObject chosenContact = contacts.get(0); - } finally { - if (cursor != null) { - cursor.close(); + Map dataProjectionMap = getContactDataProjectionMap(); + ContentQuery contactDataQuery = new ContentQuery.Builder() + .withUri(ContactsContract.Data.CONTENT_URI) + .withProjection(dataProjectionMap) + .withSelection(CONTACT_DATA_SELECT_CLAUSE) + .withSelectionArgs(new String[]{chosenContact.getString(PluginContactFields.IDENTIFIER)}) + .withSortOrder(ContactsContract.Data.MIMETYPE) + .build(); + + try (ContentQueryService.VisitableCursorWrapper dataVcw = ContentQueryService.query(getContext(), contactDataQuery)) { + + ContactDataExtractorVisitor contactDataExtractor = new ContactDataExtractorVisitor(dataProjectionMap); + dataVcw.accept(contactDataExtractor); + + return transformContactObject(chosenContact, contactDataExtractor.getEmailAddresses(), contactDataExtractor.getPhoneNumbers()); } } } } + private JSObject transformContactObject(JSObject tempContact, JSArray emailAddresses, JSArray phoneNumbers) { + JSObject contact = new JSObject(); + contact.put(PluginContactFields.IDENTIFIER, tempContact.getString(PluginContactFields.IDENTIFIER)); + String displayName = tempContact.getString(PluginContactFields.DISPLAY_NAME); + contact.put(PluginContactFields.FULL_NAME, displayName); + if (displayName != null && displayName.contains(" ")) { + contact.put(PluginContactFields.GIVEN_NAME, displayName.split(" ")[0]); + contact.put(PluginContactFields.FAMILY_NAME, displayName.split(" ")[1]); + } + contact.put(PluginContactFields.EMAIL_ADDRESSES, emailAddresses); + contact.put(PluginContactFields.PHONE_NUMBERS, phoneNumbers); + return contact; + } + + private Map getContactProjectionMap() { + Map contactFieldsMap = new HashMap<>(); + contactFieldsMap.put(ContactsContract.Contacts.LOOKUP_KEY, PluginContactFields.IDENTIFIER); + contactFieldsMap.put(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, PluginContactFields.DISPLAY_NAME); + return contactFieldsMap; + } + + private Map getContactDataProjectionMap() { + Map contactFieldsMap = new HashMap<>(); + contactFieldsMap.put(CommonDataKinds.Email.MIMETYPE, PluginContactFields.MIME_TYPE); + contactFieldsMap.put(ContactsContract.Data.DATA1, ContactsContract.Data.DATA1); + return contactFieldsMap; + } + } diff --git a/android/src/main/java/com/teamhive/capacitor/PluginContactFields.java b/android/src/main/java/com/teamhive/capacitor/PluginContactFields.java new file mode 100644 index 0000000..30a6fa3 --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/PluginContactFields.java @@ -0,0 +1,12 @@ +package com.teamhive.capacitor; + +public class PluginContactFields { + public static final String IDENTIFIER = "identifier"; + public static final String DISPLAY_NAME = "displayName"; + public static final String FULL_NAME = "fullName"; + public static final String GIVEN_NAME = "givenName"; + public static final String FAMILY_NAME = "familyName"; + public static final String EMAIL_ADDRESSES = "emailAddresses"; + public static final String PHONE_NUMBERS = "phoneNumbers"; + public static final String MIME_TYPE = "mimeType"; +} diff --git a/android/src/main/java/com/teamhive/capacitor/contentQuery/ContentQuery.java b/android/src/main/java/com/teamhive/capacitor/contentQuery/ContentQuery.java new file mode 100644 index 0000000..ae16830 --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/contentQuery/ContentQuery.java @@ -0,0 +1,83 @@ +package com.teamhive.capacitor.contentQuery; + +import android.net.Uri; +import android.os.CancellationSignal; + +import java.util.Map; + +public class ContentQuery { + + private Uri uri; + private Map projection; + private String selection; + private String[] selectionArgs; + private String sortOrder; + private CancellationSignal cancellationSignal; + + private ContentQuery() { + } + + public Uri getUri() { + return uri; + } + + public Map getProjection() { + return projection; + } + + public String getSelection() { + return selection; + } + + public String[] getSelectionArgs() { + return selectionArgs; + } + + public String getSortOrder() { + return sortOrder; + } + + public CancellationSignal getCancellationSignal() { + return cancellationSignal; + } + + public static class Builder { + + private ContentQuery contentQuery = new ContentQuery(); + + public Builder withUri(Uri uri) { + contentQuery.uri = uri; + return this; + } + + public Builder withProjection(Map projection) { + contentQuery.projection = projection; + return this; + } + + public Builder withSelection(String selection) { + contentQuery.selection = selection; + return this; + } + + public Builder withSelectionArgs(String[] selectionArgs) { + contentQuery.selectionArgs = selectionArgs; + return this; + } + + public Builder withSortOrder(String sortOrder) { + contentQuery.sortOrder = sortOrder; + return this; + } + + public Builder withCancellationSignal(CancellationSignal cancellationSignal) { + contentQuery.cancellationSignal = cancellationSignal; + return this; + } + + public ContentQuery build() { + return contentQuery; + } + + } +} diff --git a/android/src/main/java/com/teamhive/capacitor/contentQuery/ContentQueryService.java b/android/src/main/java/com/teamhive/capacitor/contentQuery/ContentQueryService.java new file mode 100644 index 0000000..53d8d00 --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/contentQuery/ContentQueryService.java @@ -0,0 +1,60 @@ +package com.teamhive.capacitor.contentQuery; + +import android.content.Context; +import android.database.Cursor; +import com.getcapacitor.JSObject; +import com.teamhive.capacitor.utils.Utils; +import com.teamhive.capacitor.utils.Visitable; +import com.teamhive.capacitor.utils.Visitor; + +import java.io.Closeable; +import java.util.Map; + +public class ContentQueryService { + + public static VisitableCursorWrapper query(Context context, ContentQuery query) { + try { + String[] projectionArray = Utils.getMapKeysAsArray(query.getProjection()); + Cursor cursor = context.getContentResolver().query(query.getUri(), projectionArray, query.getSelection(), query.getSelectionArgs(), query.getSortOrder(), query.getCancellationSignal()); + return new VisitableCursorWrapper(cursor); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static JSObject extractDataFromResultSet(Cursor cursor, Map projectionMap) { + try { + JSObject result = new JSObject(); + for (Map.Entry entry : projectionMap.entrySet()) { + int columnIndex = cursor.getColumnIndex(entry.getKey()); + result.put(entry.getValue(), cursor.getString(columnIndex)); + } + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static class VisitableCursorWrapper implements Visitable, Closeable, AutoCloseable { + + private Cursor cursor; + + private VisitableCursorWrapper(Cursor cursor) { + this.cursor = cursor; + } + + public void accept(Visitor visitor) { + while (cursor != null && cursor.moveToNext()) { + visitor.visit(cursor); + } + } + + @Override + public void close() { + if (cursor != null) { + cursor.close(); + } + } + } + +} diff --git a/android/src/main/java/com/teamhive/capacitor/utils/Utils.java b/android/src/main/java/com/teamhive/capacitor/utils/Utils.java new file mode 100644 index 0000000..0f8fd1e --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/utils/Utils.java @@ -0,0 +1,19 @@ +package com.teamhive.capacitor.utils; + +import com.getcapacitor.JSObject; + +import java.util.Map; + +public class Utils { + + public static String[] getMapKeysAsArray(Map map) { + return map.keySet().toArray(new String[]{}); + } + + public static JSObject wrapIntoResult(JSObject contact) { + JSObject result = new JSObject(); + result.put("value", contact); + return result; + } + +} diff --git a/android/src/main/java/com/teamhive/capacitor/utils/Visitable.java b/android/src/main/java/com/teamhive/capacitor/utils/Visitable.java new file mode 100644 index 0000000..dcb964c --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/utils/Visitable.java @@ -0,0 +1,5 @@ +package com.teamhive.capacitor.utils; + +public interface Visitable { + void accept(Visitor visitor); +} diff --git a/android/src/main/java/com/teamhive/capacitor/utils/Visitor.java b/android/src/main/java/com/teamhive/capacitor/utils/Visitor.java new file mode 100644 index 0000000..d85b974 --- /dev/null +++ b/android/src/main/java/com/teamhive/capacitor/utils/Visitor.java @@ -0,0 +1,5 @@ +package com.teamhive.capacitor.utils; + +public interface Visitor { + void visit(T element); +}