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 @@ -222,6 +224,7 @@ 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 Select a ringtone Blocked numbers Don\'t show calls and SMS from these numbers. Please specify one number per line 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 @@ -27,19 +28,33 @@ import android.os.Build; import android.provider.ContactsContract; import android.provider.ContactsContract.PhoneLookup; +import android.support.annotation.RequiresApi; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; 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); @@ -122,5 +137,339 @@ } } -} + /** + * 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 getAllContactContactIDs(Context context) { + ArrayList toReturn = new ArrayList<>(); + + // Define the columns we want to read from the Contacts database + final String[] projection = new String[]{ + ContactsContract.Contacts.LOOKUP_KEY + }; + + Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; + Cursor contactsCursor = context.getContentResolver().query( + contactsUri, + projection, + null, null, null); + if (contactsCursor != null && contactsCursor.moveToFirst()) { + do { + uID contactID; + + int idIndex = contactsCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY); + if (idIndex != -1) { + contactID = new uID(contactsCursor.getString(idIndex)); + } else { + // Something went wrong with this contact + // If you are experiencing this, please open a bug report indicating how you got here + Log.e("ContactsHelper", "Got a contact which does not have a LOOKUP_KEY"); + continue; + } + + toReturn.add(contactID); + } while (contactsCursor.moveToNext()); + try { + contactsCursor.close(); + } catch (Exception e) { + } + } + + return toReturn; + } + + /** + * Get VCards using the batch database query which requires Android API 21 + * + * @param context android.content.Context running the request + * @param IDs collection of raw contact IDs to look up + * @param lookupKeys + * @return Mapping of raw contact IDs to corresponding VCard + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + protected static Map getVCardsFast(Context context, Collection IDs, Map lookupKeys) { + Map toReturn = new HashMap<>(); + StringBuilder keys = new StringBuilder(); + + List orderedIDs = new ArrayList<>(IDs); + + for (Long ID : orderedIDs) { + String key = lookupKeys.get(ID); + keys.append(key); + keys.append(':'); + } + + // Remove trailing ':' + keys.deleteCharAt(keys.length() - 1); + + Uri vcardURI = Uri.withAppendedPath( + ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, + Uri.encode(keys.toString())); + + InputStream input; + StringBuilder vcardJumble = new StringBuilder(); + try { + input = context.getContentResolver().openInputStream(vcardURI); + + BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input)); + String line; + + while ((line = bufferedInput.readLine()) != null) { + vcardJumble.append(line).append('\n'); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + // At this point we are screwed: + // There is no way to figure out, given the lookup we just made, which VCard belonges + // to which ID. They appear to be in the same order as the request was made, but this + // is (provably) unreliable. I am leaving this code in case it is useful, but unless + // Android improves their API there is nothing we can do with it + + return null; + } + + /** + * Get VCards using serial database lookups. This is tragically slow, but the faster method using + * + * There is a faster API specified using ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, + * but there does not seem to be a way to figure out which ID resulted in which VCard using that API + * + * @param context android.content.Context running the request + * @param IDs collection of uIDs to look up + * @return + */ + protected static Map getVCardsSlow(Context context, Collection IDs) { + Map toReturn = new HashMap<>(); + + for (uID ID : IDs) { + String lookupKey = ID.toString(); + Uri vcardURI = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); + InputStream input; + try { + input = context.getContentResolver().openInputStream(vcardURI); + + BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input)); + + StringBuilder vcard = new StringBuilder(); + String line; + while ((line = bufferedInput.readLine()) != null) { + vcard.append(line).append('\n'); + } + + toReturn.put(ID, new VCardBuilder(vcard.toString())); + } catch (FileNotFoundException e) { + // If you are experiencing this, please open a bug report indicating how you got here + e.printStackTrace(); + continue; + } catch (IOException e) { + e.printStackTrace(); + continue; + } + } + + return toReturn; + } + + /** + * Get the VCard for every specified raw contact ID + * + * @param context android.content.Context running the request + * @param IDs collection of raw contact IDs to look up + * @return Mapping of raw contact IDs to the corresponding VCard + */ + public static Map getVCardsForContactIDs(Context context, Collection IDs) { + Map toReturn = new HashMap<>(); + + return getVCardsSlow(context, IDs); + } + + /** + * Return a mapping of contact IDs to a map of the requested data from the Contacts database + *

