File indexing completed on 2024-12-22 04:41:39
0001 /* 0002 * ContactsPlugin.java - This file is part of KDE Connect's Android App 0003 * Implement a way to request and send contact information 0004 * 0005 * SPDX-FileCopyrightText: 2018 Simon Redman <simon@ergotech.com> 0006 * 0007 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0008 */ 0009 0010 package org.kde.kdeconnect.Plugins.ContactsPlugin; 0011 0012 import android.Manifest; 0013 import android.util.Log; 0014 0015 import androidx.annotation.NonNull; 0016 import androidx.fragment.app.DialogFragment; 0017 0018 import org.kde.kdeconnect.Helpers.ContactsHelper; 0019 import org.kde.kdeconnect.Helpers.ContactsHelper.ContactNotFoundException; 0020 import org.kde.kdeconnect.Helpers.ContactsHelper.VCardBuilder; 0021 import org.kde.kdeconnect.Helpers.ContactsHelper.uID; 0022 import org.kde.kdeconnect.NetworkPacket; 0023 import org.kde.kdeconnect.Plugins.Plugin; 0024 import org.kde.kdeconnect.Plugins.PluginFactory; 0025 import org.kde.kdeconnect.UserInterface.AlertDialogFragment; 0026 import org.kde.kdeconnect_tp.R; 0027 0028 import java.util.ArrayList; 0029 import java.util.HashSet; 0030 import java.util.List; 0031 import java.util.Map; 0032 import java.util.Set; 0033 0034 @PluginFactory.LoadablePlugin 0035 public class ContactsPlugin extends Plugin { 0036 0037 /** 0038 * Used to request the device send the unique ID of every contact 0039 */ 0040 private static final String PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS = "kdeconnect.contacts.request_all_uids_timestamps"; 0041 0042 /** 0043 * Used to request the names for the contacts corresponding to a list of UIDs 0044 * <p> 0045 * It shall contain the key "uids", which will have a list of uIDs (long int, as string) 0046 */ 0047 private static final String PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS = "kdeconnect.contacts.request_vcards_by_uid"; 0048 0049 /** 0050 * Response indicating the packet contains a list of contact uIDs 0051 * <p> 0052 * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) 0053 * The returned IDs can be used in future requests for more information about the contact 0054 */ 0055 private static final String PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS = "kdeconnect.contacts.response_uids_timestamps"; 0056 0057 /** 0058 * Response indicating the packet contains a list of contact names 0059 * <p> 0060 * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) 0061 * then, for each UID, there shall be a field with the key of that UID and the value of the name of the contact 0062 * <p> 0063 * For example: 0064 * { 'uids' : ['1', '3', '15'], 0065 * '1' : 'John Smith', 0066 * '3' : 'Abe Lincoln', 0067 * '15' : 'Mom' 0068 * } 0069 */ 0070 private static final String PACKET_TYPE_CONTACTS_RESPONSE_VCARDS = "kdeconnect.contacts.response_vcards"; 0071 0072 @Override 0073 public @NonNull String getDisplayName() { 0074 return context.getResources().getString(R.string.pref_plugin_contacts); 0075 } 0076 0077 @Override 0078 public @NonNull String getDescription() { 0079 return context.getResources().getString(R.string.pref_plugin_contacts_desc); 0080 } 0081 0082 @Override 0083 public @NonNull String[] getSupportedPacketTypes() { 0084 return new String[]{ 0085 PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS, 0086 PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS 0087 }; 0088 } 0089 0090 @Override 0091 public @NonNull String[] getOutgoingPacketTypes() { 0092 return new String[]{ 0093 PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS, 0094 PACKET_TYPE_CONTACTS_RESPONSE_VCARDS 0095 }; 0096 } 0097 0098 @Override 0099 protected int getPermissionExplanation() { 0100 return R.string.contacts_permission_explanation; 0101 } 0102 0103 @Override 0104 public boolean isEnabledByDefault() { 0105 return true; 0106 } 0107 0108 @Override 0109 public @NonNull String[] getRequiredPermissions() { 0110 return new String[]{Manifest.permission.READ_CONTACTS}; 0111 // One day maybe we will also support WRITE_CONTACTS, but not yet 0112 } 0113 0114 @Override 0115 public boolean checkRequiredPermissions() { 0116 if (!arePermissionsGranted(getRequiredPermissions())) { 0117 return false; 0118 } 0119 return preferences.getBoolean("acceptedToTransferContacts", false); 0120 } 0121 0122 @Override 0123 public boolean supportsDeviceSpecificSettings() { 0124 return true; 0125 } 0126 0127 public @NonNull DialogFragment getPermissionExplanationDialog() { 0128 if (!arePermissionsGranted(getRequiredPermissions())) { 0129 return super.getPermissionExplanationDialog(); 0130 } 0131 AlertDialogFragment dialog = new AlertDialogFragment.Builder() 0132 .setTitle(getDisplayName()) 0133 .setMessage(R.string.contacts_per_device_confirmation) 0134 .setPositiveButton(R.string.ok) 0135 .setNegativeButton(R.string.cancel) 0136 .create(); 0137 dialog.setCallback(new AlertDialogFragment.Callback() { 0138 @Override 0139 public void onPositiveButtonClicked() { 0140 preferences.edit().putBoolean("acceptedToTransferContacts", true).apply(); 0141 device.reloadPluginsFromSettings(); 0142 } 0143 }); 0144 return dialog; 0145 } 0146 0147 /** 0148 * Add custom fields to the vcard to keep track of KDE Connect-specific fields 0149 * <p> 0150 * These include the local device's uID as well as last-changed timestamp 0151 * <p> 0152 * This might be extended in the future to include more fields 0153 * 0154 * @param vcard vcard to apply metadata to 0155 * @param uID uID to which the vcard corresponds 0156 * @throws ContactNotFoundException If the given ID for some reason does not match a contact 0157 * @return The same VCard as was passed in, but now with KDE Connect-specific fields 0158 */ 0159 private VCardBuilder addVCardMetadata(VCardBuilder vcard, uID uID) throws ContactNotFoundException { 0160 // Append the device ID line 0161 // Unclear if the deviceID forms a valid name per the vcard spec. Worry about that later.. 0162 vcard.appendLine("X-KDECONNECT-ID-DEV-" + device.getDeviceId(), 0163 uID.toString()); 0164 0165 // Build the timestamp line 0166 // Maybe one day this should be changed into the vcard-standard REV key 0167 Long timestamp = ContactsHelper.getContactTimestamp(context, uID); 0168 vcard.appendLine("X-KDECONNECT-TIMESTAMP", 0169 timestamp.toString()); 0170 0171 return vcard; 0172 } 0173 0174 /** 0175 * Return a unique identifier (Contacts.LOOKUP_KEY) for all contacts in the Contacts database 0176 * <p> 0177 * The identifiers returned can be used in future requests to get more information 0178 * about the contact 0179 * 0180 * @param np The packet containing the request 0181 * @return true if successfully handled, false otherwise 0182 */ 0183 @SuppressWarnings("SameReturnValue") 0184 private boolean handleRequestAllUIDsTimestamps(@SuppressWarnings("unused") NetworkPacket np) { 0185 NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS); 0186 0187 Map<uID, Long> uIDsToTimestamps = ContactsHelper.getAllContactTimestamps(context); 0188 0189 int contactCount = uIDsToTimestamps.size(); 0190 0191 List<String> uIDs = new ArrayList<>(contactCount); 0192 0193 for (uID contactID : uIDsToTimestamps.keySet()) { 0194 Long timestamp = uIDsToTimestamps.get(contactID); 0195 reply.set(contactID.toString(), timestamp); 0196 uIDs.add(contactID.toString()); 0197 } 0198 0199 reply.set("uids", uIDs); 0200 0201 device.sendPacket(reply); 0202 0203 return true; 0204 } 0205 0206 private boolean handleRequestVCardsByUIDs(NetworkPacket np) { 0207 if (!np.has("uids")) { 0208 Log.e("ContactsPlugin", "handleRequestNamesByUIDs received a malformed packet with no uids key"); 0209 return false; 0210 } 0211 0212 List<String> uIDsAsStrings = np.getStringList("uids"); 0213 0214 // Convert to Collection<uIDs> to call getVCardsForContactIDs 0215 Set<uID> uIDs = new HashSet<>(uIDsAsStrings.size()); 0216 for (String uID : uIDsAsStrings) { 0217 uIDs.add(new uID(uID)); 0218 } 0219 0220 Map<uID, VCardBuilder> uIDsToVCards = ContactsHelper.getVCardsForContactIDs(context, uIDs); 0221 0222 // ContactsHelper.getVCardsForContactIDs(..) is allowed to reply without 0223 // some of the requested uIDs if they were not in the database, so update our list 0224 uIDsAsStrings = new ArrayList<>(uIDsToVCards.size()); 0225 0226 NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_VCARDS); 0227 0228 // Add the vcards to the packet 0229 for (uID uID : uIDsToVCards.keySet()) { 0230 VCardBuilder vcard = uIDsToVCards.get(uID); 0231 0232 try { 0233 vcard = this.addVCardMetadata(vcard, uID); 0234 0235 // Store this as a valid uID 0236 uIDsAsStrings.add(uID.toString()); 0237 // Add the uid -> vcard pairing to the packet 0238 reply.set(uID.toString(), vcard.toString()); 0239 0240 } catch (ContactsHelper.ContactNotFoundException e) { 0241 e.printStackTrace(); 0242 } 0243 } 0244 0245 // Add the valid uIDs to the packet 0246 reply.set("uids", uIDsAsStrings); 0247 0248 device.sendPacket(reply); 0249 0250 return true; 0251 } 0252 0253 @Override 0254 public boolean onPacketReceived(@NonNull NetworkPacket np) { 0255 switch (np.getType()) { 0256 case PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS: 0257 return this.handleRequestAllUIDsTimestamps(np); 0258 case PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS: 0259 return this.handleRequestVCardsByUIDs(np); 0260 default: 0261 Log.e("ContactsPlugin", "Contacts plugin received an unexpected packet!"); 0262 return false; 0263 } 0264 } 0265 }