diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -17,6 +17,8 @@ Provides a remote control for your media player Run Command Trigger remote commands from your phone or tablet + Contacts Synchronizer + Allow synchronizing the device\'s contacts book Ping Send and receive pings Notification sync @@ -216,5 +218,6 @@ To read and write SMS from your desktop you need to give permission to SMS To see phone calls and SMS from the desktop you need to give permission to phone calls and SMS To see a contact name instead of a phone number you need to give access to the phone\'s contacts + To see a contact name instead of a phone number you need to give access to the phone\'s contacts diff --git a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java --- a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java +++ b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Albert Vaca Cintora + * Copyright 2018 Simon Redman * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -31,15 +32,25 @@ import android.util.Base64OutputStream; import android.util.Log; +import org.json.JSONArray; + import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; public class ContactsHelper { @TargetApi(Build.VERSION_CODES.HONEYCOMB) + /** + * Lookup the name and photoID of a contact given a phone number + */ public static Map phoneNumberLookup(Context context, String number) { //Log.e("PhoneNumberLookup", number); @@ -111,5 +122,289 @@ try { output.close(); } catch(Exception ignored) { }; } } + + /** + * Return all the NAME_RAW_CONTACT_IDS which contribute an entry to a Contact in the database + *

+ * If the user has, for example, joined several contacts, on the phone, the IDs returned will + * be representative of the joined contact + *

+ * See here: https://developer.android.com/reference/android/provider/ContactsContract.Contacts.html + * for more information about the connection between contacts and raw contacts + * + * @param context android.content.Context running the request + * @return List of each NAME_RAW_CONTACT_ID in the Contacts database + */ + public static List getAllContactRawContactIDs(Context context) { + ArrayList toReturn = new ArrayList(); + + // Define the columns we want to read from the Contacts database + final String[] projection = new String[]{ + ContactsContract.Contacts.NAME_RAW_CONTACT_ID + }; + + Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; + Cursor contactsCursor = context.getContentResolver().query( + contactsUri, + projection, + null, null, null); + if (contactsCursor != null && contactsCursor.moveToFirst()) { + do { + Long contactID; + + int idIndex = contactsCursor.getColumnIndex(ContactsContract.Contacts.NAME_RAW_CONTACT_ID); + if (idIndex != -1) { + contactID = contactsCursor.getLong(idIndex); + } else { + // Something went wrong with this contact + // TODO: Investigate why this would happen + Log.e("ContactsHelper", "Got a contact which does not have a NAME_RAW_CONTACT_ID"); + continue; + } + + toReturn.add(contactID); + } while (contactsCursor.moveToNext()); + try { + contactsCursor.close(); + } catch (Exception e) { + } + } + + return toReturn; + } + + /** + * Return a mapping of raw contact IDs to a map of the requested data from the Contacts database + *

