contactInfo = new HashMap<>();
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
- Cursor cursor = null;
+ Cursor cursor;
try {
cursor = context.getContentResolver().query(
uri,
@@ -77,7 +92,7 @@
try {
cursor.close();
- } catch (Exception e) {
+ } catch (Exception ignored) {
}
if (!contactInfo.isEmpty()) {
@@ -102,6 +117,7 @@
input = context.getContentResolver().openInputStream(photoUri);
byte[] buffer = new byte[1024];
int len;
+ //noinspection ConstantConditions
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
@@ -111,16 +127,359 @@
return "";
} finally {
try {
+ //noinspection ConstantConditions
input.close();
} catch (Exception ignored) {
}
try {
+ //noinspection ConstantConditions
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 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 ignored) {
+ }
+ }
+
+ 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
+ */
+ @SuppressWarnings("ALL") // Since this method is busted anyway
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ @Deprecated
+ protected static Map getVCardsFast(Context context, Collection IDs, Map lookupKeys) {
+ LongSparseArray toReturn = new LongSparseArray<>();
+ 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 (IOException e) {
+ // If you are experiencing this, please open a bug report indicating how you got here
+ 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 Mapping of uIDs to the corresponding VCard
+ */
+ @SuppressWarnings("UnnecessaryContinue")
+ 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);
+
+ if (input == null)
+ {
+ throw new NullPointerException("ContentResolver did not give us a stream for the VCard for uID " + ID);
+ }
+
+ 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()));
+ input.close();
+ } catch (IOException e) {
+ // If you are experiencing this, please open a bug report indicating how you got here
+ e.printStackTrace();
+ continue;
+ } catch (NullPointerException e)
+ {
+ // If you are experiencing this, please open a bug report indicating how you got here
+ e.printStackTrace();
+ }
+ }
+
+ 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) {
+ 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
+ );
+
+ if (contactsCursor != null && contactsCursor.moveToFirst()) {
+ do {
+ Map requestedData = new HashMap<>();
+
+ 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 ignored) {
+ }
+ }
+
+ 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 = ":";
+
+ final 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
+ */
+ final 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/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,258 @@
+/*
+ * 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 The same VCard as was passed in, but now with KDE Connect-specific fields
+ */
+ 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",
+ 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
+ */
+ @SuppressWarnings("SameReturnValue")
+ protected boolean handleRequestAllUIDsTimestamps(@SuppressWarnings("unused") 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) {
+ switch (np.getType()) {
+ case PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS:
+ return this.handleRequestAllUIDsTimestamps(np);
+ case PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS:
+ return this.handleRequestVCardsByUIDs(np);
+ default:
+ 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;
@@ -128,6 +129,7 @@
PluginFactory.registerPlugin(TelepathyPlugin.class);
PluginFactory.registerPlugin(FindMyPhonePlugin.class);
PluginFactory.registerPlugin(RunCommandPlugin.class);
+ PluginFactory.registerPlugin(ContactsPlugin.class);
PluginFactory.registerPlugin(RemoteKeyboardPlugin.class);
//PluginFactory.registerPlugin(MprisReceiverPlugin.class);
}