2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-28 20:57:42 +00:00

Refactor contacts-getting code to be either "everything" or "one"

Fixes bug from mailing list conversation dated 12 October 2019
This commit is contained in:
Simon Redman 2019-10-27 21:23:52 +00:00
parent 1d5c280401
commit 6f81c67632
3 changed files with 128 additions and 102 deletions

View File

@ -33,8 +33,8 @@ import android.util.Base64OutputStream;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.LongSparseArray;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
@ -52,6 +52,7 @@ import java.util.Set;
public class ContactsHelper {
static final String LOG_TAG = "ContactsHelper";
/**
* Lookup the name and photoID of a contact given a phone number
@ -103,7 +104,7 @@ public class ContactsHelper {
}
return encodedPhoto.toString();
} catch (Exception ex) {
Log.e("ContactsHelper", ex.toString());
Log.e(LOG_TAG, ex.toString());
return "";
}
}
@ -140,7 +141,7 @@ public class ContactsHelper {
} 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");
Log.e(LOG_TAG, "Got a contact which does not have a LOOKUP_KEY");
continue;
}
@ -211,97 +212,110 @@ public class ContactsHelper {
}
/**
* Return a mapping of contact IDs to a map of the requested data from the Contacts database
* <p>
* 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
* Get the last-modified timestamp for every contact in the database
*
* @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 uID to last-modified timestamp
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) // Need API 18 for contact timestamps
public static Map<uID, Long> getAllContactTimestamps(Context context) {
String[] projection = { uID.COLUMN, ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
Map<uID, Map<String, String>> databaseValues = accessContactsDatabase(context, projection, null, null, null);
Map<uID, Long> timestamps = new HashMap<>();
for (uID contactID : databaseValues.keySet()) {
Map<String, String> data = databaseValues.get(contactID);
timestamps.put(
contactID,
Long.parseLong(data.get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP))
);
}
return timestamps;
}
/**
* Get the last-modified timestamp for the specified contact
*
* @param context android.content.Context running the request
* @param contactID Contact uID to read
* @throws ContactNotFoundException If the given ID for some reason does not match a contact
* @return Last-modified timestamp of the contact
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) // Need API 18 for contact timestamps
public static Long getContactTimestamp(Context context, uID contactID) throws ContactNotFoundException {
String[] projection = { uID.COLUMN, ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
String selection = uID.COLUMN + " = ?";
String[] selectionArgs = { contactID.toString() };
Map<uID, Map<String, String>> databaseValue = accessContactsDatabase(context, projection, selection, selectionArgs, null);
if (databaseValue.size() == 0) {
throw new ContactNotFoundException("Querying for contact with id " + contactID + " returned no results.");
}
if (databaseValue.size() != 1) {
Log.w(LOG_TAG, "Received an improper number of return values from the database in getContactTimestamp: " + databaseValue.size());
}
Long timestamp = Long.parseLong(databaseValue.get(contactID).get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP));
return timestamp;
}
/**
* Return a mapping of contact IDs to a map of the requested data from the Contacts database.
*
* @param context android.content.Context running the request
* @param projection List of column names to extract, defined in ContactsContract.Contacts. Must contain uID.COLUMN
* @param selection Parameterizable filter to use with the ContentResolver query. May be null.
* @param selectionArgs Parameters for selection. May be null.
* @param sortOrder Sort order to request from the ContentResolver query. May be null.
* @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<uID, Map<String, Object>> getColumnsFromContactsForIDs(Context context, Collection<uID> IDs, String[] contactsProjection) {
HashMap<uID, Map<String, Object>> toReturn = new HashMap<>();
if (IDs.isEmpty()) {
return toReturn;
}
private static Map<uID, Map<String, String>> accessContactsDatabase(
@NonNull Context context,
@NonNull String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder
) {
Uri contactsUri = ContactsContract.Contacts.CONTENT_URI;
// Regardless of whether it was requested, we need to look up the uID column
Set<String> lookupProjection = new HashSet<>(Arrays.asList(contactsProjection));
lookupProjection.add(uID.COLUMN);
// We need a selection which looks like "<column> 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<String> contactsArgs = new ArrayList<>();
for (uID ID : IDs) {
contactsArgs.add(ID.toString());
}
HashMap<uID, Map<String, String>> toReturn = new HashMap<>();
try (Cursor contactsCursor = context.getContentResolver().query(
contactsUri,
lookupProjection.toArray(new String[0]),
contactsSelection.toString(),
contactsArgs.toArray(new String[0]),
null
projection,
selection,
selectionArgs,
sortOrder
)) {
if (contactsCursor != null && contactsCursor.moveToFirst()) {
do {
Map<String, Object> requestedData = new HashMap<>();
Map<String, String> requestedData = new HashMap<>();
int lookupKeyIdx = contactsCursor.getColumnIndexOrThrow(uID.COLUMN);
String lookupKey = contactsCursor.getString(lookupKeyIdx);
int uIDIndex = contactsCursor.getColumnIndexOrThrow(uID.COLUMN);
uID uID = new uID(contactsCursor.getString(uIDIndex));
// For each column, collect the data from that column
for (String column : contactsProjection) {
for (String column : projection) {
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;
String data;
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");
Log.e(LOG_TAG, "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);
toReturn.put(uID, requestedData);
} while (contactsCursor.moveToNext());
}
}
@ -391,4 +405,17 @@ public class ContactsHelper {
return contactLookupKey.equals(other);
}
}
/**
* Exception to indicate that a specified contact was not found
*/
public static class ContactNotFoundException extends Exception {
public ContactNotFoundException(uID contactID) {
super("Unable to find contact with ID " + contactID);
}
public ContactNotFoundException(String message) {
super(message);
}
}
}

