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 }