+ * If for some reason there is no row associated with the raw contact ID in the database, + * there will not be a corresponding field in the returned map + * + * @param context android.content.Context running the request + * @param IDs collection of raw contact IDs to look up + * @param contactsProjection List of column names to extract, defined in ContactsContract.Contacts + * @return mapping of raw contact IDs to desired values, which are a mapping of column names to the data contained there + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) // Needed for Cursor.getType(..) + public static Map> getColumnsFromContactsForRawContactIDs(Context context, Set IDs, String[] contactsProjection) { + HashMap> toReturn = new HashMap<>(); + + // Define the columns we want to read from the RawContacts database + final String[] rawContactsProjection = new String[]{ + ContactsContract.RawContacts._ID, + ContactsContract.RawContacts.CONTACT_ID + }; + + Uri rawContactsUri = ContactsContract.RawContacts.CONTENT_URI; + Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; + + Cursor rawContactsCursor = context.getContentResolver().query( + rawContactsUri, + rawContactsProjection, + null, + null, + null); + + if (rawContactsCursor != null && rawContactsCursor.moveToFirst()) { + do { + Long rawContactID; + Long contactID; + + int rawContactIDIndex = rawContactsCursor.getColumnIndex(ContactsContract.RawContacts._ID); + if (rawContactIDIndex != -1) { + rawContactID = rawContactsCursor.getLong(rawContactIDIndex); + } else { + // This raw contact didn't have an ID? Something is very wrong. + // TODO: Investigate why this would happen + Log.e("ContactsHelper", "Got a raw contact which does not have an _ID"); + continue; + } + + // Filter only for the rawContactIDs we were asked to look up + if (!IDs.contains(rawContactID)) { + // This should be achievable (and faster) by providing a selection + // and selectionArgs when fetching rawContactsCursor, but I can't + // figure that out + continue; + } + + int contactIDIndex = rawContactsCursor.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID); + if (contactIDIndex != -1) { + contactID = rawContactsCursor.getLong(contactIDIndex); + } else { + // Something went wrong with this contact + // TODO: Investigate why this would happen + Log.e("ContactsHelper", "Got a raw contact which does not have a CONTACT_ID"); + continue; + } + + // Filter on only the contact we are interested in + final String contactsSelection = ContactsContract.Contacts._ID + " == ? "; + final String[] contactsArgs = new String[]{contactID.toString()}; + + Cursor contactsCursor = context.getContentResolver().query( + contactsUri, + contactsProjection, + contactsSelection, + contactsArgs, null + ); + + Map requestedData = new HashMap<>(); + + if (contactsCursor != null && contactsCursor.moveToFirst()) { + // For each column, collect the data from that column + for (String column : contactsProjection) { + int index = contactsCursor.getColumnIndex(column); + // Since we might be getting various kinds of data, Object is the best we can do + Object data; + int type; + if (index == -1) { + // This raw contact didn't have an ID? Something is very wrong. + // TODO: Investigate why this would happen + Log.e("ContactsHelper", "Got a raw contact which does not have an _ID"); + continue; + } + + type = contactsCursor.getType(index); + switch (type) { + case Cursor.FIELD_TYPE_INTEGER: + data = contactsCursor.getInt(index); + break; + case Cursor.FIELD_TYPE_FLOAT: + data = contactsCursor.getFloat(index); + break; + case Cursor.FIELD_TYPE_STRING: + data = contactsCursor.getString(index); + break; + case Cursor.FIELD_TYPE_BLOB: + data = contactsCursor.getBlob(index); + break; + default: + Log.e("ContactsHelper", "Got an undefined type of column " + column); + continue; + } + + requestedData.put(column, data); + } + } + contactsCursor.close(); + + toReturn.put(rawContactID, requestedData); + } while (rawContactsCursor.moveToNext()); + rawContactsCursor.close(); + } + + return toReturn; + } + + /** + * Return a mapping of raw contact IDs to a map of the requested data from the Data database + *

+ * If for some reason there is no row associated with the raw contact ID in the database, + * there will not be a corresponding field in the returned map + *

