contactInfo = new HashMap<>();
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
- Cursor cursor = null;
+ Cursor cursor;
try {
cursor = context.getContentResolver().query(
uri,
@@ -77,7 +92,7 @@
try {
cursor.close();
- } catch (Exception e) {
+ } catch (Exception ignored) {
}
if (!contactInfo.isEmpty()) {
@@ -102,6 +117,7 @@
input = context.getContentResolver().openInputStream(photoUri);
byte[] buffer = new byte[1024];
int len;
+ //noinspection ConstantConditions
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
@@ -111,16 +127,359 @@
return "";
} finally {
try {
+ //noinspection ConstantConditions
input.close();
} catch (Exception ignored) {
}
try {
+ //noinspection ConstantConditions
output.close();
} catch (Exception ignored) {
}
}
}
-}
+ /**
+ * Return all the NAME_RAW_CONTACT_IDS which contribute an entry to a Contact in the database
+ *
+ * If the user has, for example, joined several contacts, on the phone, the IDs returned will
+ * be representative of the joined contact
+ *
+ * See here: https://developer.android.com/reference/android/provider/ContactsContract.Contacts.html
+ * for more information about the connection between contacts and raw contacts
+ *
+ * @param context android.content.Context running the request
+ * @return List of each NAME_RAW_CONTACT_ID in the Contacts database
+ */
+ public static List getAllContactContactIDs(Context context) {
+ ArrayList toReturn = new ArrayList<>();
+
+ // Define the columns we want to read from the Contacts database
+ final String[] projection = new String[]{
+ ContactsContract.Contacts.LOOKUP_KEY
+ };
+
+ Uri contactsUri = ContactsContract.Contacts.CONTENT_URI;
+ Cursor contactsCursor = context.getContentResolver().query(
+ contactsUri,
+ projection,
+ null, null, null);
+ if (contactsCursor != null && contactsCursor.moveToFirst()) {
+ do {
+ uID contactID;
+
+ int idIndex = contactsCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
+ if (idIndex != -1) {
+ contactID = new uID(contactsCursor.getString(idIndex));
+ } 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");
+ continue;
+ }
+
+ toReturn.add(contactID);
+ } while (contactsCursor.moveToNext());
+ try {
+ contactsCursor.close();
+ } catch (Exception ignored) {
+ }
+ }
+
+ return toReturn;
+ }
+
+ /**
+ * Get VCards using the batch database query which requires Android API 21
+ *
+ * @param context android.content.Context running the request
+ * @param IDs collection of raw contact IDs to look up
+ * @param lookupKeys
+ * @return Mapping of raw contact IDs to corresponding VCard
+ */
+ @SuppressWarnings("ALL") // Since this method is busted anyway
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ @Deprecated
+ protected static Map getVCardsFast(Context context, Collection IDs, Map lookupKeys) {
+ LongSparseArray toReturn = new LongSparseArray<>();
+ StringBuilder keys = new StringBuilder();
+
+ List orderedIDs = new ArrayList<>(IDs);
+
+ for (Long ID : orderedIDs) {
+ String key = lookupKeys.get(ID);
+ keys.append(key);
+ keys.append(':');
+ }
+
+ // Remove trailing ':'
+ keys.deleteCharAt(keys.length() - 1);
+
+ Uri vcardURI = Uri.withAppendedPath(
+ ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI,
+ Uri.encode(keys.toString()));
+
+ InputStream input;
+ StringBuilder vcardJumble = new StringBuilder();
+ try {
+ input = context.getContentResolver().openInputStream(vcardURI);
+
+ BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input));
+ String line;
+
+ while ((line = bufferedInput.readLine()) != null) {
+ vcardJumble.append(line).append('\n');
+ }
+ } catch (IOException e) {
+ // If you are experiencing this, please open a bug report indicating how you got here
+ e.printStackTrace();
+ }
+
+ // At this point we are screwed:
+ // There is no way to figure out, given the lookup we just made, which VCard belonges
+ // to which ID. They appear to be in the same order as the request was made, but this
+ // is (provably) unreliable. I am leaving this code in case it is useful, but unless
+ // Android improves their API there is nothing we can do with it
+
+ return null;
+ }
+
+ /**
+ * Get VCards using serial database lookups. This is tragically slow, but the faster method using
+ *
+ * There is a faster API specified using ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI,
+ * but there does not seem to be a way to figure out which ID resulted in which VCard using that API
+ *
+ * @param context android.content.Context running the request
+ * @param IDs collection of uIDs to look up
+ * @return Mapping of uIDs to the corresponding VCard
+ */
+ @SuppressWarnings("UnnecessaryContinue")
+ protected static Map getVCardsSlow(Context context, Collection IDs) {
+ Map toReturn = new HashMap<>();
+
+ for (uID ID : IDs) {
+ String lookupKey = ID.toString();
+ Uri vcardURI = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
+ InputStream input;
+ try {
+ input = context.getContentResolver().openInputStream(vcardURI);
+
+ if (input == null)
+ {
+ throw new NullPointerException("ContentResolver did not give us a stream for the VCard for uID " + ID);
+ }
+
+ BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input));
+
+ StringBuilder vcard = new StringBuilder();
+ String line;
+ while ((line = bufferedInput.readLine()) != null) {
+ vcard.append(line).append('\n');
+ }
+
+ toReturn.put(ID, new VCardBuilder(vcard.toString()));
+ input.close();
+ } catch (IOException e) {
+ // If you are experiencing this, please open a bug report indicating how you got here
+ e.printStackTrace();
+ continue;
+ } catch (NullPointerException e)
+ {
+ // If you are experiencing this, please open a bug report indicating how you got here
+ e.printStackTrace();
+ }
+ }
+
+ return toReturn;
+ }
+
+ /**
+ * Get the VCard for every specified raw contact ID
+ *
+ * @param context android.content.Context running the request
+ * @param IDs collection of raw contact IDs to look up
+ * @return Mapping of raw contact IDs to the corresponding VCard
+ */
+ public static Map getVCardsForContactIDs(Context context, Collection IDs) {
+ return getVCardsSlow(context, IDs);
+ }
+
+ /**
+ * Return a mapping of contact IDs to a map of the requested data from the Contacts database
+ *
+ * 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
+ *
+ * @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 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> getColumnsFromContactsForIDs(Context context, Collection IDs, String[] contactsProjection) {
+ HashMap> toReturn = new HashMap<>();
+
+ Uri contactsUri = ContactsContract.Contacts.CONTENT_URI;
+
+ // Regardless of whether it was requested, we need to look up the uID column
+ Set lookupProjection = new HashSet<>(Arrays.asList(contactsProjection));
+ lookupProjection.add(uID.COLUMN);
+
+ // We need a selection which looks like " 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 contactsArgs = new ArrayList<>();
+ for (uID ID : IDs) {
+ contactsArgs.add(ID.toString());
+ }
+
+ Cursor contactsCursor = context.getContentResolver().query(
+ contactsUri,
+ lookupProjection.toArray(new String[0]),
+ contactsSelection.toString(),
+ contactsArgs.toArray(new String[0]), null
+ );
+
+ if (contactsCursor != null && contactsCursor.moveToFirst()) {
+ do {
+ Map requestedData = new HashMap<>();
+
+ int lookupKeyIdx = contactsCursor.getColumnIndexOrThrow(uID.COLUMN);
+ String lookupKey = contactsCursor.getString(lookupKeyIdx);
+
+ // For each column, collect the data from that column
+ for (String column : contactsProjection) {
+ 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;
+ 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");
+ 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);
+ } while (contactsCursor.moveToNext());
+ try {
+ contactsCursor.close();
+ } catch (Exception ignored) {
+ }
+ }
+
+ return toReturn;
+ }
+
+ /**
+ * This is a cheap ripoff of com.android.vcard.VCardBuilder
+ *
+ * Maybe in the future that library will be made public and we can switch to using that!
+ *
+ * The main similarity is the usage of .toString() to produce the finalized VCard and the
+ * usage of .appendLine(String, String) to add stuff to the vcard
+ */
+ public static class VCardBuilder {
+ protected static final String VCARD_END = "END:VCARD"; // Written to terminate the vcard
+ protected static final String VCARD_DATA_SEPARATOR = ":";
+
+ final StringBuilder vcardBody;
+
+ /**
+ * Take a partial vcard as a string and make a VCardBuilder
+ *
+ * @param vcard vcard to build upon
+ */
+ public VCardBuilder(String vcard) {
+ // Remove the end tag. We will add it back on in .toString()
+ vcard = vcard.substring(0, vcard.indexOf(VCARD_END));
+
+ vcardBody = new StringBuilder(vcard);
+ }
+
+ /**
+ * Appends one line with a given property name and value.
+ */
+ public void appendLine(final String propertyName, final String rawValue) {
+ vcardBody.append(propertyName)
+ .append(VCARD_DATA_SEPARATOR)
+ .append(rawValue)
+ .append("\n");
+ }
+
+ public String toString() {
+ return vcardBody.toString() + VCARD_END;
+ }
+ }
+
+ /**
+ * Essentially a typedef of the type used for a unique identifier
+ */
+ public static class uID {
+ /**
+ * We use the LOOKUP_KEY column of the Contacts table as a unique ID, since that's what it's
+ * for
+ */
+ final String contactLookupKey;
+
+ /**
+ * Which Contacts column this uID is pulled from
+ */
+ static final String COLUMN = ContactsContract.Contacts.LOOKUP_KEY;
+
+ public uID(String lookupKey) {
+ contactLookupKey = lookupKey;
+ }
+
+ public String toString() {
+ return this.contactLookupKey;
+ }
+
+ @Override
+ public int hashCode() {
+ return contactLookupKey.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof uID) {
+ return contactLookupKey.equals(((uID) other).contactLookupKey);
+ }
+ return contactLookupKey.equals(other);
+ }
+ }
+}
diff --git a/src/org/kde/kdeconnect/Helpers/NetworkHelper.java b/src/org/kde/kdeconnect/Helpers/NetworkHelper.java
--- a/src/org/kde/kdeconnect/Helpers/NetworkHelper.java
+++ b/src/org/kde/kdeconnect/Helpers/NetworkHelper.java
@@ -1,14 +1,14 @@
package org.kde.kdeconnect.Helpers;
+import java.io.FileReader;
+import java.io.LineNumberReader;
+
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.util.Log;
-import java.io.FileReader;
-import java.io.LineNumberReader;
-
public class NetworkHelper {
public static boolean isOnMobileNetwork(Context context) {
@@ -55,5 +55,4 @@
}
return false;
}
-
}
diff --git a/src/org/kde/kdeconnect/Helpers/SMSHelper.java b/src/org/kde/kdeconnect/Helpers/SMSHelper.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Helpers/SMSHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2018 Simon Redman
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+package org.kde.kdeconnect.Helpers;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.Telephony;
+import android.support.annotation.RequiresApi;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SMSHelper {
+
+ /**
+ * Get the base address for the SMS content
+ *
+ * If we want to support API < 19, it seems to be possible to read via this query
+ * This is highly undocumented and very likely varies between vendors but appears to work
+ */
+ protected static Uri getSMSURIBad() {
+ return Uri.parse("content://sms/");
+ }
+
+ /**
+ * Get the base address for the SMS content
+ *
+ * Use the new API way which should work on any phone API >= 19
+ */
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ protected static Uri getSMSURIGood() {
+ // TODO: Why not use Telephony.MmsSms.CONTENT_URI?
+ return Telephony.Sms.CONTENT_URI;
+ }
+
+ protected static Uri getSMSUri() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return getSMSURIGood();
+ } else {
+ return getSMSURIBad();
+ }
+ }
+
+ /**
+ * Get the base address for all message conversations
+ */
+ protected static Uri getConversationUri() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return Telephony.MmsSms.CONTENT_CONVERSATIONS_URI;
+ } else {
+ // As with getSMSUriBad, this is potentially unsafe depending on whether a specific
+ // manufacturer decided to do their own thing
+ return Uri.parse("content://mms-sms/conversations");
+ }
+ }
+
+ /**
+ * Get all the messages in a requested thread
+ *
+ * @param context android.content.Context running the request
+ * @param threadID Thread to look up
+ * @return List of all messages in the thread
+ */
+ public static List getMessagesInThread(Context context, ThreadID threadID) {
+ List toReturn = new ArrayList<>();
+
+ Uri smsUri = getSMSUri();
+
+ final String selection = ThreadID.lookupColumn + " == ?";
+ final String[] selectionArgs = new String[] { threadID.toString() };
+
+ Cursor smsCursor = context.getContentResolver().query(
+ smsUri,
+ Message.smsColumns,
+ selection,
+ selectionArgs,
+ null);
+
+ if (smsCursor != null && smsCursor.moveToFirst()) {
+ int threadColumn = smsCursor.getColumnIndexOrThrow(ThreadID.lookupColumn);
+ do {
+ int thread = smsCursor.getInt(threadColumn);
+
+ HashMap messageInfo = new HashMap<>();
+ for (int columnIdx = 0; columnIdx < smsCursor.getColumnCount(); columnIdx++) {
+ String colName = smsCursor.getColumnName(columnIdx);
+ String body = smsCursor.getString(columnIdx);
+ messageInfo.put(colName, body);
+ }
+ toReturn.add(new Message(messageInfo));
+ } while (smsCursor.moveToNext());
+ } else {
+ // No SMSes available?
+ }
+
+ if (smsCursor != null) {
+ smsCursor.close();
+ }
+
+ return toReturn;
+ }
+
+ /**
+ * Get the last message from each conversation. Can use those thread_ids to look up more
+ * messages in those conversations
+ *
+ * @param context android.content.Context running the request
+ * @return Mapping of thread_id to the first message in each thread
+ */
+ public static Map getConversations(Context context) {
+ HashMap toReturn = new HashMap<>();
+
+ Uri conversationUri = getConversationUri();
+
+ Cursor conversationsCursor = context.getContentResolver().query(
+ conversationUri,
+ Message.smsColumns,
+ null,
+ null,
+ null);
+
+ if (conversationsCursor != null && conversationsCursor.moveToFirst()) {
+ int threadColumn = conversationsCursor.getColumnIndexOrThrow(ThreadID.lookupColumn);
+ do {
+ int thread = conversationsCursor.getInt(threadColumn);
+
+ HashMap messageInfo = new HashMap<>();
+ for (int columnIdx = 0; columnIdx < conversationsCursor.getColumnCount(); columnIdx++) {
+ String colName = conversationsCursor.getColumnName(columnIdx);
+ String body = conversationsCursor.getString(columnIdx);
+ messageInfo.put(colName, body);
+ }
+ toReturn.put(new ThreadID(thread), new Message(messageInfo));
+ } while (conversationsCursor.moveToNext());
+ } else {
+ // No conversations available?
+ }
+
+ if (conversationsCursor != null) {
+ conversationsCursor.close();
+ }
+
+ return toReturn;
+ }
+
+ /**
+ * Represent an ID used to uniquely identify a message thread
+ */
+ public static class ThreadID {
+ Integer threadID;
+ static final String lookupColumn = Telephony.Sms.THREAD_ID;
+
+ public ThreadID(Integer threadID) {
+ this.threadID = threadID;
+ }
+
+ public String toString() {
+ return this.threadID.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return this.threadID.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other.getClass().isAssignableFrom(ThreadID.class)) {
+ return ((ThreadID) other).threadID.equals(this.threadID);
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Represent a message and all of its interesting data columns
+ */
+ public static class Message {
+
+ public final String m_address;
+ public final String m_body;
+ public final long m_date;
+ public final int m_type;
+ public final int m_read;
+ public final int m_threadID;
+ public final int m_uID;
+
+ /**
+ * Named constants which are used to construct a Message
+ * See: https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns.html for full documentation
+ */
+ public static final String ADDRESS = Telephony.Sms.ADDRESS; // Contact information (phone number or otherwise) of the remote
+ public static final String BODY = Telephony.Sms.BODY; // Body of the message
+ public static final String DATE = Telephony.Sms.DATE; // Date (Unix epoch millis) associated with the message
+ public static final String TYPE = Telephony.Sms.TYPE; // Compare with Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
+ public static final String READ = Telephony.Sms.READ; // Whether we have received a read report for this message (int)
+ public static final String THREAD_ID = ThreadID.lookupColumn; // Magic number which binds (message) threads
+ public static final String U_ID = Telephony.Sms._ID; // Something which uniquely identifies this message
+
+ /**
+ * Define the columns which are to be extracted from the Android SMS database
+ */
+ public static final String[] smsColumns = new String[]{
+ Message.ADDRESS,
+ Message.BODY,
+ Message.DATE,
+ Message.TYPE,
+ Message.READ,
+ Message.THREAD_ID,
+ Message.U_ID,
+ };
+
+ public Message(final HashMap messageInfo) {
+ m_address = messageInfo.get(Message.ADDRESS);
+ m_body = messageInfo.get(Message.BODY);
+ m_date = Long.parseLong(messageInfo.get(Message.DATE));
+ if (messageInfo.get(Message.TYPE) == null)
+ {
+ // To be honest, I have no idea why this happens. The docs say the TYPE field is mandatory.
+ // Just stick some junk in here and hope we can figure it out later.
+ // Quick investigation suggests that these are multi-target MMSes
+ m_type = -1;
+ } else {
+ m_type = Integer.parseInt(messageInfo.get(Message.TYPE));
+ }
+ m_read = Integer.parseInt(messageInfo.get(Message.READ));
+ m_threadID = Integer.parseInt(messageInfo.get(Message.THREAD_ID));
+ m_uID = Integer.parseInt(messageInfo.get(Message.U_ID));
+ }
+
+ public JSONObject toJSONObject() throws JSONException {
+ JSONObject json = new JSONObject();
+
+ json.put(Message.ADDRESS, m_address);
+ json.put(Message.BODY, m_body);
+ json.put(Message.DATE, m_date);
+ json.put(Message.TYPE, m_type);
+ json.put(Message.READ, m_read);
+ json.put(Message.THREAD_ID, m_threadID);
+ json.put(Message.U_ID, m_uID);
+
+ return json;
+ }
+
+ @Override
+ public String toString() {
+ return this.m_body;
+ }
+ }
+}
+
diff --git a/src/org/kde/kdeconnect/Helpers/TrustedNetworkHelper.java b/src/org/kde/kdeconnect/Helpers/TrustedNetworkHelper.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Helpers/TrustedNetworkHelper.java
@@ -0,0 +1,78 @@
+package org.kde.kdeconnect.Helpers;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import android.content.Context;
+import android.net.wifi.SupplicantState;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+public class TrustedNetworkHelper {
+
+ private static final String KEY_CUSTOM_TRUSTED_NETWORKS = "trusted_network_preference";
+ private static final String KEY_CUSTOM_TRUST_ALL_NETWORKS = "trust_all_network_preference";
+ private static final String NETWORK_SSID_DELIMITER = "#_#";
+ private static final String NOT_AVAILABLE_SSID_RESULT = "";
+
+
+ private final Context context;
+
+ public TrustedNetworkHelper(Context context) {
+ this.context = context;
+ }
+
+ public List read() {
+ String serializeTrustedNetwork = PreferenceManager.getDefaultSharedPreferences(context).getString(
+ KEY_CUSTOM_TRUSTED_NETWORKS, "");
+ if (serializeTrustedNetwork.isEmpty())
+ return Collections.emptyList();
+ return Arrays.asList(serializeTrustedNetwork.split(NETWORK_SSID_DELIMITER));
+ }
+
+ public void update(List trustedNetworks) {
+ String serialized = TextUtils.join(NETWORK_SSID_DELIMITER, trustedNetworks);
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putString(
+ KEY_CUSTOM_TRUSTED_NETWORKS, serialized).apply();
+ }
+
+ public Boolean allAllowed() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .getBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, Boolean.TRUE);
+ }
+
+ public void allAllowed(boolean isChecked) {
+ PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, isChecked)
+ .apply();
+ }
+
+ public String currentSSID() {
+ WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager == null) return "";
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) {
+ return "";
+ }
+ String ssid = wifiInfo.getSSID();
+ if (ssid.equalsIgnoreCase(NOT_AVAILABLE_SSID_RESULT)){
+ return "";
+ }
+ return ssid;
+ }
+
+ public static boolean isNotTrustedNetwork(Context context) {
+ TrustedNetworkHelper trustedNetworkHelper = new TrustedNetworkHelper(context);
+ if (trustedNetworkHelper.allAllowed()){
+ return false;
+ }
+ return trustedNetworkHelper.read().indexOf(trustedNetworkHelper.currentSSID()) == -1;
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.java b/src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/ContactsPlugin/ContactsPlugin.java
@@ -0,0 +1,258 @@
+/*
+ * ContactsPlugin.java - This file is part of KDE Connect's Android App
+ * Implement a way to request and send contact information
+ *
+ * Copyright 2018 Simon Redman
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+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.NetworkPacket;
+import org.kde.kdeconnect.Plugins.Plugin;
+import org.kde.kdeconnect_tp.R;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class ContactsPlugin extends Plugin {
+
+ /**
+ * Used to request the device send the unique ID of every contact
+ */
+ public 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
+ *
+ * It shall contain the key "uids", which will have a list of uIDs (long int, as string)
+ */
+ public 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
+ *
+ * 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
+ */
+ public static final String PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS = "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' )
+ */
+ public static final String PACKET_TYPE_CONTACTS_RESPONSE_VCARDS = "kdeconnect.contacts.response_vcards";
+
+ @Override
+ public String getDisplayName() {
+ return context.getResources().getString(R.string.pref_plugin_contacts);
+ }
+
+ @Override
+ public String getDescription() {
+ return context.getResources().getString(R.string.pref_plugin_contacts_desc);
+ }
+
+ @Override
+ public String[] getSupportedPacketTypes() {
+ return new String[]{
+ PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS,
+ PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS
+ };
+ }
+
+ @Override
+ public String[] getOutgoingPacketTypes() {
+ return new String[]{
+ PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS,
+ PACKET_TYPE_CONTACTS_RESPONSE_VCARDS
+ };
+ }
+
+ @Override
+ public boolean onCreate() {
+ permissionExplanation = R.string.contacts_permission_explanation;
+
+ return true;
+ }
+
+ @Override
+ public boolean isEnabledByDefault() {
+ return true;
+ }
+
+ @Override
+ public String[] getRequiredPermissions() {
+ return new String[]{Manifest.permission.READ_CONTACTS};
+ // One day maybe we will also support WRITE_CONTACTS, but not yet
+ }
+
+ @Override
+ public int getMinSdk() {
+ // Need API 18 for contact timestamps
+ return Build.VERSION_CODES.JELLY_BEAN_MR2;
+ }
+
+ /**
+ * 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
+ * @return The same VCard as was passed in, but now with KDE Connect-specific fields
+ */
+ protected VCardBuilder addVCardMetadata(VCardBuilder vcard, uID uID) {
+ // 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(),
+ uID.toString());
+
+ // Build the timestamp line
+ // Maybe one day this should be changed into the vcard-standard REV key
+ List uIDs = new ArrayList<>();
+ uIDs.add(uID);
+
+ final String[] contactsProjection = new String[]{
+ ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+ };
+
+ Map> timestamp = ContactsHelper.getColumnsFromContactsForIDs(context, uIDs, contactsProjection);
+ vcard.appendLine("X-KDECONNECT-TIMESTAMP",
+ timestamp.get(uID).get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_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 package containing the request
+ * @return true if successfully handled, false otherwise
+ */
+ @SuppressWarnings("SameReturnValue")
+ protected boolean handleRequestAllUIDsTimestamps(@SuppressWarnings("unused") NetworkPacket np) {
+ NetworkPacket reply = new NetworkPacket(PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS);
+
+ List uIDs = ContactsHelper.getAllContactContactIDs(context);
+
+ List uIDsAsStrings = new ArrayList<>(uIDs.size());
+
+ for (uID uID : uIDs) {
+ uIDsAsStrings.add(uID.toString());
+ }
+
+ final String[] contactsProjection = new String[]{
+ ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+ };
+
+ reply.set("uids", uIDsAsStrings);
+
+ // Add last-modified timestamps
+ Map> uIDsToTimestamps = ContactsHelper.getColumnsFromContactsForIDs(context, uIDs, contactsProjection);
+ for (uID ID : uIDsToTimestamps.keySet()) {
+ Map data = uIDsToTimestamps.get(ID);
+ reply.set(ID.toString(), (Integer) data.get(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP));
+ }
+
+ device.sendPacket(reply);
+
+ return true;
+ }
+
+ protected boolean handleRequestVCardsByUIDs(NetworkPacket np) {
+ if (!np.has("uids")) {
+ Log.e("ContactsPlugin", "handleRequestNamesByUIDs received a malformed packet with no uids key");
+ return false;
+ }
+
+ List uIDsAsStrings = np.getStringList("uids");
+
+ // Convert to Collection to call getVCardsForContactIDs
+ Set uIDs = new HashSet<>(uIDsAsStrings.size());
+ for (String uID : uIDsAsStrings) {
+ uIDs.add(new uID(uID));
+ }
+
+ Map 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);
+
+ 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());
+ }
+
+ // Add the valid uIDs to the packet
+ reply.set("uids", uIDsAsStrings);
+
+ device.sendPacket(reply);
+
+ return true;
+ }
+
+ @Override
+ public boolean onPacketReceived(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;
+ }
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/FindRemoteDevicePlugin/FindRemoteDevicePlugin.java b/src/org/kde/kdeconnect/Plugins/FindRemoteDevicePlugin/FindRemoteDevicePlugin.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/FindRemoteDevicePlugin/FindRemoteDevicePlugin.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014 Albert Vaca Cintora
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.kde.kdeconnect.Plugins.FindRemoteDevicePlugin;
+
+import android.app.Activity;
+
+import org.kde.kdeconnect.NetworkPacket;
+import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin;
+import org.kde.kdeconnect.Plugins.Plugin;
+import org.kde.kdeconnect_tp.R;
+
+
+public class FindRemoteDevicePlugin extends Plugin {
+
+ @Override
+ public String getDisplayName() {
+ return context.getResources().getString(R.string.pref_plugin_findremotedevice);
+ }
+
+ @Override
+ public String getDescription() {
+ return context.getResources().getString(R.string.pref_plugin_findremotedevice_desc);
+ }
+
+ @Override
+ public boolean onPacketReceived(NetworkPacket np) {
+ return true;
+ }
+
+ @Override
+ public String getActionName() {
+ return context.getString(R.string.ring);
+ }
+
+ @Override
+ public void startMainActivity(Activity activity) {
+ if (device != null) {
+ device.sendPacket(new NetworkPacket(FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST));
+ }
+ }
+
+ @Override
+ public boolean hasMainActivity() {
+ return true;
+ }
+
+ @Override
+ public boolean displayInContextMenu() {
+ return true;
+ }
+
+ @Override
+ public String[] getSupportedPacketTypes() {
+ return new String[]{};
+ }
+
+ @Override
+ public String[] getOutgoingPacketTypes() {
+ return new String[]{FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST};
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java
--- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java
@@ -45,12 +45,14 @@
private final static float MinDistanceToSendScroll = 2.5f; // touch gesture scroll
private final static float MinDistanceToSendGenericScroll = 0.1f; // real mouse scroll wheel event
+ private final static float StandardDpi = 240.0f; // = hdpi
private float mPrevX;
private float mPrevY;
private float mCurrentX;
private float mCurrentY;
private float mCurrentSensitivity;
+ private float displayDpiMultiplier;
private int scrollDirection = 1;
boolean isScrolling = false;
@@ -112,6 +114,10 @@
doubleTapAction = ClickType.fromString(doubleTapSetting);
tripleTapAction = ClickType.fromString(tripleTapSetting);
+ //Technically xdpi and ydpi should be handled separately,
+ //but since ydpi is usually almost equal to xdpi, only xdpi is used for the multiplier.
+ displayDpiMultiplier = StandardDpi / getResources().getDisplayMetrics().xdpi;
+
switch (sensitivitySetting) {
case "slowest":
mCurrentSensitivity = 0.2f;
@@ -215,7 +221,10 @@
Device device = service.getDevice(deviceId);
MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class);
if (mousePadPlugin == null) return;
- mousePadPlugin.sendMouseDelta(mCurrentX - mPrevX, mCurrentY - mPrevY, mCurrentSensitivity);
+ mousePadPlugin.sendMouseDelta(
+ (mCurrentX - mPrevX) * displayDpiMultiplier,
+ (mCurrentY - mPrevY) * displayDpiMultiplier,
+ mCurrentSensitivity);
mPrevX = mCurrentX;
mPrevY = mCurrentY;
});
diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
--- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
@@ -339,6 +339,11 @@
targetPlayer.next();
}));
+ findViewById(R.id.stop_button).setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> {
+ if (targetPlayer == null) return;
+ targetPlayer.stop();
+ }));
+
((SeekBar) findViewById(R.id.volume_seek)).setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
--- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java
+++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
@@ -27,7 +27,9 @@
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin;
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardPlugin;
+import org.kde.kdeconnect.Plugins.ContactsPlugin.ContactsPlugin;
import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin;
+import org.kde.kdeconnect.Plugins.FindRemoteDevicePlugin.FindRemoteDevicePlugin;
import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin;
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
import org.kde.kdeconnect.Plugins.MprisReceiverPlugin.MprisReceiverPlugin;
@@ -128,8 +130,10 @@
PluginFactory.registerPlugin(TelepathyPlugin.class);
PluginFactory.registerPlugin(FindMyPhonePlugin.class);
PluginFactory.registerPlugin(RunCommandPlugin.class);
+ PluginFactory.registerPlugin(ContactsPlugin.class);
PluginFactory.registerPlugin(RemoteKeyboardPlugin.class);
//PluginFactory.registerPlugin(MprisReceiverPlugin.class);
+ PluginFactory.registerPlugin(FindRemoteDevicePlugin.class);
}
public static PluginInfo getPluginInfo(Context context, String pluginKey) {
diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java
--- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java
@@ -21,15 +21,23 @@
package org.kde.kdeconnect.Plugins.RunCommandPlugin;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.RequiresApi;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
+import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
@@ -41,12 +49,12 @@
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
public class RunCommandActivity extends AppCompatActivity {
private String deviceId;
private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = this::updateView;
+ private ArrayList commandItems;
private void updateView() {
BackgroundService.RunCommand(this, service -> {
@@ -61,7 +69,11 @@
runOnUiThread(() -> {
ListView view = (ListView) findViewById(R.id.runcommandslist);
- final ArrayList commandItems = new ArrayList<>();
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ registerForContextMenu(view);
+ }
+
+ commandItems = new ArrayList<>();
for (JSONObject obj : plugin.getCommandList()) {
try {
commandItems.add(new CommandEntry(obj.getString("name"),
@@ -134,6 +146,28 @@
}
@Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.runcommand_context, menu);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ if (item.getItemId() == R.id.copy_url_to_clipboard) {
+ CommandEntry entry = (CommandEntry) commandItems.get(info.position);
+ String url = "kdeconnect://runcommand/" + deviceId + "/" + entry.getKey();
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setText(url);
+ Toast toast = Toast.makeText(this, R.string.clipboard_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ return false;
+ }
+
+ @Override
protected void onResume() {
super.onResume();
diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandUrlActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandUrlActivity.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandUrlActivity.java
@@ -0,0 +1,74 @@
+package org.kde.kdeconnect.Plugins.RunCommandPlugin;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import org.kde.kdeconnect.BackgroundService;
+import org.kde.kdeconnect.Device;
+import org.kde.kdeconnect_tp.R;
+
+public class RunCommandUrlActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getIntent().getAction() != null) {
+ try {
+ Uri uri = getIntent().getData();
+ String deviceId = uri.getPathSegments().get(0);
+
+ BackgroundService.RunCommand(this, service -> {
+ final Device device = service.getDevice(deviceId);
+
+ if(device == null) {
+ error(R.string.runcommand_nosuchdevice);
+ return;
+ }
+
+ if (!device.isPaired()) {
+ error(R.string.runcommand_notpaired);
+ return;
+ }
+
+ if (!device.isReachable()) {
+ error(R.string.runcommand_notreachable);
+ return;
+ }
+
+ final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
+ if (plugin == null) {
+ error(R.string.runcommand_noruncommandplugin);
+ return;
+ }
+
+ plugin.runCommand(uri.getPathSegments().get(1));
+ RunCommandUrlActivity.this.finish();
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ Vibrator vibrator = RunCommandUrlActivity.this.getSystemService(Vibrator.class);
+ if(vibrator != null && vibrator.hasVibrator()) {
+ vibrator.vibrate(100);
+ }
+ }
+ });
+ } catch (Exception e) {
+ Log.e("RuncommandPlugin", "Exception", e);
+ }
+ }
+ }
+
+ void error(int message) {
+ TextView view = new TextView(this);
+ view.setText(message);
+ view.setGravity(Gravity.CENTER);
+ setContentView(view);
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
@@ -128,8 +128,7 @@
super.onCreate(savedInstanceState);
ThemeUtil.setUserPreferredTheme(this);
- setContentView(R.layout.devices_list);
-
+ setContentView(R.layout.fragment_pair);
ActionBar actionBar = getSupportActionBar();
mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh_list_layout);
diff --git a/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java b/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java
--- a/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java
@@ -28,6 +28,7 @@
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.AudioManager;
+import android.net.Network;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
@@ -37,23 +38,78 @@
import android.telephony.TelephonyManager;
import android.util.Log;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.kde.kdeconnect.Helpers.ContactsHelper;
+import org.kde.kdeconnect.Helpers.SMSHelper;
+import org.kde.kdeconnect.Helpers.SMSHelper.ThreadID;
+import org.kde.kdeconnect.Helpers.SMSHelper.Message;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.BuildConfig;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class TelephonyPlugin extends Plugin {
+ /**
+ * Packet used to indicate a batch of messages has been pushed from the remote device
+ *
+ * The body should contain the key "messages" mapping to an array of messages
+ *
+ * For example:
+ * { "messages" : [
+ * { "event" : "sms",
+ * "messageBody" : "Hello",
+ * "phoneNumber" : "2021234567",
+ * "messageDate" : "1518846484880",
+ * "messageType" : "2",
+ * "threadID" : "132"
+ * },
+ * { ... },
+ * ...
+ * ]
+ */
+ private final static String PACKET_TYPE_TELEPHONY_MESSAGE = "kdeconnect.telephony.message";
+
+ /**
+ * Packet used for simple telephony events
+ *
+ * It contains the key "event" which maps to a string indicating the type of event:
+ * - "ringing" - A phone call is incoming
+ * - "missedCall" - An incoming call was not answered
+ * - "sms" - An incoming SMS message
+ * - Note: As of this writing (15 May 2018) the SMS interface is being improved and this type of event
+ * is no longer the preferred way of retrieving SMS. Use PACKET_TYPE_TELEPHONY_MESSAGE instead.
+ *
+ * Depending on the event, other fields may be defined
+ */
private final static String PACKET_TYPE_TELEPHONY = "kdeconnect.telephony";
public final static String PACKET_TYPE_TELEPHONY_REQUEST = "kdeconnect.telephony.request";
private static final String KEY_PREF_BLOCKED_NUMBERS = "telephony_blocked_numbers";
+ /**
+ * Packet sent to request all conversations
+ *
+ * The request packet shall contain no body
+ */
+ public final static String PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATIONS = "kdeconnect.telephony.request_conversations";
+
+ /**
+ * Packet sent to request all the messages in a particular conversation
+ *
+ * The body should contain the key "threadID" mapping to the threadID (as a string) being requested
+ * For example:
+ * { "threadID": 203 }
+ */
+ public final static String PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATION = "kdeconnect.telephony.request_conversation";
+
private int lastState = TelephonyManager.CALL_STATE_IDLE;
private NetworkPacket lastPacket = null;
private boolean isMuted = false;
@@ -284,6 +340,12 @@
@Override
public boolean onPacketReceived(NetworkPacket np) {
+ if (np.getType().equals(PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATIONS)) {
+ return this.handleRequestConversations(np);
+ }
+ else if (np.getType().equals(PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATION)) {
+ return this.handleRequestConversation(np);
+ }
if (np.getString("action").equals("mute")) {
if (!isMuted) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -311,14 +373,84 @@
return false;
}
+ /**
+ * Respond to a request for all conversations
+ *
+ * Send one packet of type PACKET_TYPE_TELEPHONY_MESSAGE with the first message in all conversations
+ */
+ protected boolean handleRequestConversations(NetworkPacket packet) {
+ Map conversations = SMSHelper.getConversations(this.context);
+
+ NetworkPacket reply = new NetworkPacket(PACKET_TYPE_TELEPHONY_MESSAGE);
+
+ JSONArray messages = new JSONArray();
+
+ for (Message message : conversations.values()) {
+ try {
+ JSONObject json = message.toJSONObject();
+
+ json.put("event", "sms");
+
+ messages.put(json);
+ } catch (JSONException e)
+ {
+ Log.e("Conversations", "Error serializing message");
+ }
+ }
+
+ reply.set("messages", messages);
+ reply.set("event", "batch_messages"); // Not really necessary, since this is implied by PACKET_TYPE_TELEPHONY_MESSAGE, but good for readability
+
+ device.sendPacket(reply);
+
+ return true;
+ }
+
+ protected boolean handleRequestConversation(NetworkPacket packet) {
+ ThreadID threadID = new ThreadID(packet.getInt("threadID"));
+
+ List conversation = SMSHelper.getMessagesInThread(this.context, threadID);
+
+ NetworkPacket reply = new NetworkPacket(PACKET_TYPE_TELEPHONY_MESSAGE);
+
+ JSONArray messages = new JSONArray();
+
+ for (Message message : conversation) {
+ try {
+ JSONObject json = message.toJSONObject();
+
+ json.put("event", "sms");
+
+ messages.put(json);
+ } catch (JSONException e)
+ {
+ Log.e("Conversations", "Error serializing message");
+ }
+ }
+
+ reply.set("messages", messages);
+ reply.set("event", "batch_messages");
+
+ device.sendPacket(reply);
+
+ return true;
+ }
+
@Override
public String[] getSupportedPacketTypes() {
- return new String[]{PACKET_TYPE_TELEPHONY_REQUEST};
+ return new String[]{
+ PACKET_TYPE_TELEPHONY_REQUEST,
+ PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATIONS,
+ PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATION,
+ };
}
@Override
public String[] getOutgoingPacketTypes() {
- return new String[]{PACKET_TYPE_TELEPHONY};
+ return new String[]{
+ PACKET_TYPE_TELEPHONY,
+ PACKET_TYPE_TELEPHONY_MESSAGE,
+ };
}
@Override
diff --git a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java
--- a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java
+++ b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java
@@ -75,7 +75,7 @@
setHasOptionsMenu(true);
- rootView = inflater.inflate(R.layout.devices_list, container, false);
+ rootView = inflater.inflate(R.layout.fragment_pair, container, false);
View listRootView = rootView.findViewById(R.id.devices_list);
mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_list_layout);
mSwipeRefreshLayout.setOnRefreshListener(
@@ -243,6 +243,9 @@
case R.id.menu_custom_device_list:
startActivity(new Intent(mActivity, CustomDevicesActivity.class));
break;
+ case R.id.menu_trusted_networks:
+ startActivity(new Intent(mActivity, TrustedNetworksActivity.class));
+ break;
default:
break;
}
diff --git a/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java b/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java
@@ -0,0 +1,145 @@
+package org.kde.kdeconnect.UserInterface;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.kde.kdeconnect.BackgroundService;
+import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
+import org.kde.kdeconnect_tp.R;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.*;
+
+public class TrustedNetworksActivity extends AppCompatActivity {
+
+ private ListView trustedNetworksView;
+ private List trustedNetworks;
+
+ private boolean dialogAlreadyShown = false;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ TrustedNetworkHelper trustedNetworkHelper = new TrustedNetworkHelper(getApplicationContext());
+ super.onCreate(savedInstanceState);
+ trustedNetworks = new ArrayList<>(trustedNetworkHelper.read());
+ ThemeUtil.setUserPreferredTheme(this);
+ setContentView(R.layout.trusted_network_list);
+ trustedNetworksView = (ListView) findViewById(android.R.id.list);
+ emptyListMessage(trustedNetworkHelper);
+
+ trustedNetworkListView(trustedNetworkHelper);
+
+ CheckBox allowAllCheckBox = (CheckBox) findViewById(R.id.trust_all_networks_checkBox);
+ allowAllCheckBox.setChecked(trustedNetworkHelper.allAllowed());
+ allowAllCheckBox.setOnCheckedChangeListener((v, isChecked) -> {
+ trustedNetworkHelper.allAllowed(isChecked);
+ trustedNetworkListView(trustedNetworkHelper);
+ });
+
+ }
+
+ private void emptyListMessage(TrustedNetworkHelper trustedNetworkHelper) {
+ boolean isVisible = trustedNetworks.isEmpty() && !trustedNetworkHelper.allAllowed();
+ findViewById(R.id.trusted_network_list_empty)
+ .setVisibility(isVisible ? VISIBLE : GONE );
+ }
+
+ private void trustedNetworkListView(TrustedNetworkHelper trustedNetworkHelper) {
+ Boolean allAllowed = trustedNetworkHelper.allAllowed();
+ emptyListMessage(trustedNetworkHelper);
+ trustedNetworksView.setVisibility(allAllowed ? GONE : VISIBLE);
+ if (allAllowed){
+ return;
+ }
+ trustedNetworksView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, trustedNetworks));
+ trustedNetworksView.setOnItemClickListener(onItemClickGenerator(trustedNetworkHelper));
+ addNetworkButton(trustedNetworkHelper);
+ }
+
+ @NonNull
+ private AdapterView.OnItemClickListener onItemClickGenerator(TrustedNetworkHelper trustedNetworkHelper) {
+ return (parent, view, position, id) -> {
+ if (dialogAlreadyShown) {
+ return;
+ }
+ String targetItem = trustedNetworks.get(position);
+
+ // remove touched item after confirmation
+ DialogInterface.OnClickListener confirmationListener = (dialog, which) -> {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ trustedNetworks.remove(position);
+ trustedNetworkHelper.update(trustedNetworks);
+ ((ArrayAdapter) trustedNetworksView.getAdapter()).notifyDataSetChanged();
+ addNetworkButton(trustedNetworkHelper);
+ emptyListMessage(trustedNetworkHelper);
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ break;
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(TrustedNetworksActivity.this);
+ builder.setMessage("Delete " + targetItem + " ?");
+ builder.setPositiveButton("Yes", confirmationListener);
+ builder.setNegativeButton("No", confirmationListener);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //DismissListener
+ dialogAlreadyShown = true;
+ builder.setOnDismissListener(dialog -> dialogAlreadyShown = false);
+ }
+
+ builder.show();
+ };
+ }
+
+ private void addNetworkButton(TrustedNetworkHelper trustedNetworkHelper) {
+ String currentSSID = trustedNetworkHelper.currentSSID();
+ if (!currentSSID.isEmpty() && trustedNetworks.indexOf(currentSSID) == -1) {
+ Button addButton = (Button) findViewById(android.R.id.button1);
+ String buttonText = getString(R.string.add_trusted_network, currentSSID);
+ addButton.setText(buttonText);
+ addButton.setOnClickListener(saveCurrentSSIDAsTrustedNetwork(currentSSID, trustedNetworkHelper));
+ addButton.setVisibility(VISIBLE);
+ }
+ }
+
+
+ @NonNull
+ private View.OnClickListener saveCurrentSSIDAsTrustedNetwork(String ssid, TrustedNetworkHelper trustedNetworkHelper) {
+ return v -> {
+ if (trustedNetworks.indexOf(ssid) != -1){
+ return;
+ }
+ trustedNetworks.add(ssid);
+ trustedNetworkHelper.update(trustedNetworks);
+ ((ArrayAdapter) trustedNetworksView.getAdapter()).notifyDataSetChanged();
+ v.setVisibility(GONE);
+ };
+ }
+
+
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ BackgroundService.addGuiInUseCounter(this);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ BackgroundService.removeGuiInUseCounter(this);
+ }
+
+}