diff --git a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java index 8e421d92..9d164646 100644 --- a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java +++ b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java @@ -1,444 +1,446 @@ /* * 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 * 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.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.provider.ContactsContract.PhoneLookup; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.collection.LongSparseArray; + import java.io.BufferedReader; import java.io.ByteArrayOutputStream; 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; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.collection.LongSparseArray; - public class ContactsHelper { /** * Lookup the name and photoID of a contact given a phone number */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static Map phoneNumberLookup(Context context, String number) { Map contactInfo = new HashMap<>(); Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); String[] columns = new String[]{ PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI /*, PhoneLookup.TYPE , PhoneLookup.LABEL , PhoneLookup.ID */ }; try (Cursor cursor = context.getContentResolver().query(uri, columns,null, null, null)) { // Take the first match only if (cursor != null && cursor.moveToFirst()) { int nameIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); if (nameIndex != -1) { contactInfo.put("name", cursor.getString(nameIndex)); } nameIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); if (nameIndex != -1) { contactInfo.put("photoID", cursor.getString(nameIndex)); } } } catch (Exception ignored) { } return contactInfo; } public static String photoId64Encoded(Context context, String photoId) { if (photoId == null) { return ""; } Uri photoUri = Uri.parse(photoId); ByteArrayOutputStream encodedPhoto = new ByteArrayOutputStream(); try (InputStream input = context.getContentResolver().openInputStream(photoUri); Base64OutputStream output = new Base64OutputStream(encodedPhoto, Base64.DEFAULT)) { byte[] buffer = new byte[1024]; int len; //noinspection ConstantConditions while ((len = input.read(buffer)) != -1) { output.write(buffer, 0, len); } return encodedPhoto.toString(); } catch (Exception ex) { Log.e("ContactsHelper", ex.toString()); return ""; } } /** * 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[] columns = new String[]{ ContactsContract.Contacts.LOOKUP_KEY }; Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; try (Cursor contactsCursor = context.getContentResolver().query(contactsUri, columns, 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); + if (!toReturn.contains(contactID)) { + toReturn.add(contactID); + } } while (contactsCursor.moveToNext()); } } 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())); ; StringBuilder vcardJumble = new StringBuilder(); try (InputStream 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 Log.e("Contacts", "Exception while fetching vcards", e); } // 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 */ private 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); try (InputStream 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())); } catch (IOException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } catch (NullPointerException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } } 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<>(); if (IDs.isEmpty()) { return toReturn; } 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()); } try (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()); } } 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 { static final String VCARD_END = "END:VCARD"; // Written to terminate the vcard 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 */ 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"); } @NonNull 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) { if (lookupKey == null) throw new IllegalArgumentException("lookUpKey should not be null"); contactLookupKey = lookupKey; } @NonNull 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); } } }