+ * For some types of data, there may be many entries in the Data database with the same raw contact ID, + * so a list of the relevant data is returned + * + * @param context android.content.Context running the request + * @param IDs collection of raw contact IDs to look up + * @param dataMimetype Mimetype of the column to look up, defined in ContactsContract.CommonDataKinds..CONTENT_ITEM_TYPE + * @param dataProjection List of column names to extract, defined in ContactsContract.CommonDataKinds. + * @return mapping of raw contact IDs to desired values, which are a mapping of column names to the data contained there + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) // Needed for Cursor.getType(..) + public static Map>> getColumnsFromDataForRawContactIDs(Context context, Set IDs, String dataMimetype, String[] dataProjection) { + HashMap>> toReturn = new HashMap<>(); + + // Define a filter for the type of data we were asked to get + final String dataSelection = ContactsContract.Data.MIMETYPE + " == ?"; + final String[] dataSelectionArgs = {dataMimetype}; + + Uri dataUri = ContactsContract.Data.CONTENT_URI; + + // Regardless of what the user requested, we need the RAW_CONTACT_ID field + // This will not be returned to the user if it wasn't asked for + Set actualDataProjectionSet = new HashSet(); + actualDataProjectionSet.addAll(Arrays.asList(dataProjection)); + actualDataProjectionSet.add(ContactsContract.Data.RAW_CONTACT_ID); + + String[] actualDataProjection = new String[0]; + actualDataProjection = actualDataProjectionSet.toArray(actualDataProjection); + + Cursor dataCursor = context.getContentResolver().query( + dataUri, + actualDataProjection, + dataSelection, + dataSelectionArgs, + null); + + if (dataCursor != null && dataCursor.moveToFirst()) { + do { + Long rawContactID; + + Map requestedData = new HashMap<>(); + + int rawContactIDIndex = dataCursor.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID); + if (rawContactIDIndex != -1) { + rawContactID = dataCursor.getLong(rawContactIDIndex); + } else { + // This didn't have a RAW_CONTACT_ID? Something is very wrong. + // TODO: Investigate why this would happen + Log.e("ContactsHelper", "Got a data contact which does not have a RAW_CONTACT_ID"); + continue; + } + + // Filter only for the rawContactIDs we were asked to look up + if (!IDs.contains(rawContactID)) { + // This should be achievable (and faster) by providing a selection + // and selectionArgs when fetching dataCursor, but I can't + // figure that out + continue; + } + // For each column, collect the data from that column + for (String column : dataProjection) { + int index = dataCursor.getColumnIndex(column); + // Since we might be getting various kinds of data, Object is the best we can do + Object data; + int type; + if (index == -1) { + // This raw contact didn't have an ID? Something is very wrong. + // TODO: Investigate why this would happen + Log.e("ContactsHelper", "Got a raw contact which does not have an _ID"); + continue; + } + + type = dataCursor.getType(index); + switch (type) { + case Cursor.FIELD_TYPE_INTEGER: + data = dataCursor.getInt(index); + break; + case Cursor.FIELD_TYPE_FLOAT: + data = dataCursor.getFloat(index); + break; + case Cursor.FIELD_TYPE_STRING: + data = dataCursor.getString(index); + break; + case Cursor.FIELD_TYPE_BLOB: + data = dataCursor.getBlob(index); + break; + default: + Log.w("ContactsHelper", "Got an undefined type of column " + column + " -- Skipping"); + continue; + } + + requestedData.put(column, data); + } + + // If we have not already stored some data for this contact, make a new list + if (!toReturn.containsKey(rawContactID)) { + toReturn.put(rawContactID, new ArrayList>()); + } + toReturn.get(rawContactID).add(requestedData); + } while (dataCursor.moveToNext()); + dataCursor.close(); + } + + return toReturn; + } } diff --git a/src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.java b/src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.java @@ -0,0 +1,452 @@ +/* + * ContactsPlugin.java - This file is part of KDE Connect's Android App + * Implement a way to request and send contact information + * + * Copyright 2018 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Plugins.ContactsPlugin; + +import android.Manifest; +import android.provider.ContactsContract; +import android.util.Log; + +import org.json.JSONArray; +import org.kde.kdeconnect.Helpers.ContactsHelper; +import org.kde.kdeconnect.NetworkPackage; +import org.kde.kdeconnect.Plugins.Plugin; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ContactsPlugin extends Plugin { + + /** + * Used to request the device send the unique ID of every contact + */ + public static final String PACKAGE_TYPE_CONTACTS_REQUEST_ALL_UIDS = "kdeconnect.contacts.request_all_uids"; + + /** + * Used to request the names for the contacts corresponding to a list of UIDs + *