View File

@ -108,6 +108,13 @@ public class NetworkPacket {
return mBody.optInt(key, defaultValue);
}
public void set(String key, int value) {
try {
mBody.put(key, value);
} catch (Exception ignored) {
}
}
public long getLong(String key) {
return mBody.optLong(key, -1);
}
@ -116,7 +123,7 @@ public class NetworkPacket {
return mBody.optLong(key, defaultValue);
}
public void set(String key, int value) {
public void set(String key, long value) {
try {
mBody.put(key, value);
} catch (Exception ignored) {

View File

@ -26,12 +26,12 @@ 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.Helpers.ContactsHelper.ContactNotFoundException;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
@ -141,9 +141,10 @@ public class ContactsPlugin extends Plugin {
*
* @param vcard vcard to apply metadata to
* @param uID uID to which the vcard corresponds
* @throws ContactNotFoundException If the given ID for some reason does not match a contact
* @return The same VCard as was passed in, but now with KDE Connect-specific fields
*/
private VCardBuilder addVCardMetadata(VCardBuilder vcard, uID uID) {
private VCardBuilder addVCardMetadata(VCardBuilder vcard, uID uID) throws ContactNotFoundException {
// 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(),
@ -151,16 +152,9 @@ public class ContactsPlugin extends Plugin {
// Build the timestamp line
// Maybe one day this should be changed into the vcard-standard REV key
List<uID> uIDs = new ArrayList<>();
uIDs.add(uID);
final String[] contactsProjection = new String[]{
ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
};
Map<uID, Map<String, Object>> timestamp = ContactsHelper.getColumnsFromContactsForIDs(context, uIDs, contactsProjection);
Long timestamp = ContactsHelper.getContactTimestamp(context, uID);
vcard.appendLine("X-KDECONNECT-TIMESTAMP",
timestamp.get(uID).get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP).toString());
timestamp.toString());
return vcard;
}
@ -178,26 +172,19 @@ public class ContactsPlugin extends Plugin {
private boolean handleRequestAllUIDsTimestamps(@SuppressWarnings("unused") NetworkPacket np) {
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS);
List<uID> uIDs = ContactsHelper.getAllContactContactIDs(context);
Map<uID, Long> uIDsToTimestamps = ContactsHelper.getAllContactTimestamps(context);
List<String> uIDsAsStrings = new ArrayList<>(uIDs.size());
int contactCount = uIDsToTimestamps.size();
for (uID uID : uIDs) {
uIDsAsStrings.add(uID.toString());
List<String> uIDs = new ArrayList<>(contactCount);
for (uID contactID : uIDsToTimestamps.keySet()) {
Long timestamp = uIDsToTimestamps.get(contactID);
reply.set(contactID.toString(), timestamp);
uIDs.add(contactID.toString());
}
final String[] contactsProjection = new String[]{
ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
};
reply.set("uids", uIDsAsStrings);
// Add last-modified timestamps
Map<uID, Map<String, Object>> uIDsToTimestamps = ContactsHelper.getColumnsFromContactsForIDs(context, uIDs, contactsProjection);
for (uID ID : uIDsToTimestamps.keySet()) {
Map<String, Object> data = uIDsToTimestamps.get(ID);
reply.set(ID.toString(), (Integer) data.get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP));
}
reply.set("uids", uIDs);
device.sendPacket(reply);
@ -230,12 +217,17 @@ public class ContactsPlugin extends Plugin {
for (uID uID : uIDsToVCards.keySet()) {
VCardBuilder vcard = uIDsToVCards.get(uID);
try {
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());
} catch (ContactsHelper.ContactNotFoundException e) {
e.printStackTrace();
}
}
// Add the valid uIDs to the packet