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 }