+ * It shall contain the key "uids", which will have a list of uIDs (long int, as string) + */ + public static final String PACKAGE_TYPE_CONTACTS_REQUEST_NAMES_BY_UIDS = "kdeconnect.contacts.request_names_by_uid"; + + /** + * Used to request the phone numbers for the contacts corresponding to a list of UIDs + *

+ * It shall contain the key "uids", which will have a list of uIDs (long int, as string) + */ + public static final String PACKAGE_TYPE_CONTACTS_REQUEST_PHONES_BY_UIDS = "kdeconnect.contacts.request_phones_by_uid"; + + /** + * Used to request the email addresses for the contacts corresponding to a list of UIDs + *

+ * It shall contain the key "uids", which will have a list of uIDs (long int, as string) + */ + public static final String PACKAGE_TYPE_CONTACTS_REQUEST_EMAILS_BY_UIDS = "kdeconnect.contacts.request_emails_by_uid"; + + /** + * Response indicating the package contains a list of contact uIDs + *

+ * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) + * The returned IDs can be used in future requests for more information about the contact + */ + public static final String PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS = "kdeconnect.contacts.response_uids"; + + /** + * Response indicating the package contains a list of contact names + *

+ * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) + * then, for each UID, there shall be a field with the key of that UID and the value of the name of the contact + *

+ * For example: + * ( 'uids' : ['1', '3', '15'], + * '1' : 'John Smith', + * '3' : 'Abe Lincoln', + * '15' : 'Mom' ) + */ + public static final String PACKAGE_TYPE_CONTACTS_RESPONSE_NAMES = "kdeconnect.contacts.response_names"; + + /** + * Response indicating the package contains a list of contact numbers + *

+ * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) + * then, for each UID, there shall be a 3-field list containing the phone number, the type, and the label + *

+ * For now, the values in types are undefined, but coincidentally match the list here: + * https://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Phone.html + *

+ * The label field is defined to be the custom label if the number is a custom type, otherwise the empty string + *

+ * For example: + * ( 'uids' : ['1', '3', '15'], + * '1' : [ [ '+221234', '2', '' ] ] + * '3' : [ [ '+1(222)333-4444', '0', 'Big Red Button' ] ] // This number has a custom type + * '15' : [ [ '6061234', '1', '' ] ] ) + */ + public static final String PACKAGE_TYPE_CONTACTS_RESPONSE_PHONES = "kdeconnect.contacts.response_phones"; + + /** + * Response indicating the package contains a list of contact email addresses + *

+ * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) + * then, for each UID, there shall be a 3-field list containing the email address, the type, and the label + *

+ * For now, the values in types are undefined, but coincidentally match the list here: + * https://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Email.html + *

+ * The label field is defined to be the custom label if the number is a custom type, otherwise the empty string + *

+ * For example: + * ( 'uids' : ['1', '3', '15'], + * '1' : [ [ 'john@example.com', '2', '' ] ] + * '3' : [ [ 'abel@example.com', '0', 'Priority' ] ] // This email address has a custom type + * '15' : [ [ 'mom@example.com', '1', '' ] ] ) + */ + public static final String PACKAGE_TYPE_CONTACTS_RESPONSE_EMAILS = "kdeconnect.contacts.response_emails"; + + private int contactsPermissionExplanation = R.string.contacts_permission_explanation; + + @Override + public String getDisplayName() { + return context.getResources().getString(R.string.pref_plugin_contacts); + } + + @Override + public String getDescription() { + return context.getResources().getString(R.string.pref_plugin_contacts_desc); + } + + @Override + public String[] getSupportedPackageTypes() { + return new String[]{ + PACKAGE_TYPE_CONTACTS_REQUEST_ALL_UIDS, + PACKAGE_TYPE_CONTACTS_REQUEST_NAMES_BY_UIDS, + PACKAGE_TYPE_CONTACTS_REQUEST_PHONES_BY_UIDS, + PACKAGE_TYPE_CONTACTS_REQUEST_EMAILS_BY_UIDS + }; + } + + @Override + public String[] getOutgoingPackageTypes() { + return new String[]{ + PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS, + PACKAGE_TYPE_CONTACTS_RESPONSE_NAMES, + PACKAGE_TYPE_CONTACTS_RESPONSE_PHONES, + PACKAGE_TYPE_CONTACTS_RESPONSE_EMAILS + }; + } + + @Override + public boolean onCreate() { + permissionExplanation = contactsPermissionExplanation; + + return true; + } + + @Override + /** + * Since this plugin could leak sensitive information, probably best to leave disabled by default + */ + public boolean isEnabledByDefault() { + return false; + } + + @Override + public String[] getRequiredPermissions() { + return new String[]{Manifest.permission.READ_CONTACTS}; + // One day maybe we will also support WRITE_CONTACTS, but not yet + } + + /** + * Return a unique identifier (long int) for all contacts in the Contacts database + *

+ * The identifiers returned can be used in future requests to get more information + * about the contact + * + * @param np The package containing the request + * @return true if successfully handled, false otherwise + */ + protected boolean handleRequestAllUIDs(NetworkPackage np) { + NetworkPackage reply = new NetworkPackage(PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS); + + List uIDs = ContactsHelper.getAllContactRawContactIDs(context); + + List uIDsAsStrings = new ArrayList(uIDs.size()); + + for (Long uID : uIDs) { + uIDsAsStrings.add(uID.toString()); + } + + reply.set("uids", uIDsAsStrings); + + device.sendPackage(reply); + + return true; + } + + protected boolean handleRequestNamesByUIDs(NetworkPackage np) { + if (!np.has("uids")) { + Log.e("ContactsPlugin", "handleRequestNamesByUIDs received a malformed packet with no uids key"); + return false; + } + + List uIDsAsStrings = np.getStringList("uids"); + + // Convert to Set to call getColumnsFromContactsForRawContactIDs + Set uIDs = new HashSet(uIDsAsStrings.size()); + for (String uID : uIDsAsStrings) { + uIDs.add(Long.parseLong(uID)); + } + + final String[] contactsProjection = new String[]{ + ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + }; + + Map> uIDsToNames = ContactsHelper.getColumnsFromContactsForRawContactIDs(context, uIDs, contactsProjection); + + // ContactsHelper.getColumnsFromContactsForRawContactIDs(..) is allowed to reply without + // some of the requested uIDs if they were not in the database, so update our list + uIDsAsStrings = new ArrayList<>(uIDsToNames.size()); + + NetworkPackage reply = new NetworkPackage(PACKAGE_TYPE_CONTACTS_RESPONSE_NAMES); + + // Add the names to the packet + for (Long uID : uIDsToNames.keySet()) { + Map data = uIDsToNames.get(uID); + String name; + + if (!data.containsKey(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)) { + // This contact apparently does not have a name + Log.w("ContactsPlugin", "Got a uID " + uID + " which does not have a name"); + continue; + } + + name = (String) data.get(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY); + + // Store this as a valid uID + uIDsAsStrings.add(uID.toString()); + // Add the uid : name pairing to the packet + reply.set(uID.toString(), name); + } + + // Add the valid uIDs to the packet + reply.set("uids", uIDsAsStrings); + + device.sendPackage(reply); + + return true; + } + + protected boolean handleRequestPhonesByUIDs(NetworkPackage np) { + if (!np.has("uids")) { + Log.e("ContactsPlugin", "handleRequestPhonesByUIDs received a malformed packet with no uids key"); + return false; + } + + List uIDsAsStrings = np.getStringList("uids"); + + // Convert to Set to call getColumnsFromDataForRawContactIDs + Set uIDs = new HashSet(uIDsAsStrings.size()); + for (String uID : uIDsAsStrings) { + uIDs.add(Long.parseLong(uID)); + } + + final String dataMimetype = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE; + final String[] dataProjection = { + // We want the actual phone number + ContactsContract.CommonDataKinds.Phone.NUMBER + // As well as what type it is + , ContactsContract.CommonDataKinds.Phone.TYPE + // Stores the label of the type of the number if it is a custom type + , ContactsContract.CommonDataKinds.Phone.LABEL}; + + Map>> uIDsToPhones = + ContactsHelper.getColumnsFromDataForRawContactIDs(context, uIDs, dataMimetype, dataProjection); + + // ContactsHelper.getColumnsFromContactsForRawContactIDs(..) is allowed to reply without + // some of the requested uIDs if they were not in the database, so update our list + uIDsAsStrings = new ArrayList<>(uIDsToPhones.size()); + + NetworkPackage reply = new NetworkPackage(PACKAGE_TYPE_CONTACTS_RESPONSE_PHONES); + + // Add the phone numbers to the packet + for (Long uID : uIDsToPhones.keySet()) { + List> allPhoneNumbers = new ArrayList<>(); + for (Map data : uIDsToPhones.get(uID)) { + HashMap numberTypesToNumbers = new HashMap<>(); + String number; + Integer type; + String label; // Label appears to only be defined if type is custom + + + if (!data.containsKey(ContactsContract.CommonDataKinds.Phone.NUMBER)) { + // This is the wrong data type? + Log.w("ContactsPlugin", "Got a uID " + uID + " which does not have a phone number"); + continue; + } + + number = (String) data.get(ContactsContract.CommonDataKinds.Phone.NUMBER); + + // The Android docs say type should be an int + // However, my phone has that field stored as a string... + Object typeField = data.get(ContactsContract.CommonDataKinds.Phone.TYPE); + if (typeField instanceof String) { + type = Integer.parseInt((String) typeField); + } else if (typeField instanceof Integer) { + type = (Integer) typeField; + } else { + Log.w("ContactsPlugin", "Android docs are wrong -- cannot get Java type of 'type' field"); + continue; // Continue in case something works... + } + + if (type == ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM) { + // If the label is defined, use it + label = (String) data.get(ContactsContract.CommonDataKinds.Phone.LABEL); + } else { + // Otherwise, use the empty string + label = ""; + } + allPhoneNumbers.add(Arrays.asList(new String[]{number, type.toString(), label})); + } + + // Store this as a valid uID + uIDsAsStrings.add(uID.toString()); + // Add the uid : [ ['number', 'type', 'label'] ... ] pairing to the packet + reply.set(uID.toString(), new JSONArray(allPhoneNumbers)); + } + + // Add the valid uIDs to the packet + reply.set("uids", uIDsAsStrings); + + device.sendPackage(reply); + + return true; + } + + protected boolean handleRequestEmailsByUIDs(NetworkPackage np) { + if (!np.has("uids")) { + Log.e("ContactsPlugin", "handleRequestEmailsByUIDs received a malformed packet with no uids key"); + return false; + } + + List uIDsAsStrings = np.getStringList("uids"); + + // Convert to Set to call getColumnsFromDataForRawContactIDs + Set uIDs = new HashSet(uIDsAsStrings.size()); + for (String uID : uIDsAsStrings) { + uIDs.add(Long.parseLong(uID)); + } + + final String dataMimetype = ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE; + final String[] dataProjection = { + // We want the actual phone number + ContactsContract.CommonDataKinds.Email.ADDRESS + // As well as what type it is + , ContactsContract.CommonDataKinds.Email.TYPE + // Stores the label of the type of the number if it is a custom type + , ContactsContract.CommonDataKinds.Email.LABEL}; + + Map>> uIDsToEmails = + ContactsHelper.getColumnsFromDataForRawContactIDs(context, uIDs, dataMimetype, dataProjection); + + // ContactsHelper.getColumnsFromContactsForRawContactIDs(..) is allowed to reply without + // some of the requested uIDs if they were not in the database, so update our list + uIDsAsStrings = new ArrayList<>(uIDsToEmails.size()); + + NetworkPackage reply = new NetworkPackage(PACKAGE_TYPE_CONTACTS_RESPONSE_EMAILS); + + // Add the email addresses to the packet + for (Long uID : uIDsToEmails.keySet()) { + List> allEmailAddresses = new ArrayList<>(); + for (Map data : uIDsToEmails.get(uID)) { + HashMap numberTypesToNumbers = new HashMap<>(); + String email; + Integer type; + String label; // Label appears to only be defined if type is custom + + + if (!data.containsKey(ContactsContract.CommonDataKinds.Email.ADDRESS)) { + // This is the wrong data mimetype? + Log.w("ContactsPlugin", "Got a uID " + uID + " which does not have an email address"); + continue; + } + + email = (String) data.get(ContactsContract.CommonDataKinds.Email.ADDRESS); + + // The Android docs say type should be an int + // However, my phone has that field stored as a string... + Object typeField = data.get(ContactsContract.CommonDataKinds.Email.TYPE); + if (typeField instanceof String) { + type = Integer.parseInt((String) typeField); + } else if (typeField instanceof Integer) { + type = (Integer) typeField; + } else { + Log.w("ContactsPlugin", "Android docs are wrong -- cannot get Java type of 'type' field"); + continue; // Continue in case something works... + } + + if (type == ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM) { + // If the label is defined, use it + label = (String) data.get(ContactsContract.CommonDataKinds.Email.LABEL); + } else { + // Otherwise, use the empty string + label = ""; + } + allEmailAddresses.add(Arrays.asList(new String[]{email, type.toString(), label})); + } + + // Store this as a valid uID + uIDsAsStrings.add(uID.toString()); + // Add the uid : [ ['number', 'type', 'label'] ... ] pairing to the packet + reply.set(uID.toString(), new JSONArray(allEmailAddresses)); + } + + // Add the valid uIDs to the packet + reply.set("uids", uIDsAsStrings); + + device.sendPackage(reply); + + return true; + } + + @Override + public boolean onPackageReceived(NetworkPackage np) { + if (np.getType().equals(PACKAGE_TYPE_CONTACTS_REQUEST_ALL_UIDS)) { + return this.handleRequestAllUIDs(np); + } else if (np.getType().equals(PACKAGE_TYPE_CONTACTS_REQUEST_NAMES_BY_UIDS)) { + return this.handleRequestNamesByUIDs(np); + } else if (np.getType().equals(PACKAGE_TYPE_CONTACTS_REQUEST_PHONES_BY_UIDS)) { + return this.handleRequestPhonesByUIDs(np); + } else if (np.getType().equals(PACKAGE_TYPE_CONTACTS_REQUEST_EMAILS_BY_UIDS)) { + return this.handleRequestEmailsByUIDs(np); + } else { + Log.e("ContactsPlugin", "Contacts plugin received an unexpected packet!"); + return false; + } + } +} diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java --- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java +++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java @@ -27,6 +27,7 @@ import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin; import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardPlugin; +import org.kde.kdeconnect.Plugins.ContactsPlugin.ContactsPlugin; import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin; import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin; import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin; @@ -125,6 +126,7 @@ PluginFactory.registerPlugin(TelepathyPlugin.class); PluginFactory.registerPlugin(FindMyPhonePlugin.class); PluginFactory.registerPlugin(RunCommandPlugin.class); + PluginFactory.registerPlugin(ContactsPlugin.class); PluginFactory.registerPlugin(RemoteKeyboardPlugin.class); }