mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-10-17 14:19:33 +00:00
Compare commits
7 Commits
work/alber
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
27172a8aef | ||
|
5cd400e5b1 | ||
|
15bfa52304 | ||
|
a3a1e797d4 | ||
|
2d8b43d75d | ||
|
3fdc2b4951 | ||
|
3d7de57336 |
@@ -3,7 +3,7 @@
|
||||
# This file is distributed under the license LGPL version 2.1 or
|
||||
# version 3 or later versions approved by the membership of KDE e.V.
|
||||
#
|
||||
# Josep M. Ferrer <txemaq@gmail.com>, 2023, 2024.
|
||||
# SPDX-FileCopyrightText: 2023, 2024 Josep M. Ferrer <txemaq@gmail.com>
|
||||
#. extracted from ./metadata/android/en-US/full_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# This file is distributed under the license LGPL version 2.1 or
|
||||
# version 3 or later versions approved by the membership of KDE e.V.
|
||||
#
|
||||
# Josep M. Ferrer <txemaq@gmail.com>, 2023.
|
||||
# SPDX-FileCopyrightText: 2023 Josep M. Ferrer <txemaq@gmail.com>
|
||||
#. extracted from ./metadata/android/en-US/short_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# This file is distributed under the license LGPL version 2.1 or
|
||||
# version 3 or later versions approved by the membership of KDE e.V.
|
||||
#
|
||||
# Josep M. Ferrer <txemaq@gmail.com>, 2023, 2024.
|
||||
# SPDX-FileCopyrightText: 2023, 2024 Josep M. Ferrer <txemaq@gmail.com>
|
||||
#. extracted from ./metadata/android/en-US/full_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# This file is distributed under the license LGPL version 2.1 or
|
||||
# version 3 or later versions approved by the membership of KDE e.V.
|
||||
#
|
||||
# Josep M. Ferrer <txemaq@gmail.com>, 2023.
|
||||
# SPDX-FileCopyrightText: 2023 Josep M. Ferrer <txemaq@gmail.com>
|
||||
#. extracted from ./metadata/android/en-US/short_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@@ -240,6 +240,7 @@
|
||||
<string name="custom_devices_settings_summary">%d أجهزة مضافة يدوياً</string>
|
||||
<string name="custom_device_list">أضف أجهزة بعنوان IP</string>
|
||||
<string name="custom_device_deleted">حُذف الجهاز المخصّص</string>
|
||||
<string name="custom_device_list_help">إذا لم يُكتشف جهازك آليّاً يمكنك إضافة عنوان IP الخاص به أو اسم المضيف الخاص به من خلال النقر على الزر في الأسفل</string>
|
||||
<string name="custom_device_fab_hint">أضف جهازاً</string>
|
||||
<string name="undo">تراجع</string>
|
||||
<string name="share_notification_preference">إشعارات مزعجة</string>
|
||||
|
@@ -208,6 +208,7 @@
|
||||
<string name="custom_devices_settings_summary">%d dispositivi aggiunti manualmente</string>
|
||||
<string name="custom_device_list">Aggiungi dispositivi per IP</string>
|
||||
<string name="custom_device_deleted">Dispositivo personalizzato eliminato</string>
|
||||
<string name="custom_device_list_help">Se il tuo dispositivo non è rilevato automaticamente, puoi aggiungere il suo indirizzo IP o il nome host facendo clic sul pulsante seguente</string>
|
||||
<string name="custom_device_fab_hint">Aggiungi un dispositivo</string>
|
||||
<string name="undo">Annulla</string>
|
||||
<string name="share_notification_preference">Notifiche rumorose</string>
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">רגישות גלילה</string>
|
||||
<string name="gyro_mouse_enabled_title">הפעלת עכבר גירוסקופי</string>
|
||||
<string name="gyro_mouse_sensitivity_title">רגישות גירוסקופ</string>
|
||||
<string name="bigscreen_show_home_title">הצגת כפתור הבית</string>
|
||||
<string name="bigscreen_show_back_title">הצגת כפתור חזרה</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>לחיצה שמאלית</item>
|
||||
<item>לחיצה ימנית</item>
|
||||
@@ -222,6 +224,7 @@
|
||||
<string name="custom_devices_settings_summary">%d התקנים נוספו ידנית</string>
|
||||
<string name="custom_device_list">הוספת מכשירים לפי IP</string>
|
||||
<string name="custom_device_deleted">מכשיר מותאם אישית נמחק</string>
|
||||
<string name="custom_device_list_help">אם המכשיר שלך לא מזוהה אוטומטית אפשר להוסיף את כתובת ה־IP או את שם המארח שלו בלחיצה על הכפתור שלהלן</string>
|
||||
<string name="custom_device_fab_hint">הוספת מכשיר</string>
|
||||
<string name="undo">החזרה</string>
|
||||
<string name="share_notification_preference">התראות רועשות</string>
|
||||
@@ -362,6 +365,7 @@
|
||||
<string name="bigscreen_select">בחירה</string>
|
||||
<string name="bigscreen_right">ימין</string>
|
||||
<string name="bigscreen_down">למטה</string>
|
||||
<string name="bigscreen_back">חזרה</string>
|
||||
<string name="bigscreen_mic">מיקרופון</string>
|
||||
<string name="pref_plugin_bigscreen">שלט Bigscreen</string>
|
||||
<string name="pref_plugin_bigscreen_desc">אפשר להשתמש במכשיר שלך כשלט ל־Bigscreen פלזמה</string>
|
||||
|
@@ -53,7 +53,21 @@ object SMSHelper {
|
||||
// The constant Telephony.Mms.Part.CONTENT_URI was added in API 29
|
||||
val mMSPartUri : Uri = "content://mms/part/".toUri()
|
||||
|
||||
val mConversationUri : Uri = "content://mms-sms/conversations?simple=true".toUri()
|
||||
/**
|
||||
* Get the base address for all message conversations
|
||||
* We only use this to fetch thread_ids because the data it returns if often incomplete or useless
|
||||
*/
|
||||
private fun getConversationUri(): Uri {
|
||||
// Special case for Samsung
|
||||
// For some reason, Samsung devices do not support the regular SmsMms column.
|
||||
// However, according to https://stackoverflow.com/a/13640868/3723163, we can work around it this way.
|
||||
// By my understanding, "simple=true" means we can't support multi-target messages.
|
||||
// Go complain to Samsung about their annoying OS changes!
|
||||
if ("Samsung".equals(Build.MANUFACTURER, ignoreCase = true)) {
|
||||
Log.i("SMSHelper", "This appears to be a Samsung device. This may cause some features to not work properly.")
|
||||
}
|
||||
return "content://mms-sms/conversations?simple=true".toUri()
|
||||
}
|
||||
|
||||
private fun getCompleteConversationsUri(): Uri {
|
||||
// This glorious - but completely undocumented - content URI gives us all messages, both MMS and SMS,
|
||||
@@ -362,7 +376,7 @@ object SMSHelper {
|
||||
if (getSubscriptionIdSupport(uri, context)) {
|
||||
allColumns.addAll(Message.multiSIMColumns)
|
||||
}
|
||||
if (uri != mConversationUri) {
|
||||
if (uri != getConversationUri()) {
|
||||
// See https://issuetracker.google.com/issues/134592631
|
||||
allColumns.add(TRANSPORT_TYPE_DISCRIMINATOR_COLUMN)
|
||||
}
|
||||
@@ -414,6 +428,7 @@ object SMSHelper {
|
||||
* @return Non-blocking iterable of the first message in each conversation
|
||||
*/
|
||||
fun getConversations(context: Context): Sequence<Message> {
|
||||
val uri = getConversationUri()
|
||||
|
||||
// Used to avoid spewing logs in case there is an overall problem with fetching thread IDs
|
||||
var warnedForNullThreadIDs = false
|
||||
@@ -428,7 +443,7 @@ object SMSHelper {
|
||||
// return conversations, but I doubt anyone will ever find it necessary.
|
||||
var threadIds: List<ThreadID>
|
||||
context.contentResolver.query(
|
||||
mConversationUri,
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -599,24 +614,20 @@ object SMSHelper {
|
||||
|
||||
// Get the actual image from the mms database convert it into thumbnail and encode to Base64
|
||||
val image = SmsMmsUtils.getMmsImage(context, partID)
|
||||
if (image != null) {
|
||||
val thumbnailImage = ThumbnailUtils.extractThumbnail(
|
||||
image,
|
||||
THUMBNAIL_WIDTH,
|
||||
THUMBNAIL_HEIGHT
|
||||
val thumbnailImage = ThumbnailUtils.extractThumbnail(
|
||||
image,
|
||||
THUMBNAIL_WIDTH,
|
||||
THUMBNAIL_HEIGHT
|
||||
)
|
||||
val encodedThumbnail = SmsMmsUtils.bitMapToBase64(thumbnailImage)
|
||||
attachments.add(
|
||||
Attachment(
|
||||
partID,
|
||||
contentType,
|
||||
encodedThumbnail,
|
||||
fileName
|
||||
)
|
||||
val encodedThumbnail = SmsMmsUtils.bitMapToBase64(thumbnailImage)
|
||||
attachments.add(
|
||||
Attachment(
|
||||
partID,
|
||||
contentType,
|
||||
encodedThumbnail,
|
||||
fileName
|
||||
)
|
||||
)
|
||||
thumbnailImage.recycle()
|
||||
if (!image.isRecycled) image.recycle()
|
||||
}
|
||||
)
|
||||
} else if (MimeType.isTypeVideo(contentType)) {
|
||||
val fileName = data.substring(data.lastIndexOf('/') + 1)
|
||||
|
||||
@@ -627,20 +638,17 @@ object SMSHelper {
|
||||
ContentUris.withAppendedId(mMSPartUri, partID)
|
||||
)
|
||||
val videoThumbnail = retriever.frameAtTime
|
||||
if (videoThumbnail != null) {
|
||||
val encodedThumbnail = SmsMmsUtils.bitMapToBase64(
|
||||
videoThumbnail.scale(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
|
||||
val encodedThumbnail = SmsMmsUtils.bitMapToBase64(
|
||||
videoThumbnail!!.scale(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
|
||||
)
|
||||
attachments.add(
|
||||
Attachment(
|
||||
partID,
|
||||
contentType,
|
||||
encodedThumbnail,
|
||||
fileName
|
||||
)
|
||||
attachments.add(
|
||||
Attachment(
|
||||
partID,
|
||||
contentType,
|
||||
encodedThumbnail,
|
||||
fileName
|
||||
)
|
||||
)
|
||||
}
|
||||
retriever.release()
|
||||
)
|
||||
} else if (MimeType.isTypeAudio(contentType)) {
|
||||
val fileName = data.substring(data.lastIndexOf('/') + 1)
|
||||
attachments.add(Attachment(partID, contentType, null, fileName))
|
||||
@@ -763,6 +771,15 @@ object SMSHelper {
|
||||
return body
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a ContentObserver for the Messages database
|
||||
*
|
||||
* @param observer ContentObserver to alert on Message changes
|
||||
*/
|
||||
fun registerObserver(observer: ContentObserver, context: Context) {
|
||||
context.contentResolver.registerContentObserver(getConversationUri(), true, observer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given JSONArray of attachments into List<Attachment>
|
||||
*
|
||||
@@ -864,7 +881,7 @@ object SMSHelper {
|
||||
}
|
||||
}
|
||||
|
||||
class Address(context: Context, private val address: String) {
|
||||
class Address(val context: Context, private val address: String) {
|
||||
@Throws(JSONException::class)
|
||||
fun toJson(): JSONObject {
|
||||
val json = JSONObject()
|
||||
|
@@ -202,6 +202,10 @@ class NetworkPacket private constructor(
|
||||
return mBody.has(key)
|
||||
}
|
||||
|
||||
operator fun contains(key: String): Boolean {
|
||||
return has(key)
|
||||
}
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun serialize(): String {
|
||||
val jo = JSONObject()
|
||||
|
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* ContactsPlugin.java - This file is part of KDE Connect's Android App
|
||||
* Implement a way to request and send contact information
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2018 Simon Redman <simon@ergotech.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.ContactsPlugin;
|
||||
|
||||
import android.Manifest;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper;
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.ContactNotFoundException;
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.VCardBuilder;
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.uID;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.AlertDialogFragment;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
public class ContactsPlugin extends Plugin {
|
||||
|
||||
/**
|
||||
* Used to request the device send the unique ID of every contact
|
||||
*/
|
||||
private static final String PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS = "kdeconnect.contacts.request_all_uids_timestamps";
|
||||
|
||||
/**
|
||||
* Used to request the names for the contacts corresponding to a list of UIDs
|
||||
* <p>
|
||||
* It shall contain the key "uids", which will have a list of uIDs (long int, as string)
|
||||
*/
|
||||
private static final String PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS = "kdeconnect.contacts.request_vcards_by_uid";
|
||||
|
||||
/**
|
||||
* Response indicating the packet contains a list of contact uIDs
|
||||
* <p>
|
||||
* It shall contain the key "uids", which will mark a list of uIDs (long int, as string)
|
||||
* The returned IDs can be used in future requests for more information about the contact
|
||||
*/
|
||||
private static final String PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS = "kdeconnect.contacts.response_uids_timestamps";
|
||||
|
||||
/**
|
||||
* Response indicating the packet contains a list of contact names
|
||||
* <p>
|
||||
* It shall contain the key "uids", which will mark a list of uIDs (long int, as string)
|
||||
* then, for each UID, there shall be a field with the key of that UID and the value of the name of the contact
|
||||
* <p>
|
||||
* For example:
|
||||
* { 'uids' : ['1', '3', '15'],
|
||||
* '1' : 'John Smith',
|
||||
* '3' : 'Abe Lincoln',
|
||||
* '15' : 'Mom'
|
||||
* }
|
||||
*/
|
||||
private static final String PACKET_TYPE_CONTACTS_RESPONSE_VCARDS = "kdeconnect.contacts.response_vcards";
|
||||
|
||||
@Override
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_contacts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_contacts_desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{
|
||||
PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS,
|
||||
PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{
|
||||
PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS,
|
||||
PACKET_TYPE_CONTACTS_RESPONSE_VCARDS
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getPermissionExplanation() {
|
||||
return R.string.contacts_permission_explanation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabledByDefault() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String[] getRequiredPermissions() {
|
||||
return new String[]{Manifest.permission.READ_CONTACTS};
|
||||
// One day maybe we will also support WRITE_CONTACTS, but not yet
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkRequiredPermissions() {
|
||||
if (!arePermissionsGranted(getRequiredPermissions())) {
|
||||
return false;
|
||||
}
|
||||
return getPreferences().getBoolean("acceptedToTransferContacts", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsDeviceSpecificSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public @NonNull DialogFragment getPermissionExplanationDialog() {
|
||||
if (!arePermissionsGranted(getRequiredPermissions())) {
|
||||
return super.getPermissionExplanationDialog();
|
||||
}
|
||||
AlertDialogFragment dialog = new AlertDialogFragment.Builder()
|
||||
.setTitle(getDisplayName())
|
||||
.setMessage(R.string.contacts_per_device_confirmation)
|
||||
.setPositiveButton(R.string.ok)
|
||||
.setNegativeButton(R.string.cancel)
|
||||
.create();
|
||||
dialog.setCallback(new AlertDialogFragment.Callback() {
|
||||
@Override
|
||||
public boolean onPositiveButtonClicked() {
|
||||
Objects.requireNonNull(getPreferences()).edit().putBoolean("acceptedToTransferContacts", true).apply();
|
||||
Objects.requireNonNull(getDevice()).launchBackgroundReloadPluginsFromSettings();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom fields to the vcard to keep track of KDE Connect-specific fields
|
||||
* <p>
|
||||
* These include the local device's uID as well as last-changed timestamp
|
||||
* <p>
|
||||
* This might be extended in the future to include more fields
|
||||
*
|
||||
* @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) 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-" + getDevice().getDeviceId(),
|
||||
uID.toString());
|
||||
|
||||
// Build the timestamp line
|
||||
// Maybe one day this should be changed into the vcard-standard REV key
|
||||
Long timestamp = ContactsHelper.getContactTimestamp(context, uID);
|
||||
vcard.appendLine("X-KDECONNECT-TIMESTAMP",
|
||||
timestamp.toString());
|
||||
|
||||
return vcard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique identifier (Contacts.LOOKUP_KEY) for all contacts in the Contacts database
|
||||
* <p>
|
||||
* The identifiers returned can be used in future requests to get more information
|
||||
* about the contact
|
||||
*
|
||||
* @param np The packet containing the request
|
||||
* @return true if successfully handled, false otherwise
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private boolean handleRequestAllUIDsTimestamps(@SuppressWarnings("unused") NetworkPacket np) {
|
||||
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS);
|
||||
|
||||
Map<uID, Long> uIDsToTimestamps = ContactsHelper.getAllContactTimestamps(context);
|
||||
|
||||
int contactCount = uIDsToTimestamps.size();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
reply.set("uids", uIDs);
|
||||
|
||||
getDevice().sendPacket(reply);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleRequestVCardsByUIDs(NetworkPacket np) {
|
||||
if (!np.has("uids")) {
|
||||
Log.e("ContactsPlugin", "handleRequestNamesByUIDs received a malformed packet with no uids key");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> uIDsAsStrings = np.getStringList("uids");
|
||||
|
||||
// Convert to Collection<uIDs> to call getVCardsForContactIDs
|
||||
Set<uID> uIDs = new HashSet<>(uIDsAsStrings.size());
|
||||
for (String uID : uIDsAsStrings) {
|
||||
uIDs.add(new uID(uID));
|
||||
}
|
||||
|
||||
Map<uID, VCardBuilder> uIDsToVCards = ContactsHelper.getVCardsForContactIDs(context, uIDs);
|
||||
|
||||
// ContactsHelper.getVCardsForContactIDs(..) is allowed to reply without
|
||||
// some of the requested uIDs if they were not in the database, so update our list
|
||||
uIDsAsStrings = new ArrayList<>(uIDsToVCards.size());
|
||||
|
||||
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_VCARDS);
|
||||
|
||||
// Add the vcards to the packet
|
||||
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
|
||||
reply.set("uids", uIDsAsStrings);
|
||||
|
||||
getDevice().sendPacket(reply);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
switch (np.getType()) {
|
||||
case PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS:
|
||||
return this.handleRequestAllUIDsTimestamps(np);
|
||||
case PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS:
|
||||
return this.handleRequestVCardsByUIDs(np);
|
||||
default:
|
||||
Log.e("ContactsPlugin", "Contacts plugin received an unexpected packet!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
218
src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.kt
Normal file
218
src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.kt
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* ContactsPlugin.java - This file is part of KDE Connect's Android App
|
||||
* Implement a way to request and send contact information
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2018 Simon Redman <simon@ergotech.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.ContactsPlugin
|
||||
|
||||
import android.Manifest
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.ContactNotFoundException
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.VCardBuilder
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.uID
|
||||
import org.kde.kdeconnect.NetworkPacket
|
||||
import org.kde.kdeconnect.Plugins.Plugin
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin
|
||||
import org.kde.kdeconnect.UserInterface.AlertDialogFragment
|
||||
import org.kde.kdeconnect_tp.R
|
||||
import androidx.core.content.edit
|
||||
|
||||
@LoadablePlugin
|
||||
class ContactsPlugin : Plugin() {
|
||||
override val displayName: String
|
||||
get() = context.resources.getString(R.string.pref_plugin_contacts)
|
||||
|
||||
override val description: String
|
||||
get() = context.resources.getString(R.string.pref_plugin_contacts_desc)
|
||||
|
||||
override val supportedPacketTypes: Array<String>
|
||||
get() = arrayOf(PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS, PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS)
|
||||
|
||||
override val outgoingPacketTypes: Array<String>
|
||||
get() = arrayOf(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS, PACKET_TYPE_CONTACTS_RESPONSE_VCARDS)
|
||||
|
||||
override val permissionExplanation: Int
|
||||
get() = R.string.contacts_permission_explanation
|
||||
|
||||
override val isEnabledByDefault: Boolean
|
||||
get() = true
|
||||
|
||||
// One day maybe we will also support WRITE_CONTACTS, but not yet
|
||||
override val requiredPermissions: Array<String>
|
||||
get() = arrayOf(Manifest.permission.READ_CONTACTS)
|
||||
|
||||
override fun checkRequiredPermissions(): Boolean {
|
||||
if (!arePermissionsGranted(requiredPermissions)) {
|
||||
return false
|
||||
}
|
||||
return preferences!!.getBoolean("acceptedToTransferContacts", false)
|
||||
}
|
||||
|
||||
override fun supportsDeviceSpecificSettings(): Boolean = true
|
||||
|
||||
override val permissionExplanationDialog: DialogFragment
|
||||
get() {
|
||||
if (!arePermissionsGranted(requiredPermissions)) {
|
||||
return super.permissionExplanationDialog
|
||||
}
|
||||
return AlertDialogFragment.Builder()
|
||||
.setTitle(displayName)
|
||||
.setMessage(R.string.contacts_per_device_confirmation)
|
||||
.setPositiveButton(R.string.ok)
|
||||
.setNegativeButton(R.string.cancel)
|
||||
.create()
|
||||
.apply {
|
||||
setCallback(object : AlertDialogFragment.Callback() {
|
||||
override fun onPositiveButtonClicked(): Boolean {
|
||||
preferences!!.edit { putBoolean("acceptedToTransferContacts", true) }
|
||||
device.launchBackgroundReloadPluginsFromSettings()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom fields to the vcard to keep track of KDE Connect-specific fields
|
||||
*
|
||||
*
|
||||
* These include the local device's uID as well as last-changed timestamp
|
||||
*
|
||||
*
|
||||
* This might be extended in the future to include more fields
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
@Throws(ContactNotFoundException::class)
|
||||
private fun addVCardMetadata(vcard: VCardBuilder, uID: uID): VCardBuilder {
|
||||
// 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.deviceId}", uID.toString())
|
||||
|
||||
val timestamp: Long = ContactsHelper.getContactTimestamp(context, uID)
|
||||
vcard.appendLine("REV", timestamp.toString())
|
||||
|
||||
return vcard
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique identifier (Contacts.LOOKUP_KEY) for all contacts in the Contacts database
|
||||
*
|
||||
*
|
||||
* The identifiers returned can be used in future requests to get more information about the contact
|
||||
*
|
||||
* @param np The packet containing the request
|
||||
* @return true if successfully handled, false otherwise
|
||||
*/
|
||||
private fun handleRequestAllUIDsTimestamps(@Suppress("unused") np: NetworkPacket): Boolean {
|
||||
val uIDsToTimestamps: Map<uID, Long> = ContactsHelper.getAllContactTimestamps(context)
|
||||
val reply = NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS).apply {
|
||||
val uIDsAsString = mutableListOf<String>()
|
||||
for ((contactID: uID, timestamp: Long) in uIDsToTimestamps) {
|
||||
set(contactID.toString(), timestamp.toString())
|
||||
uIDsAsString.add(contactID.toString())
|
||||
}
|
||||
set(PACKET_UIDS_KEY, uIDsAsString)
|
||||
}
|
||||
|
||||
device.sendPacket(reply)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleRequestVCardsByUIDs(np: NetworkPacket): Boolean {
|
||||
if (PACKET_UIDS_KEY !in np) {
|
||||
Log.e("ContactsPlugin", "handleRequestNamesByUIDs received a malformed packet with no uids key")
|
||||
return false
|
||||
}
|
||||
|
||||
val storedUIDs: List<uID>? = np.getStringList("uids")?.distinct()?.map { uID(it) }
|
||||
if (storedUIDs == null) {
|
||||
Log.e("ContactsPlugin", "handleRequestNamesByUIDs received a malformed packet with no uids")
|
||||
return false
|
||||
}
|
||||
|
||||
val uIDsToVCards: Map<uID, VCardBuilder> = ContactsHelper.getVCardsForContactIDs(context, storedUIDs)
|
||||
|
||||
val reply = NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_VCARDS).apply {
|
||||
// ContactsHelper.getVCardsForContactIDs(..) is allowed to reply without some of the requested uIDs if they were not in the database, so update our list
|
||||
val uIDsAsStrings = mutableListOf<String>()
|
||||
for ((uID: uID, vcard: VCardBuilder) in uIDsToVCards) {
|
||||
try {
|
||||
val vcardWithMetadata = addVCardMetadata(vcard, uID)
|
||||
// Store this as a valid uID
|
||||
uIDsAsStrings.add(uID.toString())
|
||||
// Add the uid -> vcard pairing to the packet
|
||||
set(uID.toString(), vcardWithMetadata.toString())
|
||||
} catch (e: ContactNotFoundException) {
|
||||
Log.e("ContactsPlugin", "handleRequestVCardsByUIDs failed to find contact with uID $uID")
|
||||
}
|
||||
}
|
||||
set(PACKET_UIDS_KEY, uIDsAsStrings)
|
||||
}
|
||||
|
||||
device.sendPacket(reply)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPacketReceived(np: NetworkPacket): Boolean = when (np.type) {
|
||||
PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS -> this.handleRequestAllUIDsTimestamps(np)
|
||||
PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS -> this.handleRequestVCardsByUIDs(np)
|
||||
else -> {
|
||||
Log.e("ContactsPlugin", "Contacts plugin received an unexpected packet!")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PACKET_UIDS_KEY: String = "uids"
|
||||
|
||||
/**
|
||||
* Used to request the device send the unique ID of every contact
|
||||
*/
|
||||
private const val PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS: String = "kdeconnect.contacts.request_all_uids_timestamps"
|
||||
|
||||
/**
|
||||
* Used to request the names for the contacts corresponding to a list of UIDs
|
||||
*
|
||||
*
|
||||
* It shall contain the key "uids", which will have a list of uIDs (long int, as string)
|
||||
*/
|
||||
private const val PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS: String = "kdeconnect.contacts.request_vcards_by_uid"
|
||||
|
||||
/**
|
||||
* Response indicating the packet contains a list of contact uIDs
|
||||
*
|
||||
*
|
||||
* It shall contain the key "uids", which will mark a list of uIDs (long int, as string)
|
||||
* The returned IDs can be used in future requests for more information about the contact
|
||||
*/
|
||||
private const val PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS: String = "kdeconnect.contacts.response_uids_timestamps"
|
||||
|
||||
/**
|
||||
* Response indicating the packet contains a list of contact names
|
||||
*
|
||||
*
|
||||
* It shall contain the key "uids", which will mark a list of uIDs (long int, as string)
|
||||
* then, for each UID, there shall be a field with the key of that UID and the value of the name of the contact
|
||||
*
|
||||
*
|
||||
* For example:
|
||||
* { 'uids' : ['1', '3', '15'],
|
||||
* '1' : 'John Smith',
|
||||
* '3' : 'Abe Lincoln',
|
||||
* '15' : 'Mom'
|
||||
* }
|
||||
*/
|
||||
private const val PACKET_TYPE_CONTACTS_RESPONSE_VCARDS: String = "kdeconnect.contacts.response_vcards"
|
||||
}
|
||||
}
|
@@ -28,7 +28,6 @@ import android.telephony.SmsManager
|
||||
import android.telephony.SmsMessage
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.klinker.android.logger.Log
|
||||
import com.klinker.android.send_message.Transaction
|
||||
import org.json.JSONArray
|
||||
@@ -44,6 +43,7 @@ import org.kde.kdeconnect.Helpers.SMSHelper.getMessagesInThread
|
||||
import org.kde.kdeconnect.Helpers.SMSHelper.getNewestMessageTimestamp
|
||||
import org.kde.kdeconnect.Helpers.SMSHelper.jsonArrayToAddressList
|
||||
import org.kde.kdeconnect.Helpers.SMSHelper.jsonArrayToAttachmentsList
|
||||
import org.kde.kdeconnect.Helpers.SMSHelper.registerObserver
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper.execute
|
||||
import org.kde.kdeconnect.NetworkPacket
|
||||
import org.kde.kdeconnect.Plugins.Plugin
|
||||
@@ -134,9 +134,6 @@ class SMSPlugin : Plugin() {
|
||||
}
|
||||
}
|
||||
|
||||
private val messageObserver: ContentObserver = MessageContentObserver(Handler(getLooper()!!))
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to read the latest message from the sms-mms database and sends it to the desktop
|
||||
*
|
||||
@@ -235,7 +232,9 @@ class SMSPlugin : Plugin() {
|
||||
refreshFilter.priority = 500
|
||||
context.registerReceiver(messagesUpdateReceiver, refreshFilter, ContextCompat.RECEIVER_EXPORTED)
|
||||
|
||||
context.contentResolver.registerContentObserver(SMSHelper.mConversationUri, true, messageObserver)
|
||||
val helperLooper: Looper? = getLooper()
|
||||
val messageObserver: ContentObserver = MessageContentObserver(Handler(helperLooper!!))
|
||||
registerObserver(messageObserver, context)
|
||||
|
||||
// To see debug messages for Klinker library, uncomment the below line
|
||||
//Log.setDebug(true)
|
||||
@@ -246,12 +245,6 @@ class SMSPlugin : Plugin() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
context.unregisterReceiver(receiver)
|
||||
context.unregisterReceiver(messagesUpdateReceiver)
|
||||
context.contentResolver.unregisterContentObserver(messageObserver)
|
||||
}
|
||||
|
||||
override val displayName: String
|
||||
get() = context.resources.getString(R.string.pref_plugin_telepathy)
|
||||
|
||||
@@ -288,27 +281,6 @@ class SMSPlugin : Plugin() {
|
||||
|
||||
true
|
||||
}
|
||||
TelephonyPlugin.PACKET_TYPE_TELEPHONY_REQUEST -> {
|
||||
if (np.getBoolean("sendSms")) {
|
||||
val phoneNo: String = np.getString("phoneNumber")
|
||||
val sms: String = np.getString("messageBody")
|
||||
val subID = np.getLong("subID", -1)
|
||||
|
||||
try {
|
||||
val smsManager: SmsManager = if (subID == -1L) SmsManager.getDefault() else SmsManager.getSmsManagerForSubscriptionId(subID.toInt())
|
||||
val parts: ArrayList<String> = smsManager.divideMessage(sms)
|
||||
|
||||
// If this message turns out to fit in a single SMS, sendMultipartTextMessage properly handles that case
|
||||
smsManager.sendMultipartTextMessage(phoneNo, null, parts, null, null)
|
||||
|
||||
//TODO: Notify other end
|
||||
} catch (e: Exception) {
|
||||
//TODO: Notify other end
|
||||
Log.e("SMSPlugin", "Exception", e)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
PACKET_TYPE_SMS_REQUEST_ATTACHMENT -> {
|
||||
val partID: Long = np.getLong("part_id")
|
||||
val uniqueIdentifier: String = np.getString("unique_identifier")
|
||||
@@ -387,7 +359,6 @@ class SMSPlugin : Plugin() {
|
||||
override val supportedPacketTypes: Array<String>
|
||||
get() = arrayOf(
|
||||
PACKET_TYPE_SMS_REQUEST,
|
||||
TelephonyPlugin.PACKET_TYPE_TELEPHONY_REQUEST,
|
||||
PACKET_TYPE_SMS_REQUEST_CONVERSATIONS,
|
||||
PACKET_TYPE_SMS_REQUEST_CONVERSATION,
|
||||
PACKET_TYPE_SMS_REQUEST_ATTACHMENT
|
||||
|
@@ -55,31 +55,6 @@ public class TelephonyPlugin extends Plugin {
|
||||
*/
|
||||
public final static String PACKET_TYPE_TELEPHONY = "kdeconnect.telephony";
|
||||
|
||||
/**
|
||||
* Old-style packet sent to request a simple telephony action
|
||||
* <p>
|
||||
* The events handled were:
|
||||
* - to request the device to mute its ringer
|
||||
* - to request an SMS to be sent.
|
||||
* <p>
|
||||
* In case an SMS was being requested, the body was like so:
|
||||
* { "sendSms": true,
|
||||
* "phoneNumber": "542904563213",
|
||||
* "messageBody": "Hi mom!"
|
||||
* }
|
||||
* <p>
|
||||
* In case a ringer muted was requested, the body looked like so:
|
||||
* { "action": "mute" }
|
||||
* <p>
|
||||
* Ringer mute requests are best handled by PACKET_TYPE_TELEPHONY_REQUEST_MUTE
|
||||
* <p>
|
||||
* This packet type is retained for backwards-compatibility with old desktop applications,
|
||||
* but support should be dropped once those applications are no longer supported. New
|
||||
* applications should not use this packet type.
|
||||
*/
|
||||
@Deprecated
|
||||
public final static String PACKET_TYPE_TELEPHONY_REQUEST = "kdeconnect.telephony.request";
|
||||
|
||||
/**
|
||||
* Packet sent to indicate the user has requested the device mute its ringer
|
||||
* <p>
|
||||
@@ -266,13 +241,7 @@ public class TelephonyPlugin extends Plugin {
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
|
||||
switch (np.getType()) {
|
||||
case PACKET_TYPE_TELEPHONY_REQUEST:
|
||||
if (np.getString("action").equals("mute")) {
|
||||
muteRinger();
|
||||
}
|
||||
break;
|
||||
case PACKET_TYPE_TELEPHONY_REQUEST_MUTE:
|
||||
muteRinger();
|
||||
break;
|
||||
@@ -295,7 +264,6 @@ public class TelephonyPlugin extends Plugin {
|
||||
@Override
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{
|
||||
PACKET_TYPE_TELEPHONY_REQUEST,
|
||||
PACKET_TYPE_TELEPHONY_REQUEST_MUTE,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user