diff --git a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java index 5f2a40e2..899ad152 100644 --- a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java +++ b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java @@ -1,438 +1,445 @@ /* * Copyright 2014 Albert Vaca Cintora * 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.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.provider.ContactsContract.PhoneLookup; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.collection.LongSparseArray; public class ContactsHelper { /** * Lookup the name and photoID of a contact given a phone number */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static Map phoneNumberLookup(Context context, String number) { Map contactInfo = new HashMap<>(); Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); String[] columns = new String[]{ PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI /*, PhoneLookup.TYPE , PhoneLookup.LABEL , PhoneLookup.ID */ }; try (Cursor cursor = context.getContentResolver().query(uri, columns,null, null, null)) { // Take the first match only if (cursor != null && cursor.moveToFirst()) { int nameIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); if (nameIndex != -1) { contactInfo.put("name", cursor.getString(nameIndex)); } nameIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); if (nameIndex != -1) { contactInfo.put("photoID", cursor.getString(nameIndex)); } } } catch (Exception ignored) { } return contactInfo; } public static String photoId64Encoded(Context context, String photoId) { if (photoId == null) { return ""; } Uri photoUri = Uri.parse(photoId); ByteArrayOutputStream encodedPhoto = new ByteArrayOutputStream(); try (InputStream input = context.getContentResolver().openInputStream(photoUri); Base64OutputStream output = new Base64OutputStream(encodedPhoto, Base64.DEFAULT)) { byte[] buffer = new byte[1024]; int len; //noinspection ConstantConditions while ((len = input.read(buffer)) != -1) { output.write(buffer, 0, len); } return encodedPhoto.toString(); } catch (Exception ex) { Log.e("ContactsHelper", ex.toString()); return ""; } } /** * 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[] columns = new String[]{ ContactsContract.Contacts.LOOKUP_KEY }; Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; try (Cursor contactsCursor = context.getContentResolver().query(contactsUri, columns, 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()); } } 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())); ; StringBuilder vcardJumble = new StringBuilder(); try (BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(context.getContentResolver().openInputStream(vcardURI)))) { 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 Log.e("Contacts", "Exception while fetching vcards", e); } // 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") private 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); try (InputStream input = context.getContentResolver().openInputStream(vcardURI)) { if (input == null) { throw new NullPointerException("ContentResolver did not give us a stream for the VCard for uID " + ID); } try (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())); } } catch (IOException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } catch (NullPointerException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } } 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<>(); if (IDs.isEmpty()) { return toReturn; } 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()); } try (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()); } } 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 { static final String VCARD_END = "END:VCARD"; // Written to terminate the vcard 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 */ 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"); } + @NonNull 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) { + + if (lookupKey == null) + throw new IllegalArgumentException("lookUpKey should not be null"); + contactLookupKey = lookupKey; } + @NonNull 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/Plugins/SftpPlugin/RootFile.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java index fa94245a..a6eea5dc 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java @@ -1,177 +1,178 @@ /* * Copyright 2018 Erik Duisters * * 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.SftpPlugin; import org.apache.sshd.common.file.SshFile; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Calendar; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; //TODO: ls .. and ls / only show .. and / respectively I would expect a listing //TODO: cd .. to / does not work and prints "Can't change directory: Can't check target" class RootFile implements SshFile { private final boolean exists; private final String userName; private final List files; RootFile(List files, String userName, boolean exits) { this.files = files; this.userName = userName; this.exists = exits; } public String getAbsolutePath() { return "/"; } public String getName() { return "/"; } public Map getAttributes(boolean followLinks) { Map attrs = new HashMap<>(); attrs.put(Attribute.Size, 0); attrs.put(Attribute.Owner, userName); attrs.put(Attribute.Group, userName); EnumSet p = EnumSet.noneOf(Permission.class); p.add(Permission.UserExecute); p.add(Permission.GroupExecute); p.add(Permission.OthersExecute); attrs.put(Attribute.Permissions, p); long now = Calendar.getInstance().getTimeInMillis(); attrs.put(Attribute.LastAccessTime, now); attrs.put(Attribute.LastModifiedTime, now); attrs.put(Attribute.IsSymbolicLink, false); attrs.put(Attribute.IsDirectory, true); attrs.put(Attribute.IsRegularFile, false); return attrs; } public void setAttributes(Map attributes) { } public Object getAttribute(Attribute attribute, boolean followLinks) { return null; } public void setAttribute(Attribute attribute, Object value) { } public String readSymbolicLink() { return ""; } public void createSymbolicLink(SshFile destination) { } public String getOwner() { return null; } public boolean isDirectory() { return true; } public boolean isFile() { return false; } public boolean doesExist() { return exists; } public boolean isReadable() { return true; } public boolean isWritable() { return false; } public boolean isExecutable() { return true; } public boolean isRemovable() { return false; } public SshFile getParentFile() { return this; } public long getLastModified() { return 0; } public boolean setLastModified(long time) { return false; } public long getSize() { return 0; } public boolean mkdir() { return false; } public boolean delete() { return false; } public boolean create() { return false; } public void truncate() { } public boolean move(SshFile destination) { return false; } public List listSshFiles() { return Collections.unmodifiableList(files); } public OutputStream createOutputStream(long offset) { return null; } public InputStream createInputStream(long offset) { return null; } public void handleClose() { } }