+ * If for some reason there is no row associated with the 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 contact uIDs to look up + * @param contactsProjection List of column names to extract, defined in ContactsContract.Contacts + * @return mapping of contact uIDs 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> getColumnsFromContactsForIDs(Context context, Collection IDs, String[] contactsProjection) { + HashMap> toReturn = new HashMap<>(); + + Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; + + // Regardless of whether it was requested, we need to look up the uID column + Set lookupProjection = new HashSet<>(Arrays.asList(contactsProjection)); + lookupProjection.add(uID.COLUMN); + + // We need a selection which looks like " IN(?,?,...?)" with one ? per ID + StringBuilder contactsSelection = new StringBuilder(uID.COLUMN); + contactsSelection.append(" IN("); + + for (int i = 0; i < IDs.size(); i++) { + contactsSelection.append("?,"); + } + // Remove trailing comma + contactsSelection.deleteCharAt(contactsSelection.length() - 1); + contactsSelection.append(")"); + + // We need selection arguments as simply a String representation of each ID + List contactsArgs = new ArrayList<>(); + for (uID ID : IDs) { + contactsArgs.add(ID.toString()); + } + + Cursor contactsCursor = context.getContentResolver().query( + contactsUri, + lookupProjection.toArray(new String[0]), + contactsSelection.toString(), + contactsArgs.toArray(new String[0]), null + ); + + Map requestedData = new HashMap<>(); + + if (contactsCursor != null && contactsCursor.moveToFirst()) { + do { + int lookupKeyIdx = contactsCursor.getColumnIndexOrThrow(uID.COLUMN); + String lookupKey = contactsCursor.getString(lookupKeyIdx); + + // 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 contact didn't have the requested column? Something is very wrong. + // If you are experiencing this, please open a bug report indicating how you got here + Log.e("ContactsHelper", "Got a contact which does not have a requested column"); + 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); + } + + toReturn.put(new uID(lookupKey), requestedData); + } while (contactsCursor.moveToNext()); + try { + contactsCursor.close(); + } catch (Exception e) { + } + } + + return toReturn; + } + + /** + * This is a cheap ripoff of com.android.vcard.VCardBuilder + *

+ * Maybe in the future that library will be made public and we can switch to using that! + *

+ * The main similarity is the usage of .toString() to produce the finalized VCard and the + * usage of .appendLine(String, String) to add stuff to the vcard + */ + public static class VCardBuilder { + protected static final String VCARD_END = "END:VCARD"; // Written to terminate the vcard + protected static final String VCARD_DATA_SEPARATOR = ":"; + + StringBuilder vcardBody; + + /** + * Take a partial vcard as a string and make a VCardBuilder + * + * @param vcard vcard to build upon + */ + public VCardBuilder(String vcard) { + // Remove the end tag. We will add it back on in .toString() + vcard = vcard.substring(0, vcard.indexOf(VCARD_END)); + + vcardBody = new StringBuilder(vcard); + } + + /** + * Appends one line with a given property name and value. + */ + public void appendLine(final String propertyName, final String rawValue) { + vcardBody.append(propertyName) + .append(VCARD_DATA_SEPARATOR) + .append(rawValue) + .append("\n"); + } + + public String toString() { + return vcardBody.toString() + VCARD_END; + } + } + + /** + * Essentially a typedef of the type used for a unique identifier + */ + public static class uID { + /** + * We use the LOOKUP_KEY column of the Contacts table as a unique ID, since that's what it's + * for + */ + String contactLookupKey; + + /** + * Which Contacts column this uID is pulled from + */ + static final String COLUMN = ContactsContract.Contacts.LOOKUP_KEY; + + public uID(String lookupKey) { + contactLookupKey = lookupKey; + } + + public String toString() { + return this.contactLookupKey; + } + + @Override + public int hashCode() { + return contactLookupKey.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof uID) { + return contactLookupKey.equals(((uID) other).contactLookupKey); + } + return contactLookupKey.equals(other); + } + } +} diff --git a/src/org/kde/kdeconnect/Helpers/SMSHelper.java b/src/org/kde/kdeconnect/Helpers/SMSHelper.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/Helpers/SMSHelper.java @@ -0,0 +1,123 @@ +/* + * 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.Helpers; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.Telephony; +import android.support.annotation.RequiresApi; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SMSHelper { + + /** + * Get the base address for the SMS content + * + * If we want to support API < 19, it seems to be possible to read via this query + * This is highly undocumented and very likely varies between vendors but appears to work + */ + protected static Uri getSMSURIBad() { + return Uri.parse("content://sms/"); + } + + /** + * Get the base address for the SMS content + * + * Use the new API way which should work on any phone API >= 19 + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + protected static Uri getSMSURIGood() { + return Telephony.Sms.CONTENT_URI; + } + + protected static Uri getSMSUri() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return getSMSURIGood(); + } else + { + return getSMSURIBad(); + } + } + + /** + * Get all the SMS threads on the phone as well as a bunch of useful-looking data + * + * Return a map keyed by Android's Thread ID to a list of all the messages in that thread + * Each message is represented by a map containing the keys which seemed most useful and interesting + * + * @param context android.content.Context running the request + * @return Mapping of thread ID to list of messages in that thread + */ + public static Map>> getSMS(Context context) { + HashMap>> toReturn = new HashMap<>(); + + Uri smsUri = getSMSUri(); + + final String[] smsProjection = new String[]{ + Telephony.Sms.ADDRESS, // Phone number of the remote + Telephony.Sms.BODY, // Body of the message + Telephony.Sms.DATE, // Some date associated with the message (Received?) + Telephony.Sms.TYPE, // Compare with Telephony.TextBasedSmsColumns.MESSAGE_TYPE_* + Telephony.Sms.PERSON, // Some obscure value that corresponds to the contact + Telephony.Sms.READ, // Whether we have received a read report for this message (int) + Telephony.Sms.THREAD_ID, // Magic number which binds (message) threads + }; + + Cursor smsCursor = context.getContentResolver().query( + smsUri, + smsProjection, + null, + null, + null); + + if (smsCursor.moveToFirst()) { + int addressColumn = smsCursor.getColumnIndexOrThrow(Telephony.Sms.THREAD_ID); + do { + String thread = smsCursor.getString(addressColumn); + if (! toReturn.containsKey(thread)) + { + toReturn.put(thread, new ArrayList>()); + } + Map messageInfo = new HashMap<>(); + for (int columnIdx = 0; columnIdx < smsCursor.getColumnCount(); columnIdx++) { + String colName = smsCursor.getColumnName(columnIdx); + String body = smsCursor.getString(columnIdx); + messageInfo.put(colName, body); + } + toReturn.get(thread).add(messageInfo); + } while (smsCursor.moveToNext()); + } else + { + // No SMSes available? + } + + 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,256 @@ +/* + * 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.annotation.TargetApi; +import android.os.Build; +import android.provider.ContactsContract; +import android.util.Log; + +import org.kde.kdeconnect.Helpers.ContactsHelper; +import org.kde.kdeconnect.Helpers.ContactsHelper.VCardBuilder; +import org.kde.kdeconnect.Helpers.ContactsHelper.uID; +import org.kde.kdeconnect.NetworkPacket; +import org.kde.kdeconnect.Plugins.Plugin; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +public class ContactsPlugin extends Plugin { + + /** + * Used to request the device send the unique ID of every contact + */ + public static final String PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS = "kdeconnect.contacts.request_all_uids_timestamps"; + + /** + * 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 PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS = "kdeconnect.contacts.request_vcards_by_uid"; + + /** + * Response indicating the packet 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 PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS = "kdeconnect.contacts.response_uids_timestamps"; + + /** + * Response indicating the packet 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 PACKET_TYPE_CONTACTS_RESPONSE_VCARDS = "kdeconnect.contacts.response_vcards"; + + @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[] getSupportedPacketTypes() { + return new String[]{ + PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS, + PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS + }; + } + + @Override + public String[] getOutgoingPacketTypes() { + return new String[]{ + PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS, + PACKET_TYPE_CONTACTS_RESPONSE_VCARDS + }; + } + + @Override + public boolean onCreate() { + permissionExplanation = R.string.contacts_permission_explanation; + + return true; + } + + @Override + public boolean isEnabledByDefault() { + return true; + } + + @Override + public String[] getRequiredPermissions() { + return new String[]{Manifest.permission.READ_CONTACTS}; + // One day maybe we will also support WRITE_CONTACTS, but not yet + } + + @Override + public int getMinSdk() { + // Need API 18 for contact timestamps + return Build.VERSION_CODES.JELLY_BEAN_MR2; + } + + /** + * Add custom fields to the vcard to keep track of KDE Connect-specific fields + *

+ * These include the local device's uID as well as last-changed timestamp + *

+ * This might be extended in the future to include more fields + * + * @param vcard vcard to apply metadata to + * @param uID uID to which the vcard corresponds + * @return + */ + protected VCardBuilder addVCardMetadata(VCardBuilder vcard, uID uID) { + // Append the device ID line + // Unclear if the deviceID forms a valid name per the vcard spec. Worry about that later.. + vcard.appendLine("X-KDECONNECT-ID-DEV-" + device.getDeviceId(), + uID.toString()); + + // Build the timestamp line + // Maybe one day this should be changed into the vcard-standard REV key + List uIDs = new ArrayList<>(); + uIDs.add(uID); + + final String[] contactsProjection = new String[]{ + ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + }; + + Map> timestamp = ContactsHelper.getColumnsFromContactsForIDs(context, uIDs, contactsProjection); + vcard.appendLine("X-KDECONNECT-TIMESTAMP", + ((Integer) timestamp.get(uID).get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)).toString()); + + return vcard; + } + + /** + * Return a unique identifier (Contacts.LOOKUP_KEY) 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 handleRequestAllUIDsTimestamps(NetworkPacket np) { + NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS); + + List uIDs = ContactsHelper.getAllContactContactIDs(context); + + List uIDsAsStrings = new ArrayList(uIDs.size()); + + for (uID uID : uIDs) { + uIDsAsStrings.add(uID.toString()); + } + + final String[] contactsProjection = new String[]{ + ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + }; + + reply.set("uids", uIDsAsStrings); + + // Add last-modified timestamps + Map> uIDsToTimestamps = ContactsHelper.getColumnsFromContactsForIDs(context, uIDs, contactsProjection); + for (uID ID : uIDsToTimestamps.keySet()) { + Map data = uIDsToTimestamps.get(ID); + reply.set(ID.toString(), (Integer) data.get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)); + } + + device.sendPacket(reply); + + return true; + } + + protected boolean handleRequestVCardsByUIDs(NetworkPacket 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 Collection to call getVCardsForContactIDs + Set uIDs = new HashSet<>(uIDsAsStrings.size()); + for (String uID : uIDsAsStrings) { + uIDs.add(new uID(uID)); + } + + Map uIDsToVCards = ContactsHelper.getVCardsForContactIDs(context, uIDs); + + // ContactsHelper.getVCardsForContactIDs(..) is allowed to reply without + // some of the requested uIDs if they were not in the database, so update our list + uIDsAsStrings = new ArrayList(uIDsToVCards.size()); + + NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_VCARDS); + + // Add the vcards to the packet + for (uID uID : uIDsToVCards.keySet()) { + VCardBuilder vcard = uIDsToVCards.get(uID); + + vcard = this.addVCardMetadata(vcard, uID); + + // Store this as a valid uID + uIDsAsStrings.add(uID.toString()); + // Add the uid -> vcard pairing to the packet + reply.set(uID.toString(), vcard.toString()); + } + + // Add the valid uIDs to the packet + reply.set("uids", uIDsAsStrings); + + device.sendPacket(reply); + + return true; + } + + @Override + public boolean onPacketReceived(NetworkPacket np) { + if (np.getType().equals(PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS)) { + return this.handleRequestAllUIDsTimestamps(np); + } else if (np.getType().equals(PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS)) { + return this.handleRequestVCardsByUIDs(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; @@ -127,6 +128,7 @@ PluginFactory.registerPlugin(TelepathyPlugin.class); PluginFactory.registerPlugin(FindMyPhonePlugin.class); PluginFactory.registerPlugin(RunCommandPlugin.class); + PluginFactory.registerPlugin(ContactsPlugin.class); PluginFactory.registerPlugin(RemoteKeyboardPlugin.class); }