File indexing completed on 2025-02-02 04:47:49

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  * SPDX-FileCopyrightText: 2018 Simon Redman <simon@ergotech.com>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 package org.kde.kdeconnect.Helpers;
0009 
0010 import android.content.Context;
0011 import android.database.Cursor;
0012 import android.net.Uri;
0013 import android.provider.ContactsContract;
0014 import android.provider.ContactsContract.PhoneLookup;
0015 import android.util.Base64;
0016 import android.util.Base64OutputStream;
0017 import android.util.Log;
0018 
0019 import androidx.annotation.NonNull;
0020 import androidx.annotation.Nullable;
0021 
0022 import org.apache.commons.io.IOUtils;
0023 import org.apache.commons.lang3.StringUtils;
0024 
0025 import java.io.ByteArrayOutputStream;
0026 import java.io.IOException;
0027 import java.io.InputStream;
0028 import java.util.ArrayList;
0029 import java.util.Collection;
0030 import java.util.HashMap;
0031 import java.util.List;
0032 import java.util.Map;
0033 
0034 import kotlin.text.Charsets;
0035 
0036 public class ContactsHelper {
0037 
0038     static final String LOG_TAG = "ContactsHelper";
0039 
0040     /**
0041      * Lookup the name and photoID of a contact given a phone number
0042      */
0043     public static Map<String, String> phoneNumberLookup(Context context, String number) {
0044 
0045         Map<String, String> contactInfo = new HashMap<>();
0046 
0047         Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
0048         String[] columns = new String[]{
0049                 PhoneLookup.DISPLAY_NAME,
0050                 PhoneLookup.PHOTO_URI
0051                 /*, PhoneLookup.TYPE
0052                   , PhoneLookup.LABEL
0053                   , PhoneLookup.ID */
0054         };
0055         try (Cursor cursor = context.getContentResolver().query(uri, columns,null, null, null)) {
0056             // Take the first match only
0057             if (cursor != null && cursor.moveToFirst()) {
0058                 int nameIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
0059                 if (nameIndex != -1) {
0060                     contactInfo.put("name", cursor.getString(nameIndex));
0061                 }
0062 
0063                 nameIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
0064                 if (nameIndex != -1) {
0065                     contactInfo.put("photoID", cursor.getString(nameIndex));
0066                 }
0067             }
0068         } catch (Exception ignored) {
0069         }
0070         return contactInfo;
0071     }
0072 
0073     public static String photoId64Encoded(Context context, String photoId) {
0074         if (photoId == null) {
0075             return "";
0076         }
0077         Uri photoUri = Uri.parse(photoId);
0078 
0079         ByteArrayOutputStream encodedPhoto = new ByteArrayOutputStream();
0080         try (InputStream input = context.getContentResolver().openInputStream(photoUri);
0081              Base64OutputStream output = new Base64OutputStream(encodedPhoto, Base64.DEFAULT)) {
0082             IOUtils.copy(input, output, 1024);
0083             return encodedPhoto.toString();
0084         } catch (Exception ex) {
0085             Log.e(LOG_TAG, ex.toString());
0086             return "";
0087         }
0088     }
0089 
0090     /**
0091      * Return all the NAME_RAW_CONTACT_IDS which contribute an entry to a Contact in the database
0092      * <p>
0093      * If the user has, for example, joined several contacts, on the phone, the IDs returned will
0094      * be representative of the joined contact
0095      * <p>
0096      * See here: https://developer.android.com/reference/android/provider/ContactsContract.Contacts.html
0097      * for more information about the connection between contacts and raw contacts
0098      *
0099      * @param context android.content.Context running the request
0100      * @return List of each NAME_RAW_CONTACT_ID in the Contacts database
0101      */
0102     public static List<uID> getAllContactContactIDs(Context context) {
0103         ArrayList<uID> toReturn = new ArrayList<>();
0104 
0105         // Define the columns we want to read from the Contacts database
0106         final String[] columns = new String[]{
0107                 ContactsContract.Contacts.LOOKUP_KEY
0108         };
0109 
0110         Uri contactsUri = ContactsContract.Contacts.CONTENT_URI;
0111         try (Cursor contactsCursor = context.getContentResolver().query(contactsUri, columns, null, null, null)) {
0112             if (contactsCursor != null && contactsCursor.moveToFirst()) {
0113                 do {
0114                     uID contactID;
0115 
0116                     int idIndex = contactsCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
0117                     if (idIndex != -1) {
0118                         contactID = new uID(contactsCursor.getString(idIndex));
0119                     } else {
0120                         // Something went wrong with this contact
0121                         // If you are experiencing this, please open a bug report indicating how you got here
0122                         Log.e(LOG_TAG, "Got a contact which does not have a LOOKUP_KEY");
0123                         continue;
0124                     }
0125 
0126                     if (!toReturn.contains(contactID)) {
0127                         toReturn.add(contactID);
0128                     }
0129                 } while (contactsCursor.moveToNext());
0130             }
0131         }
0132 
0133         return toReturn;
0134     }
0135 
0136     /**
0137      * Get VCards using serial database lookups. This is tragically slow, so call only when needed.
0138      *
0139      * There is a faster API specified using ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI,
0140      * but there does not seem to be a way to figure out which ID resulted in which VCard using that API
0141      *
0142      * @param context    android.content.Context running the request
0143      * @param IDs        collection of uIDs to look up
0144      * @return Mapping of uIDs to the corresponding VCard
0145      */
0146     private static Map<uID, VCardBuilder> getVCardsSlow(Context context, Collection<uID> IDs) {
0147         Map<uID, VCardBuilder> toReturn = new HashMap<>();
0148 
0149         for (uID ID : IDs) {
0150             String lookupKey = ID.toString();
0151             Uri vcardURI = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
0152 
0153             try (InputStream input = context.getContentResolver().openInputStream(vcardURI)) {
0154                 if (input == null) {
0155                     throw new NullPointerException("ContentResolver did not give us a stream for the VCard for uID " + ID);
0156                 }
0157                 final List<String> lines = IOUtils.readLines(input, Charsets.UTF_8);
0158                 toReturn.put(ID, new VCardBuilder(StringUtils.join(lines, '\n')));
0159             } catch (IOException | NullPointerException e) {
0160                 // If you are experiencing this, please open a bug report indicating how you got here
0161                 Log.e("Contacts", "Exception while fetching vcards", e);
0162             }
0163         }
0164 
0165         return toReturn;
0166     }
0167 
0168     /**
0169      * Get the VCard for every specified raw contact ID
0170      *
0171      * @param context android.content.Context running the request
0172      * @param IDs     collection of raw contact IDs to look up
0173      * @return Mapping of raw contact IDs to the corresponding VCard
0174      */
0175     public static Map<uID, VCardBuilder> getVCardsForContactIDs(Context context, Collection<uID> IDs) {
0176         return getVCardsSlow(context, IDs);
0177     }
0178 
0179     /**
0180      * Get the last-modified timestamp for every contact in the database
0181      *
0182      * @param context android.content.Context running the request
0183      * @return Mapping of contact uID to last-modified timestamp
0184      */
0185     public static Map<uID, Long> getAllContactTimestamps(Context context) {
0186         String[] projection = { uID.COLUMN, ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
0187 
0188         Map<uID, Map<String, String>> databaseValues = accessContactsDatabase(context, projection, null, null, null);
0189 
0190         Map<uID, Long> timestamps = new HashMap<>();
0191         for (uID contactID : databaseValues.keySet()) {
0192             Map<String, String> data = databaseValues.get(contactID);
0193             timestamps.put(
0194               contactID,
0195               Long.parseLong(data.get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP))
0196             );
0197         }
0198 
0199         return timestamps;
0200     }
0201 
0202     /**
0203      * Get the last-modified timestamp for the specified contact
0204      *
0205      * @param context   android.content.Context running the request
0206      * @param contactID Contact uID to read
0207      * @throws ContactNotFoundException If the given ID for some reason does not match a contact
0208      * @return          Last-modified timestamp of the contact
0209      */
0210     public static Long getContactTimestamp(Context context, uID contactID) throws ContactNotFoundException {
0211         String[] projection = { uID.COLUMN, ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
0212         String selection = uID.COLUMN + " = ?";
0213         String[] selectionArgs = { contactID.toString() };
0214 
0215         Map<uID, Map<String, String>> databaseValue = accessContactsDatabase(context, projection, selection, selectionArgs, null);
0216 
0217         if (databaseValue.size() == 0) {
0218             throw new ContactNotFoundException("Querying for contact with id " + contactID + " returned no results.");
0219         }
0220 
0221         if (databaseValue.size() != 1) {
0222             Log.w(LOG_TAG, "Received an improper number of return values from the database in getContactTimestamp: " + databaseValue.size());
0223         }
0224 
0225         Long timestamp = Long.parseLong(databaseValue.get(contactID).get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP));
0226 
0227         return timestamp;
0228     }
0229 
0230     /**
0231      * Return a mapping of contact IDs to a map of the requested data from the Contacts database.
0232      *
0233      * @param context    android.content.Context running the request
0234      * @param projection List of column names to extract, defined in ContactsContract.Contacts. Must contain uID.COLUMN
0235      * @param selection  Parameterizable filter to use with the ContentResolver query. May be null.
0236      * @param selectionArgs Parameters for selection. May be null.
0237      * @param sortOrder  Sort order to request from the ContentResolver query. May be null.
0238      * @return mapping of contact uIDs to desired values, which are a mapping of column names to the data contained there
0239      */
0240     private static Map<uID, Map<String, String>> accessContactsDatabase(
0241             @NonNull Context context,
0242             @NonNull String[] projection,
0243             @Nullable String   selection,
0244             @Nullable String[] selectionArgs,
0245             @Nullable String   sortOrder
0246             ) {
0247         Uri contactsUri = ContactsContract.Contacts.CONTENT_URI;
0248 
0249         HashMap<uID, Map<String, String>> toReturn = new HashMap<>();
0250 
0251         try (Cursor contactsCursor = context.getContentResolver().query(
0252                 contactsUri,
0253                 projection,
0254                 selection,
0255                 selectionArgs,
0256                 sortOrder
0257         )) {
0258             if (contactsCursor != null && contactsCursor.moveToFirst()) {
0259                 do {
0260                     Map<String, String> requestedData = new HashMap<>();
0261 
0262                     int uIDIndex = contactsCursor.getColumnIndexOrThrow(uID.COLUMN);
0263                     uID uID = new uID(contactsCursor.getString(uIDIndex));
0264 
0265                     // For each column, collect the data from that column
0266                     for (String column : projection) {
0267                         int index = contactsCursor.getColumnIndex(column);
0268                         // Since we might be getting various kinds of data, Object is the best we can do
0269                         String data;
0270                         if (index == -1) {
0271                             // This contact didn't have the requested column? Something is very wrong.
0272                             // If you are experiencing this, please open a bug report indicating how you got here
0273                             Log.e(LOG_TAG, "Got a contact which does not have a requested column");
0274                             continue;
0275                         }
0276                         data = contactsCursor.getString(index);
0277 
0278                         requestedData.put(column, data);
0279                     }
0280 
0281                     toReturn.put(uID, requestedData);
0282                 } while (contactsCursor.moveToNext());
0283             }
0284         }
0285         return toReturn;
0286     }
0287 
0288     /**
0289      * This is a cheap ripoff of com.android.vcard.VCardBuilder
0290      * <p>
0291      * Maybe in the future that library will be made public and we can switch to using that!
0292      * <p>
0293      * The main similarity is the usage of .toString() to produce the finalized VCard and the
0294      * usage of .appendLine(String, String) to add stuff to the vcard
0295      */
0296     public static class VCardBuilder {
0297         static final String VCARD_END = "END:VCARD"; // Written to terminate the vcard
0298         static final String VCARD_DATA_SEPARATOR = ":";
0299 
0300         final StringBuilder vcardBody;
0301 
0302         /**
0303          * Take a partial vcard as a string and make a VCardBuilder
0304          *
0305          * @param vcard vcard to build upon
0306          */
0307         VCardBuilder(String vcard) {
0308             // Remove the end tag. We will add it back on in .toString()
0309             vcard = vcard.substring(0, vcard.indexOf(VCARD_END));
0310 
0311             vcardBody = new StringBuilder(vcard);
0312         }
0313 
0314         /**
0315          * Appends one line with a given property name and value.
0316          */
0317         public void appendLine(final String propertyName, final String rawValue) {
0318             vcardBody.append(propertyName)
0319                     .append(VCARD_DATA_SEPARATOR)
0320                     .append(rawValue)
0321                     .append("\n");
0322         }
0323 
0324         @NonNull
0325         public String toString() {
0326             return vcardBody.toString() + VCARD_END;
0327         }
0328     }
0329 
0330     /**
0331      * Essentially a typedef of the type used for a unique identifier
0332      */
0333     public static class uID {
0334         /**
0335          * We use the LOOKUP_KEY column of the Contacts table as a unique ID, since that's what it's
0336          * for
0337          */
0338         final String contactLookupKey;
0339 
0340         /**
0341          * Which Contacts column this uID is pulled from
0342          */
0343         static final String COLUMN = ContactsContract.Contacts.LOOKUP_KEY;
0344 
0345         public uID(String lookupKey) {
0346 
0347             if (lookupKey == null)
0348                 throw new IllegalArgumentException("lookUpKey should not be null");
0349 
0350             contactLookupKey = lookupKey;
0351         }
0352 
0353         @NonNull
0354         public String toString() {
0355             return this.contactLookupKey;
0356         }
0357 
0358         @Override
0359         public int hashCode() {
0360             return contactLookupKey.hashCode();
0361         }
0362 
0363         @Override
0364         public boolean equals(Object other) {
0365             if (other instanceof uID) {
0366                 return contactLookupKey.equals(((uID) other).contactLookupKey);
0367             }
0368             return contactLookupKey.equals(other);
0369         }
0370     }
0371 
0372     /**
0373      * Exception to indicate that a specified contact was not found
0374      */
0375     public static class ContactNotFoundException extends Exception {
0376         public ContactNotFoundException(uID contactID) {
0377             super("Unable to find contact with ID " + contactID);
0378         }
0379 
0380         public ContactNotFoundException(String message) {
0381             super(message);
0382         }
0383     }
0384 }