diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index e3919fae6..c58e4675d 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -287,6 +287,10 @@ android { exclude 'META-INF/NOTICE' exclude '.readme' } + + dataBinding { + enabled = true + } } task jacocoTestReport(type:JacocoReport, dependsOn: "testFdroidDebugWithTestCoverageUnitTest") { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java index 18cbd4561..a1b2289f4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java @@ -17,6 +17,8 @@ package org.sufficientlysecure.keychain.keyimport; +import android.support.annotation.NonNull; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableProxy; @@ -26,8 +28,6 @@ import java.net.Proxy; import java.util.ArrayList; import java.util.Vector; -import android.support.annotation.NonNull; - /** * Search two or more types of server for online keys. */ @@ -38,8 +38,8 @@ public class CloudSearch { public static ArrayList search( @NonNull final String query, Preferences.CloudSearchPrefs cloudPrefs, @NonNull final ParcelableProxy proxy) throws Keyserver.CloudSearchFailureException { - final ArrayList servers = new ArrayList<>(); + final ArrayList servers = new ArrayList<>(); // it's a Vector for sync, multiple threads might report problems final Vector problems = new Vector<>(); @@ -52,46 +52,48 @@ public class CloudSearch { if (cloudPrefs.searchFacebook) { servers.add(new FacebookKeyserver()); } - final ImportKeysList results = new ImportKeysList(servers.size()); - ArrayList searchThreads = new ArrayList<>(); - for (final Keyserver keyserver : servers) { - Runnable r = new Runnable() { - @Override - public void run() { - try { - results.addAll(keyserver.search(query, proxy)); - } catch (Keyserver.CloudSearchFailureException e) { - problems.add(e); + int numberOfServers = servers.size(); + final ImportKeysList results = new ImportKeysList(numberOfServers); + + if (numberOfServers > 0) { + ArrayList searchThreads = new ArrayList<>(); + for (final Keyserver keyserver : servers) { + Runnable r = new Runnable() { + @Override + public void run() { + try { + results.addAll(keyserver.search(query, proxy)); + } catch (Keyserver.CloudSearchFailureException e) { + problems.add(e); + } + results.finishedAdding(); // notifies if all searchers done } - results.finishedAdding(); // notifies if all searchers done - } - }; - Thread searchThread = new Thread(r); - searchThreads.add(searchThread); - searchThread.start(); - } - - // wait for either all the searches to come back, or 10 seconds. If using proxy, wait 30 seconds. - synchronized (results) { - try { - if (proxy.getProxy() == Proxy.NO_PROXY) { - results.wait(30 * SECONDS); - } else { - results.wait(10 * SECONDS); - } - for (Thread thread : searchThreads) { - // kill threads that haven't returned yet - thread.interrupt(); - } - } catch (InterruptedException ignored) { + }; + Thread searchThread = new Thread(r); + searchThreads.add(searchThread); + searchThread.start(); } - } - if (results.outstandingSuppliers() > 0) { - String message = "Launched " + servers.size() + " cloud searchers, but " + - results.outstandingSuppliers() + "failed to complete."; - problems.add(new Keyserver.QueryFailedException(message)); + // wait for either all the searches to come back, or 10 seconds. If using proxy, wait 30 seconds. + synchronized (results) { + try { + results.wait((proxy.getProxy() == Proxy.NO_PROXY ? 30 : 10) * SECONDS); + for (Thread thread : searchThreads) { + // kill threads that haven't returned yet + thread.interrupt(); + } + } catch (InterruptedException ignored) { + } + } + + if (results.outstandingSuppliers() > 0) { + String message = "Launched " + servers.size() + " cloud searchers, but " + + results.outstandingSuppliers() + "failed to complete."; + problems.add(new Keyserver.QueryFailedException(message)); + } + } else { + problems.add(new Keyserver.QueryNoEnabledSourceException()); } if (!problems.isEmpty()) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java index 661b50424..0ab6a6941 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java @@ -23,11 +23,6 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; @@ -40,13 +35,16 @@ import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.TlsHelper; import java.io.IOException; - import java.net.Proxy; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + public class FacebookKeyserver extends Keyserver { private static final String FB_KEY_URL_FORMAT @@ -148,7 +146,6 @@ public class FacebookKeyserver extends Keyserver { throws UnsupportedOperationException { ImportKeysListEntry entry = new ImportKeysListEntry(); entry.setSecretKey(false); // keys imported from Facebook must be public - entry.addOrigin(ORIGIN); // so we can query for the Facebook username directly, and to identify the source to // download the key from @@ -156,17 +153,11 @@ public class FacebookKeyserver extends Keyserver { UncachedPublicKey key = ring.getPublicKey(); - entry.setPrimaryUserId(key.getPrimaryUserIdWithFallback()); entry.setUserIds(key.getUnorderedUserIds()); - entry.updateMergedUserIds(); - entry.setPrimaryUserId(key.getPrimaryUserIdWithFallback()); entry.setKeyId(key.getKeyId()); - entry.setKeyIdHex(KeyFormattingUtils.convertKeyIdToHex(key.getKeyId())); - - entry.setFingerprintHex(KeyFormattingUtils.convertFingerprintToHex(key.getFingerprint())); - + entry.setFingerprint(key.getFingerprint()); try { if (key.isEC()) { // unsupported key format (ECDH or ECDSA) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysList.java index 75a219191..2e7872b82 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysList.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysList.java @@ -70,44 +70,25 @@ public class ImportKeysList extends ArrayList { modified = true; } - // keep track if this key result is from a HKP keyserver - boolean incomingFromHkpServer = true; - // we’re going to want to try to fetch the key from everywhere we found it, so remember - // all the origins - for (String origin : incoming.getOrigins()) { - existing.addOrigin(origin); + if (incoming.getKeyserver() != null) { + existing.setKeyserver(incoming.getKeyserver()); + // Mail addresses returned by HKP servers are preferred over keybase.io IDs + existing.setPrimaryUserId(incoming.getPrimaryUserId()); + modified = true; + } else if (incoming.getKeybaseName() != null) { // to work properly, Keybase-sourced/Facebook-sourced entries need to pass along the // identifying name/id - if (incoming.getKeybaseName() != null) { - existing.setKeybaseName(incoming.getKeybaseName()); - // one of the origins is not a HKP keyserver - incomingFromHkpServer = false; - } - if (incoming.getFbUsername() != null) { - existing.setFbUsername(incoming.getFbUsername()); - // one of the origins is not a HKP keyserver - incomingFromHkpServer = false; - } + existing.setKeybaseName(incoming.getKeybaseName()); + modified = true; + } else if (incoming.getFbUsername() != null) { + existing.setFbUsername(incoming.getFbUsername()); + modified = true; } - ArrayList incomingIDs = incoming.getUserIds(); - ArrayList existingIDs = existing.getUserIds(); - for (String incomingID : incomingIDs) { - if (!existingIDs.contains(incomingID)) { - // prepend HKP server results to the start of the list, - // so that the UI (for cloud key search, which is picking the first list item) - // shows the right main email address, as mail addresses returned by HKP servers - // are preferred over keybase.io IDs - if (incomingFromHkpServer) { - existingIDs.add(0, incomingID); - } else { - existingIDs.add(incomingID); - } - modified = true; - } - } - existing.updateMergedUserIds(); + if (existing.addUserIds(incoming.getUserIds())) + modified = true; + return modified; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java index c0cc9e2a3..89c3fcfb1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java @@ -21,108 +21,59 @@ import android.content.Context; import android.os.Parcel; import android.os.Parcelable; -import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.R; +import org.openintents.openpgp.util.OpenPgpUtils.UserId; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; public class ImportKeysListEntry implements Serializable, Parcelable { private static final long serialVersionUID = -7797972103284992662L; + private ParcelableKeyRing mParcelableKeyRing; + private ArrayList mUserIds; private HashMap> mMergedUserIds; - private long mKeyId; + private ArrayList>> mSortedUserIds; + private String mKeyIdHex; + + private boolean mSecretKey; private boolean mRevoked; private boolean mExpired; - private Date mDate; // TODO: not displayed + private boolean mUpdated; + + private Date mDate; private String mFingerprintHex; private Integer mBitStrength; private String mCurveOid; private String mAlgorithm; - private boolean mSecretKey; - private String mPrimaryUserId; + + private UserId mPrimaryUserId; + private ParcelableHkpKeyserver mKeyserver; private String mKeybaseName; private String mFbUsername; + private String mQuery; - private ArrayList mOrigins; private Integer mHashCode = null; - private boolean mSelected; - - public int describeContents() { - return 0; + public ParcelableKeyRing getParcelableKeyRing() { + return mParcelableKeyRing; } - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mPrimaryUserId); - dest.writeStringList(mUserIds); - dest.writeSerializable(mMergedUserIds); - dest.writeLong(mKeyId); - dest.writeByte((byte) (mRevoked ? 1 : 0)); - dest.writeByte((byte) (mExpired ? 1 : 0)); - dest.writeInt(mDate == null ? 0 : 1); - if (mDate != null) { - dest.writeLong(mDate.getTime()); - } - dest.writeString(mFingerprintHex); - dest.writeString(mKeyIdHex); - dest.writeInt(mBitStrength == null ? 0 : 1); - if (mBitStrength != null) { - dest.writeInt(mBitStrength); - } - dest.writeString(mAlgorithm); - dest.writeByte((byte) (mSecretKey ? 1 : 0)); - dest.writeByte((byte) (mSelected ? 1 : 0)); - dest.writeString(mKeybaseName); - dest.writeString(mFbUsername); - dest.writeStringList(mOrigins); - } - - public static final Creator CREATOR = new Creator() { - public ImportKeysListEntry createFromParcel(final Parcel source) { - ImportKeysListEntry vr = new ImportKeysListEntry(); - vr.mPrimaryUserId = source.readString(); - vr.mUserIds = new ArrayList<>(); - source.readStringList(vr.mUserIds); - vr.mMergedUserIds = (HashMap>) source.readSerializable(); - vr.mKeyId = source.readLong(); - vr.mRevoked = source.readByte() == 1; - vr.mExpired = source.readByte() == 1; - vr.mDate = source.readInt() != 0 ? new Date(source.readLong()) : null; - vr.mFingerprintHex = source.readString(); - vr.mKeyIdHex = source.readString(); - vr.mBitStrength = source.readInt() != 0 ? source.readInt() : null; - vr.mAlgorithm = source.readString(); - vr.mSecretKey = source.readByte() == 1; - vr.mSelected = source.readByte() == 1; - vr.mKeybaseName = source.readString(); - vr.mFbUsername = source.readString(); - vr.mOrigins = new ArrayList<>(); - source.readStringList(vr.mOrigins); - - return vr; - } - - public ImportKeysListEntry[] newArray(final int size) { - return new ImportKeysListEntry[size]; - } - }; - - public int hashCode() { - if (mHashCode != null) { - return mHashCode; - } - return super.hashCode(); + public void setParcelableKeyRing(ParcelableKeyRing parcelableKeyRing) { + this.mParcelableKeyRing = parcelableKeyRing; } public boolean hasSameKeyAs(ImportKeysListEntry other) { @@ -136,12 +87,12 @@ public class ImportKeysListEntry implements Serializable, Parcelable { return mKeyIdHex; } - public boolean isSelected() { - return mSelected; + public void setKeyIdHex(String keyIdHex) { + mKeyIdHex = keyIdHex; } - public void setSelected(boolean selected) { - mSelected = selected; + public void setKeyId(long keyId) { + mKeyIdHex = KeyFormattingUtils.convertKeyIdToHex(keyId); } public boolean isExpired() { @@ -152,18 +103,6 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mExpired = expired; } - public long getKeyId() { - return mKeyId; - } - - public void setKeyId(long keyId) { - mKeyId = keyId; - } - - public void setKeyIdHex(String keyIdHex) { - mKeyIdHex = keyIdHex; - } - public boolean isRevoked() { return mRevoked; } @@ -172,10 +111,22 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mRevoked = revoked; } + public boolean isRevokedOrExpired() { + return mRevoked || mExpired; + } + public Date getDate() { return mDate; } + public boolean isUpdated() { + return mUpdated; + } + + public void setUpdated(boolean updated) { + mUpdated = updated; + } + public void setDate(Date date) { mDate = date; } @@ -188,6 +139,10 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mFingerprintHex = fingerprintHex; } + public void setFingerprint(byte[] fingerprint) { + mFingerprintHex = KeyFormattingUtils.convertFingerprintToHex(fingerprint); + } + public Integer getBitStrength() { return mBitStrength; } @@ -216,35 +171,38 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mSecretKey = secretKey; } - public ArrayList getUserIds() { - return mUserIds; - } - - public void setUserIds(ArrayList userIds) { - mUserIds = userIds; - updateMergedUserIds(); - } - - public String getPrimaryUserId() { + public UserId getPrimaryUserId() { return mPrimaryUserId; } - public void setPrimaryUserId(String uid) { - mPrimaryUserId = uid; + public void setPrimaryUserId(String userId) { + mPrimaryUserId = KeyRing.splitUserId(userId); + } + + public void setPrimaryUserId(UserId primaryUserId) { + mPrimaryUserId = primaryUserId; + } + + public ParcelableHkpKeyserver getKeyserver() { + return mKeyserver; + } + + public void setKeyserver(ParcelableHkpKeyserver keyserver) { + mKeyserver = keyserver; } public String getKeybaseName() { return mKeybaseName; } - public String getFbUsername() { - return mFbUsername; - } - public void setKeybaseName(String keybaseName) { mKeybaseName = keybaseName; } + public String getFbUsername() { + return mFbUsername; + } + public void setFbUsername(String fbUsername) { mFbUsername = fbUsername; } @@ -257,16 +215,49 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mQuery = query; } - public ArrayList getOrigins() { - return mOrigins; + public int hashCode() { + return mHashCode != null ? mHashCode : super.hashCode(); } - public void addOrigin(String origin) { - mOrigins.add(origin); + public List getUserIds() { + // To ensure choerency, use methods of this class to edit the list + return Collections.unmodifiableList(mUserIds); } - public HashMap> getMergedUserIds() { - return mMergedUserIds; + public ArrayList>> getSortedUserIds() { + if (mSortedUserIds == null) + sortMergedUserIds(); + + return mSortedUserIds; + } + + public void setUserIds(ArrayList userIds) { + mUserIds = userIds; + updateMergedUserIds(); + } + + public boolean addUserIds(List userIds) { + boolean modified = false; + for (String uid : userIds) { + if (!mUserIds.contains(uid)) { + mUserIds.add(uid); + modified = true; + } + } + + if (modified) + updateMergedUserIds(); + + return modified; + } + + public ArrayList getKeybaseUserIds() { + ArrayList keybaseUserIds = new ArrayList<>(); + for (String s : mUserIds) { + if (s.contains(":")) + keybaseUserIds.add(s); + } + return keybaseUserIds; } /** @@ -275,51 +266,44 @@ public class ImportKeysListEntry implements Serializable, Parcelable { public ImportKeysListEntry() { // keys from keyserver are always public keys; from keybase too mSecretKey = false; - // do not select by default - mSelected = false; + mUserIds = new ArrayList<>(); - mOrigins = new ArrayList<>(); } /** * Constructor based on key object, used for import from NFC, QR Codes, files */ - @SuppressWarnings("unchecked") - public ImportKeysListEntry(Context context, UncachedKeyRing ring) { - // selected is default - this.mSelected = true; - + public ImportKeysListEntry(Context ctx, UncachedKeyRing ring) { mSecretKey = ring.isSecret(); + UncachedPublicKey key = ring.getPublicKey(); - - mHashCode = key.hashCode(); - - mPrimaryUserId = key.getPrimaryUserIdWithFallback(); - mUserIds = key.getUnorderedUserIds(); - updateMergedUserIds(); - - // if there was no user id flagged as primary, use the first one - if (mPrimaryUserId == null) { - mPrimaryUserId = context.getString(R.string.user_id_none); - } - - mKeyId = key.getKeyId(); - mKeyIdHex = KeyFormattingUtils.convertKeyIdToHex(mKeyId); + setPrimaryUserId(key.getPrimaryUserIdWithFallback()); + setKeyId(key.getKeyId()); + setFingerprint(key.getFingerprint()); // NOTE: Dont use maybe methods for now, they can be wrong. mRevoked = false; //key.isMaybeRevoked(); mExpired = false; //key.isMaybeExpired(); - mFingerprintHex = KeyFormattingUtils.convertFingerprintToHex(key.getFingerprint()); + mBitStrength = key.getBitStrength(); mCurveOid = key.getCurveOid(); - final int algorithm = key.getAlgorithm(); - mAlgorithm = KeyFormattingUtils.getAlgorithmInfo(context, algorithm, mBitStrength, mCurveOid); + int algorithm = key.getAlgorithm(); + mAlgorithm = KeyFormattingUtils.getAlgorithmInfo(ctx, algorithm, mBitStrength, mCurveOid); + mHashCode = key.hashCode(); + + setUserIds(key.getUnorderedUserIds()); + + try { + byte[] encoded = ring.getEncoded(); + mParcelableKeyRing = new ParcelableKeyRing(encoded); + } catch (IOException ignored) { + } } - public void updateMergedUserIds() { + private void updateMergedUserIds() { mMergedUserIds = new HashMap<>(); for (String userId : mUserIds) { - OpenPgpUtils.UserId userIdSplit = KeyRing.splitUserId(userId); + UserId userIdSplit = KeyRing.splitUserId(userId); // TODO: comment field? @@ -341,6 +325,87 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mMergedUserIds.put(userId, new HashSet()); } } + + mSortedUserIds = null; + } + + private void sortMergedUserIds() { + mSortedUserIds = new ArrayList<>(mMergedUserIds.entrySet()); + + Collections.sort(mSortedUserIds, new Comparator>>() { + @Override + public int compare(Map.Entry> entry1, + Map.Entry> entry2) { + + // sort keybase UserIds after non-Keybase + boolean e1IsKeybase = entry1.getKey().contains(":"); + boolean e2IsKeybase = entry2.getKey().contains(":"); + if (e1IsKeybase != e2IsKeybase) { + return (e1IsKeybase) ? 1 : -1; + } + return entry1.getKey().compareTo(entry2.getKey()); + } + }); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mParcelableKeyRing, flags); + dest.writeSerializable(mPrimaryUserId); + dest.writeStringList(mUserIds); + dest.writeSerializable(mMergedUserIds); + dest.writeByte((byte) (mRevoked ? 1 : 0)); + dest.writeByte((byte) (mExpired ? 1 : 0)); + dest.writeByte((byte) (mUpdated ? 1 : 0)); + dest.writeInt(mDate == null ? 0 : 1); + if (mDate != null) { + dest.writeLong(mDate.getTime()); + } + dest.writeString(mFingerprintHex); + dest.writeString(mKeyIdHex); + dest.writeInt(mBitStrength == null ? 0 : 1); + if (mBitStrength != null) { + dest.writeInt(mBitStrength); + } + dest.writeString(mAlgorithm); + dest.writeByte((byte) (mSecretKey ? 1 : 0)); + dest.writeParcelable(mKeyserver, flags); + dest.writeString(mKeybaseName); + dest.writeString(mFbUsername); + } + + public static final Creator CREATOR = new Creator() { + public ImportKeysListEntry createFromParcel(final Parcel source) { + ImportKeysListEntry vr = new ImportKeysListEntry(); + + vr.mParcelableKeyRing = source.readParcelable(ParcelableKeyRing.class.getClassLoader()); + vr.mPrimaryUserId = (UserId) source.readSerializable(); + vr.mUserIds = new ArrayList<>(); + source.readStringList(vr.mUserIds); + vr.mMergedUserIds = (HashMap>) source.readSerializable(); + vr.mRevoked = source.readByte() == 1; + vr.mExpired = source.readByte() == 1; + vr.mUpdated = source.readByte() == 1; + vr.mDate = source.readInt() != 0 ? new Date(source.readLong()) : null; + vr.mFingerprintHex = source.readString(); + vr.mKeyIdHex = source.readString(); + vr.mBitStrength = source.readInt() != 0 ? source.readInt() : null; + vr.mAlgorithm = source.readString(); + vr.mSecretKey = source.readByte() == 1; + vr.mKeyserver = source.readParcelable(ParcelableHkpKeyserver.class.getClassLoader()); + vr.mKeybaseName = source.readString(); + vr.mFbUsername = source.readString(); + + return vr; + } + + public ImportKeysListEntry[] newArray(final int size) { + return new ImportKeysListEntry[size]; + } + }; + + public int describeContents() { + return 0; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java index 6a87167fd..9a28ba900 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java @@ -18,8 +18,8 @@ package org.sufficientlysecure.keychain.keyimport; import com.textuality.keybase.lib.KeybaseException; -import com.textuality.keybase.lib.Match; import com.textuality.keybase.lib.KeybaseQuery; +import com.textuality.keybase.lib.Match; import com.textuality.keybase.lib.User; import org.sufficientlysecure.keychain.Constants; @@ -32,7 +32,6 @@ import java.util.ArrayList; import java.util.List; public class KeybaseKeyserver extends Keyserver { - public static final String ORIGIN = "keybase:keybase.io"; public KeybaseKeyserver() { } @@ -68,7 +67,6 @@ public class KeybaseKeyserver extends Keyserver { private ImportKeysListEntry makeEntry(Match match, String query) throws KeybaseException { final ImportKeysListEntry entry = new ImportKeysListEntry(); entry.setQuery(query); - entry.addOrigin(ORIGIN); entry.setRevoked(false); // keybase doesn’t say anything about revoked keys diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java index fd28c0221..f5970ef8a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java @@ -66,6 +66,10 @@ public abstract class Keyserver { private static final long serialVersionUID = 2703768928624654518L; } + public static class QueryNoEnabledSourceException extends QueryNeedsRepairException { + private static final long serialVersionUID = 2703768928624654519L; + } + public static class AddKeyException extends Exception { private static final long serialVersionUID = -507574859137295530L; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java index 25e2d0699..143818122 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java @@ -22,6 +22,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; @@ -269,7 +270,6 @@ public class ParcelableHkpKeyserver extends Keyserver implements Parcelable { while (matcher.find()) { final ImportKeysListEntry entry = new ImportKeysListEntry(); entry.setQuery(query); - entry.addOrigin(getHostID()); // group 1 contains the full fingerprint (v4) or the long key id if available // see https://bitbucket.org/skskeyserver/sks-keyserver/pull-request/12/fixes-for-machine-readable-indexes/diff @@ -293,10 +293,10 @@ public class ParcelableHkpKeyserver extends Keyserver implements Parcelable { int algorithmId = Integer.decode(matcher.group(2)); entry.setAlgorithm(KeyFormattingUtils.getAlgorithmInfo(algorithmId, bitSize, null)); - final long creationDate = Long.parseLong(matcher.group(4)); - final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC")); - tmpGreg.setTimeInMillis(creationDate * 1000); - entry.setDate(tmpGreg.getTime()); + long creationDate = Long.parseLong(matcher.group(4)); + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTimeInMillis(creationDate * 1000); + entry.setDate(calendar.getTime()); } catch (NumberFormatException e) { Log.e(Constants.TAG, "Conversation for bit size, algorithm, or creation date failed.", e); // skip this key @@ -305,7 +305,18 @@ public class ParcelableHkpKeyserver extends Keyserver implements Parcelable { try { entry.setRevoked(matcher.group(6).contains("r")); - entry.setExpired(matcher.group(6).contains("e")); + boolean expired = matcher.group(6).contains("e"); + + // It may be expired even without flag, thus check expiration date + String expiration; + if (!expired && !(expiration = matcher.group(5)).isEmpty()) { + long expirationDate = Long.parseLong(expiration); + TimeZone timeZoneUTC = TimeZone.getTimeZone("UTC"); + GregorianCalendar calendar = new GregorianCalendar(timeZoneUTC); + calendar.setTimeInMillis(expirationDate * 1000); + expired = new GregorianCalendar(timeZoneUTC).compareTo(calendar) >= 0; + } + entry.setExpired(expired); } catch (NullPointerException e) { Log.e(Constants.TAG, "Check for revocation or expiry failed.", e); // skip this key @@ -338,6 +349,7 @@ public class ParcelableHkpKeyserver extends Keyserver implements Parcelable { } entry.setUserIds(userIds); entry.setPrimaryUserId(userIds.get(0)); + entry.setKeyserver(this); results.add(entry); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableKeyRing.java index a94ce0dce..41a065314 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableKeyRing.java @@ -21,7 +21,8 @@ package org.sufficientlysecure.keychain.keyimport; import android.os.Parcel; import android.os.Parcelable; -/** This class is a parcelable representation of either a keyring as raw data, +/** + * This class is a parcelable representation of either a keyring as raw data, * or a (unique) reference to one as a fingerprint, keyid, or keybase name. */ public class ParcelableKeyRing implements Parcelable { @@ -35,36 +36,23 @@ public class ParcelableKeyRing implements Parcelable { public final String mFbUsername; public ParcelableKeyRing(byte[] bytes) { - this(null, bytes, false); - } - - /** - * @param disAmbiguator useless parameter intended to distinguish this overloaded constructor - * for when null is passed as first two arguments - */ - public ParcelableKeyRing(String expectedFingerprint, byte[] bytes, boolean disAmbiguator) { - mBytes = bytes; - mExpectedFingerprint = expectedFingerprint; - mKeyIdHex = null; - mKeybaseName = null; - mFbUsername = null; - } - - public ParcelableKeyRing(String expectedFingerprint, String keyIdHex) { - mBytes = null; - mExpectedFingerprint = expectedFingerprint; - mKeyIdHex = keyIdHex; - mKeybaseName = null; - mFbUsername = null; + this(bytes, null, null, null, null); } public ParcelableKeyRing(String expectedFingerprint, String keyIdHex, String keybaseName, String fbUsername) { - mBytes = null; - mExpectedFingerprint = expectedFingerprint; - mKeyIdHex = keyIdHex; - mKeybaseName = keybaseName; - mFbUsername = fbUsername; + + this(null, expectedFingerprint, keyIdHex, keybaseName, fbUsername); + } + + public ParcelableKeyRing(byte[] bytes, String expectedFingerprint, String keyIdHex, + String keybaseName, String fbUsername) { + + this.mBytes = bytes; + this.mExpectedFingerprint = expectedFingerprint; + this.mKeyIdHex = keyIdHex; + this.mKeybaseName = keybaseName; + this.mFbUsername = fbUsername; } private ParcelableKeyRing(Parcel source) { @@ -78,6 +66,7 @@ public class ParcelableKeyRing implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeByteArray(mBytes); + dest.writeString(mExpectedFingerprint); dest.writeString(mKeyIdHex); dest.writeString(mKeybaseName); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/AsyncTaskResultWrapper.java similarity index 96% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/AsyncTaskResultWrapper.java index 152629ef4..3f1c9a08b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/AsyncTaskResultWrapper.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.ui.adapter; +package org.sufficientlysecure.keychain.keyimport.processing; import org.sufficientlysecure.keychain.operations.results.OperationResult; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/BytesLoaderState.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/BytesLoaderState.java new file mode 100644 index 000000000..03e4dbc14 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/BytesLoaderState.java @@ -0,0 +1,20 @@ +package org.sufficientlysecure.keychain.keyimport.processing; + +import android.net.Uri; + +public class BytesLoaderState implements LoaderState { + + public byte[] mKeyBytes; + public Uri mDataUri; + + public BytesLoaderState(byte[] keyBytes, Uri dataUri) { + mKeyBytes = keyBytes; + mDataUri = dataUri; + } + + @Override + public boolean isBasicModeSupported() { + return true; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/CloudLoaderState.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/CloudLoaderState.java new file mode 100644 index 000000000..5d7e1f626 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/CloudLoaderState.java @@ -0,0 +1,20 @@ +package org.sufficientlysecure.keychain.keyimport.processing; + +import org.sufficientlysecure.keychain.util.Preferences; + +public class CloudLoaderState implements LoaderState { + + public Preferences.CloudSearchPrefs mCloudPrefs; + public String mServerQuery; + + public CloudLoaderState(String serverQuery, Preferences.CloudSearchPrefs cloudPrefs) { + mServerQuery = serverQuery; + mCloudPrefs = cloudPrefs; + } + + @Override + public boolean isBasicModeSupported() { + return false; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListCloudLoader.java similarity index 82% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListCloudLoader.java index 5725989b4..dd59feff0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListCloudLoader.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.ui.adapter; +package org.sufficientlysecure.keychain.keyimport.processing; import android.content.Context; import android.support.annotation.Nullable; @@ -25,6 +25,7 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.keyimport.CloudSearch; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.Keyserver; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.GetKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -38,11 +39,9 @@ import java.util.ArrayList; public class ImportKeysListCloudLoader extends AsyncTaskLoader>> { - Context mContext; - - Preferences.CloudSearchPrefs mCloudPrefs; - String mServerQuery; + private Context mContext; + private CloudLoaderState mState; private ParcelableProxy mParcelableProxy; private ArrayList mEntryList = new ArrayList<>(); @@ -51,18 +50,18 @@ public class ImportKeysListCloudLoader /** * Searches a keyserver as specified in cloudPrefs, using an explicit proxy if passed * - * @param serverQuery string to search on servers for. If is a fingerprint, - * will enforce fingerprint check - * @param cloudPrefs contains keyserver to search on, whether to search on the keyserver, - * and whether to search keybase.io + * @param loaderState state containing the string to search on servers for (if it is a + * fingerprint, will enforce fingerprint check) and the keyserver to + * search on (whether to search on the keyserver, and whether to search + * keybase.io) * @param parcelableProxy explicit proxy to use. If null, will retrieve from preferences */ - public ImportKeysListCloudLoader(Context context, String serverQuery, Preferences.CloudSearchPrefs cloudPrefs, + public ImportKeysListCloudLoader(Context context, CloudLoaderState loaderState, @Nullable ParcelableProxy parcelableProxy) { + super(context); mContext = context; - mServerQuery = serverQuery; - mCloudPrefs = cloudPrefs; + mState = loaderState; mParcelableProxy = parcelableProxy; } @@ -70,18 +69,24 @@ public class ImportKeysListCloudLoader public AsyncTaskResultWrapper> loadInBackground() { mEntryListWrapper = new AsyncTaskResultWrapper<>(mEntryList, null); - if (mServerQuery == null) { + if (mState.mServerQuery == null) { Log.e(Constants.TAG, "mServerQuery is null!"); return mEntryListWrapper; } - if (mServerQuery.startsWith("0x") && mServerQuery.length() == 42) { + if (mState.mServerQuery.startsWith("0x") && mState.mServerQuery.length() == 42) { Log.d(Constants.TAG, "This search is based on a unique fingerprint. Enforce a fingerprint check!"); queryServer(true); } else { queryServer(false); } + // Now we have all the data needed to build the parcelable key ring for this key + for (ImportKeysListEntry e : mEntryList) { + e.setParcelableKeyRing(new ParcelableKeyRing(e.getFingerprintHex(), e.getKeyIdHex(), + e.getKeybaseName(), e.getFbUsername())); + } + return mEntryListWrapper; } @@ -133,15 +138,15 @@ public class ImportKeysListCloudLoader try { ArrayList searchResult = CloudSearch.search( - mServerQuery, - mCloudPrefs, + mState.mServerQuery, + mState.mCloudPrefs, proxy ); mEntryList.clear(); // add result to data if (enforceFingerprint) { - String fingerprint = mServerQuery.substring(2); + String fingerprint = mState.mServerQuery.substring(2); Log.d(Constants.TAG, "fingerprint: " + fingerprint); // query must return only one result! if (searchResult.size() == 1) { @@ -151,7 +156,6 @@ public class ImportKeysListCloudLoader * to enforce a check when the key is imported by KeychainService */ uniqueEntry.setFingerprintHex(fingerprint); - uniqueEntry.setSelected(true); mEntryList.add(uniqueEntry); } } else { @@ -175,6 +179,9 @@ public class ImportKeysListCloudLoader } else if (e instanceof Keyserver.QueryTooShortOrTooManyResponsesException) { error = GetKeyResult.RESULT_ERROR_TOO_SHORT_OR_TOO_MANY_RESPONSES; logType = OperationResult.LogType.MSG_GET_QUERY_TOO_SHORT_OR_TOO_MANY_RESPONSES; + } else if (e instanceof Keyserver.QueryNoEnabledSourceException) { + error = GetKeyResult.RESULT_ERROR_NO_ENABLED_SOURCE; + logType = OperationResult.LogType.MSG_GET_NO_ENABLED_SOURCE; } OperationResult.OperationLog log = new OperationResult.OperationLog(); log.add(logType, 0); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java similarity index 73% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java index df24e9877..f4afecc09 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java @@ -15,9 +15,26 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.ui.adapter; +package org.sufficientlysecure.keychain.keyimport.processing; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.content.AsyncTaskLoader; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.operations.results.GetKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; +import org.sufficientlysecure.keychain.util.FileHelper; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.PositionAwareInputStream; + import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; @@ -25,40 +42,19 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.util.LongSparseArray; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.results.GetKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; -import org.sufficientlysecure.keychain.ui.ImportKeysListFragment.BytesLoaderState; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.InputData; -import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.PositionAwareInputStream; - public class ImportKeysListLoader extends AsyncTaskLoader>> { - final Context mContext; - final BytesLoaderState mLoaderState; + private Context mContext; + private BytesLoaderState mState; - ArrayList mData = new ArrayList<>(); - LongSparseArray mParcelableRings = new LongSparseArray<>(); - AsyncTaskResultWrapper> mEntryListWrapper; + private ArrayList mData = new ArrayList<>(); + private AsyncTaskResultWrapper> mEntryListWrapper; - public ImportKeysListLoader(Context context, BytesLoaderState inputData) { + public ImportKeysListLoader(Context context, BytesLoaderState loaderState) { super(context); - this.mContext = context; - this.mLoaderState = inputData; + mContext = context; + mState = loaderState; } @Override @@ -73,13 +69,13 @@ public class ImportKeysListLoader mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult); } - if (mLoaderState == null) { + if (mState == null) { Log.e(Constants.TAG, "Input data is null!"); return mEntryListWrapper; } try { - InputData inputData = getInputData(getContext(), mLoaderState); + InputData inputData = getInputData(mState); generateListOfKeyrings(inputData); } catch (FileNotFoundException e) { OperationLog log = new OperationLog(); @@ -109,16 +105,9 @@ public class ImportKeysListLoader super.cancelLoad(); } - @Override - public void deliverResult(AsyncTaskResultWrapper> data) { - super.deliverResult(data); - } - - public LongSparseArray getParcelableRings() { - return mParcelableRings; - } - - /** Reads all PGPKeyRing objects from the bytes of an InputData object. */ + /** + * Reads all PGPKeyRing objects from the bytes of an InputData object. + */ private void generateListOfKeyrings(InputData inputData) { PositionAwareInputStream progressIn = new PositionAwareInputStream( inputData.getInputStream()); @@ -131,10 +120,7 @@ public class ImportKeysListLoader // parse all keyrings IteratorWithIOThrow it = UncachedKeyRing.fromStream(bufferedInput); while (it.hasNext()) { - UncachedKeyRing ring = it.next(); - ImportKeysListEntry item = new ImportKeysListEntry(getContext(), ring); - mData.add(item); - mParcelableRings.put(item.hashCode(), new ParcelableKeyRing(ring.getEncoded())); + mData.add(new ImportKeysListEntry(mContext, it.next())); } } catch (IOException e) { Log.e(Constants.TAG, "IOException on parsing key file! Return NoValidKeysException!", e); @@ -147,13 +133,15 @@ public class ImportKeysListLoader } @NonNull - private static InputData getInputData(Context context, BytesLoaderState loaderState) throws FileNotFoundException { + private InputData getInputData(BytesLoaderState ls) + throws FileNotFoundException { + InputData inputData; - if (loaderState.mKeyBytes != null) { - inputData = new InputData(new ByteArrayInputStream(loaderState.mKeyBytes), loaderState.mKeyBytes.length); - } else if (loaderState.mDataUri != null) { - InputStream inputStream = context.getContentResolver().openInputStream(loaderState.mDataUri); - long length = FileHelper.getFileSize(context, loaderState.mDataUri, -1); + if (ls.mKeyBytes != null) { + inputData = new InputData(new ByteArrayInputStream(ls.mKeyBytes), ls.mKeyBytes.length); + } else if (ls.mDataUri != null) { + InputStream inputStream = mContext.getContentResolver().openInputStream(ls.mDataUri); + long length = FileHelper.getFileSize(mContext, ls.mDataUri, -1); inputData = new InputData(inputStream, length); } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListener.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListener.java new file mode 100644 index 000000000..55b20c561 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListener.java @@ -0,0 +1,13 @@ +package org.sufficientlysecure.keychain.keyimport.processing; + +import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; + +import java.util.List; + +public interface ImportKeysListener extends ImportKeysResultListener { + + void loadKeys(LoaderState loaderState); + + void importKeys(List entries); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysOperationCallback.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysOperationCallback.java new file mode 100644 index 000000000..aae47c590 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysOperationCallback.java @@ -0,0 +1,46 @@ +package org.sufficientlysecure.keychain.keyimport.processing; + +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; + +public class ImportKeysOperationCallback implements + CryptoOperationHelper.Callback { + + private ImportKeysResultListener mResultListener; + private ImportKeyringParcel mKeyringParcel; + + public ImportKeysOperationCallback( + ImportKeysResultListener resultListener, + ImportKeyringParcel inputParcel + ) { + this.mResultListener = resultListener; + this.mKeyringParcel = inputParcel; + } + + @Override + public ImportKeyringParcel createOperationInput() { + return mKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + mResultListener.handleResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + // do nothing + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + mResultListener.handleResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysResultListener.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysResultListener.java new file mode 100644 index 000000000..35e30c257 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysResultListener.java @@ -0,0 +1,10 @@ +package org.sufficientlysecure.keychain.keyimport.processing; + +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; + +public interface ImportKeysResultListener { + + void handleResult(ImportKeyResult result); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/LoaderState.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/LoaderState.java new file mode 100644 index 000000000..098a2677d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/LoaderState.java @@ -0,0 +1,13 @@ +package org.sufficientlysecure.keychain.keyimport.processing; + +public interface LoaderState { + + /** + * Basic mode includes ability to import all keys retrieved from the selected source + * This doesn't make sense for all sources (for example keyservers..) + * + * @return if currently selected source supports basic mode + */ + boolean isBasicModeSupported(); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java index 078bf305d..f9f811d39 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -19,20 +19,6 @@ package org.sufficientlysecure.keychain.operations; -import java.io.IOException; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.GregorianCalendar; -import java.util.Iterator; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - import android.content.Context; import android.support.annotation.NonNull; @@ -49,6 +35,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; @@ -57,14 +44,28 @@ import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.util.IteratorWithSize; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ProgressScaler; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; +import java.io.IOException; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + /** * An operation class which implements high level import * operations. @@ -79,13 +80,13 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; * not include self certificates for user ids in the secret keyring. The import * method here will generally import keyrings in the order given by the * iterator, so this should be ensured beforehand. - * - * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() */ public class ImportOperation extends BaseOperation { private static final int MAX_THREADS = 10; + public static final String CACHE_FILE_NAME = "key_import.pcl"; + public ImportOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); @@ -98,20 +99,20 @@ public class ImportOperation extends BaseOperation { // Overloaded functions for using progressable supplied in constructor during import public ImportKeyResult serialKeyRingImport(Iterator entries, int num, - ParcelableHkpKeyserver hkpKeyserver, ParcelableProxy proxy) { - return serialKeyRingImport(entries, num, hkpKeyserver, mProgressable, proxy); + ParcelableHkpKeyserver keyserver, ParcelableProxy proxy, boolean skipSave) { + return serialKeyRingImport(entries, num, keyserver, mProgressable, proxy, skipSave); } @NonNull private ImportKeyResult serialKeyRingImport(ParcelableFileCache cache, - ParcelableHkpKeyserver hkpKeyserver, ParcelableProxy proxy) { + ParcelableHkpKeyserver keyserver, ParcelableProxy proxy, boolean skipSave) { // get entries from cached file try { IteratorWithSize it = cache.readCache(); int numEntries = it.getSize(); - return serialKeyRingImport(it, numEntries, hkpKeyserver, mProgressable, proxy); + return serialKeyRingImport(it, numEntries, keyserver, mProgressable, proxy, skipSave); } catch (IOException e) { // Special treatment here, we need a lot @@ -137,7 +138,7 @@ public class ImportOperation extends BaseOperation { @NonNull private ImportKeyResult serialKeyRingImport(Iterator entries, int num, ParcelableHkpKeyserver hkpKeyserver, Progressable progressable, - @NonNull ParcelableProxy proxy) { + @NonNull ParcelableProxy proxy, boolean skipSave) { if (progressable != null) { progressable.setProgress(R.string.progress_importing, 0, 100); } @@ -153,6 +154,8 @@ public class ImportOperation extends BaseOperation { int newKeys = 0, updatedKeys = 0, badKeys = 0, secret = 0; ArrayList importedMasterKeyIds = new ArrayList<>(); + ArrayList canKeyRings = new ArrayList<>(); + boolean cancelled = false; int position = 0; double progSteps = 100.0 / num; @@ -314,14 +317,14 @@ public class ImportOperation extends BaseOperation { // and https://github.com/open-keychain/open-keychain/issues/1480 synchronized (mProviderHelper) { mProviderHelper.clearLog(); + ProgressScaler progressScaler = new ProgressScaler(progressable, (int) (position * progSteps), + (int) ((position + 1) * progSteps), 100); if (key.isSecret()) { - result = mProviderHelper.saveSecretKeyRing(key, - new ProgressScaler(progressable, (int) (position * progSteps), - (int) ((position + 1) * progSteps), 100)); + result = mProviderHelper.saveSecretKeyRing(key, progressScaler, + canKeyRings, skipSave); } else { - result = mProviderHelper.savePublicKeyRing(key, - new ProgressScaler(progressable, (int) (position * progSteps), - (int) ((position + 1) * progSteps), 100), entry.mExpectedFingerprint); + result = mProviderHelper.savePublicKeyRing(key, progressScaler, + entry.mExpectedFingerprint, canKeyRings, skipSave); } } if (!result.success()) { @@ -337,7 +340,7 @@ public class ImportOperation extends BaseOperation { } importedMasterKeyIds.add(key.getMasterKeyId()); } - if (entry.mBytes == null) { + if (!skipSave && (entry.mBytes == null)) { // synonymous to isDownloadFromKeyserver. // If no byte data was supplied, import from keyserver took place // this prevents file imports being noted as keyserver imports @@ -360,7 +363,7 @@ public class ImportOperation extends BaseOperation { // synchronized on mProviderHelper to prevent // https://github.com/open-keychain/open-keychain/issues/1221 since a consolidate deletes // and re-inserts keys, which could conflict with a parallel db key update - if (secret > 0) { + if (!skipSave && (secret > 0)) { setPreventCancel(); ConsolidateResult result; synchronized (mProviderHelper) { @@ -418,8 +421,11 @@ public class ImportOperation extends BaseOperation { } } - return new ImportKeyResult(resultType, log, newKeys, updatedKeys, badKeys, secret, + ImportKeyResult result = new ImportKeyResult(resultType, log, newKeys, updatedKeys, badKeys, secret, importedMasterKeyIdsArray); + + result.setCanonicalizedKeyRings(canKeyRings); + return result; } @NonNull @@ -427,19 +433,18 @@ public class ImportOperation extends BaseOperation { public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) { ArrayList keyList = importInput.mKeyList; ParcelableHkpKeyserver keyServer = importInput.mKeyserver; + boolean skipSave = importInput.mSkipSave; ImportKeyResult result; - if (keyList == null) {// import from file, do serially - ParcelableFileCache cache = new ParcelableFileCache<>(mContext, - "key_import.pcl"); - - result = serialKeyRingImport(cache, null, null); + ParcelableFileCache cache = + new ParcelableFileCache<>(mContext, CACHE_FILE_NAME); + result = serialKeyRingImport(cache, null, null, skipSave); } else { ParcelableProxy proxy; if (cryptoInput.getParcelableProxy() == null) { // explicit proxy not set - if(!OrbotHelper.isOrbotInRequiredState(mContext)) { + if (!OrbotHelper.isOrbotInRequiredState(mContext)) { // show dialog to enable/install dialog return new ImportKeyResult(null, RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); @@ -449,7 +454,7 @@ public class ImportOperation extends BaseOperation { proxy = cryptoInput.getParcelableProxy(); } - result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer, proxy); + result = multiThreadedKeyImport(keyList, keyServer, proxy, skipSave); } ContactSyncAdapterService.requestContactsSync(); @@ -457,44 +462,43 @@ public class ImportOperation extends BaseOperation { } @NonNull - private ImportKeyResult multiThreadedKeyImport(@NonNull Iterator keyListIterator, - int totKeys, final ParcelableHkpKeyserver hkpKeyserver, - final ParcelableProxy proxy) { - Log.d(Constants.TAG, "Multi-threaded key import starting"); - KeyImportAccumulator accumulator = new KeyImportAccumulator(totKeys, mProgressable); + private ImportKeyResult multiThreadedKeyImport(ArrayList keyList, + final ParcelableHkpKeyserver keyServer, final ParcelableProxy proxy, + final boolean skipSave) { - final ProgressScaler ignoreProgressable = new ProgressScaler(); + Log.d(Constants.TAG, "Multi-threaded key import starting"); + + final Iterator keyListIterator = keyList.iterator(); + final int totKeys = keyList.size(); ExecutorService importExecutor = new ThreadPoolExecutor(0, MAX_THREADS, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue()); - ExecutorCompletionService importCompletionService = new ExecutorCompletionService<>(importExecutor); while (keyListIterator.hasNext()) { // submit all key rings to be imported - - final ParcelableKeyRing pkRing = keyListIterator.next(); - Callable importOperationCallable = new Callable () { @Override public ImportKeyResult call() { - if (checkCancelled()) { return null; } ArrayList list = new ArrayList<>(); - list.add(pkRing); + list.add(keyListIterator.next()); + ProgressScaler ignoreProgressable = new ProgressScaler(); - return serialKeyRingImport(list.iterator(), 1, hkpKeyserver, ignoreProgressable, proxy); + return serialKeyRingImport(list.iterator(), 1, keyServer, ignoreProgressable, + proxy, skipSave); } }; importCompletionService.submit(importOperationCallable); } + KeyImportAccumulator accumulator = new KeyImportAccumulator(totKeys, mProgressable); while (!accumulator.isImportFinished()) { // accumulate the results of each import try { accumulator.accumulateKeyImport(importCompletionService.take().get()); @@ -511,7 +515,6 @@ public class ImportOperation extends BaseOperation { } } return accumulator.getConsolidatedResult(); - } /** @@ -519,10 +522,10 @@ public class ImportOperation extends BaseOperation { */ public static class KeyImportAccumulator { private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog(); - Progressable mProgressable; + private Progressable mProgressable; private int mTotalKeys; private int mImportedKeys = 0; - ArrayList mImportedMasterKeyIds = new ArrayList<>(); + private ArrayList mImportedMasterKeyIds = new ArrayList<>(); private int mBadKeys = 0; private int mNewKeys = 0; private int mUpdatedKeys = 0; @@ -530,6 +533,8 @@ public class ImportOperation extends BaseOperation { private int mResultType = 0; private boolean mHasCancelledResult; + public ArrayList mCanonicalizedKeyRings; + /** * Accumulates keyring imports and updates the progressable whenever a new key is imported. * Also sets the progress to 0 on instantiation. @@ -544,6 +549,8 @@ public class ImportOperation extends BaseOperation { if (mProgressable != null) { mProgressable.setProgress(0, totalKeys); } + + mCanonicalizedKeyRings = new ArrayList<>(); } public void accumulateKeyImport(ImportKeyResult result) { @@ -575,6 +582,8 @@ public class ImportOperation extends BaseOperation { mImportedMasterKeyIds.add(masterKeyId); } + mCanonicalizedKeyRings.addAll(result.mCanonicalizedKeyRings); + // if any key import has been cancelled, set result type to cancelled // resultType is added to in getConsolidatedKayImport to account for remaining factors mResultType |= result.getResult() & ImportKeyResult.RESULT_CANCELLED; @@ -614,8 +623,11 @@ public class ImportOperation extends BaseOperation { masterKeyIds[i] = mImportedMasterKeyIds.get(i); } - return new ImportKeyResult(mResultType, mImportLog, mNewKeys, mUpdatedKeys, mBadKeys, - mSecret, masterKeyIds); + ImportKeyResult result = new ImportKeyResult(mResultType, mImportLog, mNewKeys, + mUpdatedKeys, mBadKeys, mSecret, masterKeyIds); + + result.setCanonicalizedKeyRings(mCanonicalizedKeyRings); + return result; } public boolean isImportFinished() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java index 76ffaff4a..c0eab6381 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java @@ -44,13 +44,14 @@ public class GetKeyResult extends InputPendingResult { super(log, requiredInput, cryptoInputParcel); } - public static final int RESULT_ERROR_NO_VALID_KEYS = RESULT_ERROR + (1<<4); - public static final int RESULT_ERROR_NO_PGP_PARTS = RESULT_ERROR + (2<<4); - public static final int RESULT_ERROR_QUERY_TOO_SHORT = RESULT_ERROR + (3<<4); - public static final int RESULT_ERROR_TOO_MANY_RESPONSES = RESULT_ERROR + (4<<4); - public static final int RESULT_ERROR_TOO_SHORT_OR_TOO_MANY_RESPONSES = RESULT_ERROR + (5<<4); - public static final int RESULT_ERROR_QUERY_FAILED = RESULT_ERROR + (6<<4); - public static final int RESULT_ERROR_FILE_NOT_FOUND = RESULT_ERROR + (7<<4); + public static final int RESULT_ERROR_NO_VALID_KEYS = RESULT_ERROR + (1 << 4); + public static final int RESULT_ERROR_NO_PGP_PARTS = RESULT_ERROR + (2 << 4); + public static final int RESULT_ERROR_QUERY_TOO_SHORT = RESULT_ERROR + (3 << 4); + public static final int RESULT_ERROR_TOO_MANY_RESPONSES = RESULT_ERROR + (4 << 4); + public static final int RESULT_ERROR_TOO_SHORT_OR_TOO_MANY_RESPONSES = RESULT_ERROR + (5 << 4); + public static final int RESULT_ERROR_QUERY_FAILED = RESULT_ERROR + (6 << 4); + public static final int RESULT_ERROR_FILE_NOT_FOUND = RESULT_ERROR + (7 << 4); + public static final int RESULT_ERROR_NO_ENABLED_SOURCE = RESULT_ERROR + (8 << 4); public GetKeyResult(Parcel source) { super(source); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java index 5f5090bee..71072f029 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.os.Parcel; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.LogDisplayActivity; @@ -32,11 +33,16 @@ import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Showable; import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import java.util.ArrayList; + public class ImportKeyResult extends InputPendingResult { public final int mNewKeys, mUpdatedKeys, mBadKeys, mSecret; public final long[] mImportedMasterKeyIds; + // NOT PARCELED + public ArrayList mCanonicalizedKeyRings; + // At least one new key public static final int RESULT_OK_NEWKEYS = 8; // At least one updated key @@ -107,6 +113,10 @@ public class ImportKeyResult extends InputPendingResult { mImportedMasterKeyIds = new long[]{}; } + public void setCanonicalizedKeyRings(ArrayList canonicalizedKeyRings) { + this.mCanonicalizedKeyRings = canonicalizedKeyRings; + } + @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); @@ -128,7 +138,6 @@ public class ImportKeyResult extends InputPendingResult { }; public Showable createNotify(final Activity activity) { - int resultType = getResult(); String str; @@ -204,7 +213,6 @@ public class ImportKeyResult extends InputPendingResult { activity.startActivity(intent); } }, R.string.snackbar_details); - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index be736d785..7a2286fdc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -818,6 +818,7 @@ public abstract class OperationResult implements Parcelable { MSG_GET_QUERY_TOO_SHORT_OR_TOO_MANY_RESPONSES (LogLevel.ERROR, R.string.msg_get_query_too_short_or_too_many_responses), MSG_GET_QUERY_FAILED (LogLevel.ERROR, R.string.msg_download_query_failed), MSG_GET_FILE_NOT_FOUND (LogLevel.ERROR, R.string.msg_get_file_not_found), + MSG_GET_NO_ENABLED_SOURCE (LogLevel.ERROR, R.string.msg_get_no_enabled_source), MSG_DEL_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_del_error_empty), MSG_DEL_ERROR_MULTI_SECRET (LogLevel.ERROR, R.string.msg_del_error_multi_secret), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java index f1a57461f..2075f6b58 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -29,18 +29,17 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; -import java.util.Iterator; import java.util.Set; -/** A generic wrapped PGPKeyRing object. - * +/** + * A generic wrapped PGPKeyRing object. + *

* This class provides implementations for all basic getters which both * PublicKeyRing and SecretKeyRing have in common. To make the wrapped keyring * class typesafe in implementing subclasses, the field is stored in the * implementing class, providing properly typed access through the getRing * getter method. - * */ public abstract class CanonicalizedKeyRing extends KeyRing { @@ -80,16 +79,24 @@ public abstract class CanonicalizedKeyRing extends KeyRing { public boolean isRevoked() { // Is the master key revoked? - return getRing().getPublicKey().isRevoked(); + return getRing().getPublicKey().hasRevocation(); + } + + public Date getCreationDate() { + return getPublicKey().getCreationTime(); + } + + public Date getExpirationDate() { + return getPublicKey().getExpiryTime(); } public boolean isExpired() { // Is the master key expired? - Date creationDate = getPublicKey().getCreationTime(); - Date expiryDate = getPublicKey().getExpiryTime(); + Date creationDate = getCreationDate(); + Date expirationDate = getExpirationDate(); Date now = new Date(); - return creationDate.after(now) || (expiryDate != null && expiryDate.before(now)); + return creationDate.after(now) || (expirationDate != null && expirationDate.before(now)); } public boolean canCertify() throws PgpKeyNotFoundException { @@ -98,7 +105,7 @@ public abstract class CanonicalizedKeyRing extends KeyRing { public Set getEncryptIds() { HashSet result = new HashSet<>(); - for(CanonicalizedPublicKey key : publicKeyIterator()) { + for (CanonicalizedPublicKey key : publicKeyIterator()) { if (key.canEncrypt() && key.isValid()) { result.add(key.getKeyId()); } @@ -107,7 +114,7 @@ public abstract class CanonicalizedKeyRing extends KeyRing { } public long getEncryptId() throws PgpKeyNotFoundException { - for(CanonicalizedPublicKey key : publicKeyIterator()) { + for (CanonicalizedPublicKey key : publicKeyIterator()) { if (key.canEncrypt() && key.isValid()) { return key.getKeyId(); } @@ -119,7 +126,7 @@ public abstract class CanonicalizedKeyRing extends KeyRing { try { getEncryptId(); return true; - } catch(PgpKeyNotFoundException e) { + } catch (PgpKeyNotFoundException e) { return false; } } @@ -128,8 +135,10 @@ public abstract class CanonicalizedKeyRing extends KeyRing { getRing().encode(stream); } - /** Returns an UncachedKeyRing which wraps the same data as this ring. This method should - * only be used */ + /** + * Returns an UncachedKeyRing which wraps the same data as this ring. This method should + * only be used + */ public UncachedKeyRing getUncachedKeyRing() { return new UncachedKeyRing(getRing()); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java index 604a5a027..019c5568a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java @@ -173,7 +173,7 @@ public class CachedPublicKeyRing extends KeyRing { Object data = mProviderHelper.getGenericData(mUri, KeychainContract.KeyRings.VERIFIED, ProviderHelper.FIELD_TYPE_INTEGER); - return (Integer) data; + return ((Long) data).intValue(); } catch(ProviderHelper.NotFoundException e) { throw new PgpKeyNotFoundException(e); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java index 6eda21d2c..c95a8f283 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -40,6 +40,7 @@ import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; @@ -61,9 +62,9 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.IteratorWithSize; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ProgressFixedScaler; import org.sufficientlysecure.keychain.util.ProgressScaler; @@ -276,7 +277,7 @@ public class ProviderHelper { public ArrayList getConfirmedUserIds(long masterKeyId) throws NotFoundException { Cursor cursor = mContentResolver.query(UserPackets.buildUserIdsUri(masterKeyId), - new String[]{ UserPackets.USER_ID }, UserPackets.VERIFIED + " = " + Certs.VERIFIED_SECRET, null, null + new String[]{UserPackets.USER_ID}, UserPackets.VERIFIED + " = " + Certs.VERIFIED_SECRET, null, null ); if (cursor == null) { throw new NotFoundException("Key id for requested user ids not found"); @@ -476,7 +477,7 @@ public class ProviderHelper { String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId); UserPacketItem item = new UserPacketItem(); uids.add(item); - OpenPgpUtils.UserId splitUserId = KeyRing.splitUserId(userId); + OpenPgpUtils.UserId splitUserId = KeyRing.splitUserId(userId); item.userId = userId; item.name = splitUserId.name; item.email = splitUserId.email; @@ -794,7 +795,7 @@ public class ProviderHelper { } // if one is *trusted* but the other isn't, that one comes first // this overrides the primary attribute, even! - if ( (trustedCerts.size() == 0) != (o.trustedCerts.size() == 0) ) { + if ((trustedCerts.size() == 0) != (o.trustedCerts.size() == 0)) { return trustedCerts.size() > o.trustedCerts.size() ? -1 : 1; } // if one key is primary but the other isn't, the primary one always comes first @@ -903,17 +904,16 @@ public class ProviderHelper { } - public SaveKeyringResult savePublicKeyRing(UncachedKeyRing keyRing) { - return savePublicKeyRing(keyRing, new ProgressScaler(), null); - } - /** * Save a public keyring into the database. *

* This is a high level method, which takes care of merging all new information into the old and * keep public and secret keyrings in sync. */ - public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress, String expectedFingerprint) { + public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress, + String expectedFingerprint, + ArrayList canKeyRings, + boolean skipSave) { try { long masterKeyId = publicRing.getMasterKeyId(); @@ -926,10 +926,12 @@ public class ProviderHelper { } CanonicalizedPublicKeyRing canPublicRing; + boolean alreadyExists = false; // If there is an old keyring, merge it try { UncachedKeyRing oldPublicRing = getCanonicalizedPublicKeyRing(masterKeyId).getUncachedKeyRing(); + alreadyExists = true; // Merge data from new public ring into the old one log(LogType.MSG_IP_MERGE_PUBLIC); @@ -945,6 +947,7 @@ public class ProviderHelper { if (canPublicRing == null) { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } + if (canKeyRings != null) canKeyRings.add(canPublicRing); // Early breakout if nothing changed if (Arrays.hashCode(publicRing.getEncoded()) @@ -960,7 +963,7 @@ public class ProviderHelper { if (canPublicRing == null) { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } - + if (canKeyRings != null) canKeyRings.add(canPublicRing); } // If there is a secret key, merge new data (if any) and save the key for later @@ -997,29 +1000,51 @@ public class ProviderHelper { } } - int result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, canSecretRing != null); + int result; + if (!skipSave) { + result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, canSecretRing != null); + } else { + result = SaveKeyringResult.SAVED_PUBLIC + | (alreadyExists ? SaveKeyringResult.UPDATED : 0); + } // Save the saved keyring (if any) if (canSecretRing != null) { progress.setProgress(LogType.MSG_IP_REINSERT_SECRET.getMsgId(), 90, 100); - int secretResult = saveCanonicalizedSecretKeyRing(canSecretRing); + + int secretResult; + if (!skipSave) { + secretResult = saveCanonicalizedSecretKeyRing(canSecretRing); + } else { + secretResult = SaveKeyringResult.SAVED_SECRET; + } + if ((secretResult & SaveKeyringResult.RESULT_ERROR) != SaveKeyringResult.RESULT_ERROR) { result |= SaveKeyringResult.SAVED_SECRET; } } return new SaveKeyringResult(result, mLog, canSecretRing); - } catch (IOException e) { log(LogType.MSG_IP_ERROR_IO_EXC); return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } finally { mIndent -= 1; } - } - public SaveKeyringResult saveSecretKeyRing(UncachedKeyRing secretRing, Progressable progress) { + public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress, + String expectedFingerprint) { + return savePublicKeyRing(publicRing, progress, expectedFingerprint, null, false); + } + + public SaveKeyringResult savePublicKeyRing(UncachedKeyRing keyRing) { + return savePublicKeyRing(keyRing, new ProgressScaler(), null); + } + + public SaveKeyringResult saveSecretKeyRing(UncachedKeyRing secretRing, Progressable progress, + ArrayList canKeyRings, + boolean skipSave) { try { long masterKeyId = secretRing.getMasterKeyId(); @@ -1032,10 +1057,12 @@ public class ProviderHelper { } CanonicalizedSecretKeyRing canSecretRing; + boolean alreadyExists = false; // If there is an old secret key, merge it. try { UncachedKeyRing oldSecretRing = getCanonicalizedSecretKeyRing(masterKeyId).getUncachedKeyRing(); + alreadyExists = true; // Merge data from new secret ring into old one log(LogType.MSG_IS_MERGE_SECRET); @@ -1052,6 +1079,7 @@ public class ProviderHelper { if (canSecretRing == null) { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } + if (canKeyRings != null) canKeyRings.add(canSecretRing); // Early breakout if nothing changed if (Arrays.hashCode(secretRing.getEncoded()) @@ -1083,7 +1111,7 @@ public class ProviderHelper { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } } - + if (canKeyRings != null) canKeyRings.add(canSecretRing); } // Merge new data into public keyring as well, if there is any @@ -1109,25 +1137,38 @@ public class ProviderHelper { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } - int result; + int publicResult; + if (!skipSave) { + publicResult = saveCanonicalizedPublicKeyRing(canPublicRing, progress, true); + } else { + publicResult = SaveKeyringResult.SAVED_PUBLIC; + } - result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, true); - if ((result & SaveKeyringResult.RESULT_ERROR) == SaveKeyringResult.RESULT_ERROR) { + if ((publicResult & SaveKeyringResult.RESULT_ERROR) == SaveKeyringResult.RESULT_ERROR) { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } progress.setProgress(LogType.MSG_IP_REINSERT_SECRET.getMsgId(), 90, 100); - result = saveCanonicalizedSecretKeyRing(canSecretRing); + + int result; + if (!skipSave) { + result = saveCanonicalizedSecretKeyRing(canSecretRing); + } else { + result = SaveKeyringResult.SAVED_SECRET + | (alreadyExists ? SaveKeyringResult.UPDATED : 0); + } return new SaveKeyringResult(result, mLog, canSecretRing); - } catch (IOException e) { log(LogType.MSG_IS_ERROR_IO_EXC); return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } finally { mIndent -= 1; } + } + public SaveKeyringResult saveSecretKeyRing(UncachedKeyRing secretRing, Progressable progress) { + return saveSecretKeyRing(secretRing, progress, null, false); } @NonNull @@ -1350,7 +1391,7 @@ public class ProviderHelper { ImportKeyResult result = new ImportOperation(mContext, this, new ProgressFixedScaler(progress, 10, 25, 100, R.string.progress_con_reimport)) - .serialKeyRingImport(itSecrets, numSecrets, null, null); + .serialKeyRingImport(itSecrets, numSecrets, null, null, false); log.add(result, indent); } else { log.add(LogType.MSG_CON_REIMPORT_SECRET_SKIP, indent); @@ -1378,7 +1419,7 @@ public class ProviderHelper { ImportKeyResult result = new ImportOperation(mContext, this, new ProgressFixedScaler(progress, 25, 99, 100, R.string.progress_con_reimport)) - .serialKeyRingImport(itPublics, numPublics, null, null); + .serialKeyRingImport(itPublics, numPublics, null, null, false); log.add(result, indent); // re-insert our backed up list of updated key times // TODO: can this cause issues in case a public key re-import failed? diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteImportKeysActivity.java index ff7138231..4d8113e3c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteImportKeysActivity.java @@ -21,9 +21,7 @@ import android.content.Intent; import android.os.Bundle; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService; import org.sufficientlysecure.keychain.ui.ImportKeysActivity; -import org.sufficientlysecure.keychain.ui.SecurityTokenOperationActivity; public class RemoteImportKeysActivity extends ImportKeysActivity { @@ -39,7 +37,7 @@ public class RemoteImportKeysActivity extends ImportKeysActivity { } @Override - protected void handleResult(ImportKeyResult result) { + public void handleResult(ImportKeyResult result) { setResult(RESULT_OK, mPendingIntentData); finish(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java index ad609d72a..050e126a9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java @@ -27,15 +27,23 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import java.util.ArrayList; public class ImportKeyringParcel implements Parcelable { - // if null, keys are expected to be read from a cache file in ImportExportOperations + // If null, keys are expected to be read from a cache file in ImportExportOperations public ArrayList mKeyList; public ParcelableHkpKeyserver mKeyserver; // must be set if keys are to be imported from a keyserver + // If false, don't save the key, only return it as part of result + public boolean mSkipSave = false; + public ImportKeyringParcel(ArrayList keyList, ParcelableHkpKeyserver keyserver) { mKeyList = keyList; mKeyserver = keyserver; } + public ImportKeyringParcel(ArrayList keyList, ParcelableHkpKeyserver keyserver, boolean skipSave) { + this(keyList, keyserver); + mSkipSave = skipSave; + } + protected ImportKeyringParcel(Parcel in) { if (in.readByte() == 0x01) { mKeyList = new ArrayList<>(); @@ -44,6 +52,7 @@ public class ImportKeyringParcel implements Parcelable { mKeyList = null; } mKeyserver = in.readParcelable(ParcelableHkpKeyserver.class.getClassLoader()); + mSkipSave = in.readInt() != 0; } @Override @@ -60,6 +69,7 @@ public class ImportKeyringParcel implements Parcelable { dest.writeList(mKeyList); } dest.writeParcelable(mKeyserver, flags); + dest.writeInt(mSkipSave ? 1 : 0); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java index ed66283e8..99b3ef7a6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java @@ -465,7 +465,7 @@ public class KeyserverSyncAdapterService extends Service { String hexKeyId = KeyFormattingUtils .convertKeyIdToHex(keyId); // we aren't updating from keybase as of now - keyList.add(new ParcelableKeyRing(fingerprint, hexKeyId)); + keyList.add(new ParcelableKeyRing(fingerprint, hexKeyId, null, null)); } keyCursor.close(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java index fd01c0ad2..f090759fe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java @@ -18,10 +18,6 @@ package org.sufficientlysecure.keychain.ui; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; - import android.app.Activity; import android.content.Intent; import android.os.Bundle; @@ -36,6 +32,7 @@ import android.widget.TextView; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.keyimport.processing.CloudLoaderState; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; @@ -48,6 +45,10 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Preferences; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + public class CreateSecurityTokenImportResetFragment extends QueueingCryptoOperationFragment @@ -211,14 +212,14 @@ public class CreateSecurityTokenImportResetFragment } public void refreshSearch() { - mListFragment.loadNew(new ImportKeysListFragment.CloudLoaderState("0x" + mTokenFingerprint, + mListFragment.loadState(new CloudLoaderState("0x" + mTokenFingerprint, Preferences.getPreferences(getActivity()).getCloudSearchPrefs())); } public void importKey() { ArrayList keyList = new ArrayList<>(); - keyList.add(new ParcelableKeyRing(mTokenFingerprint, null)); + keyList.add(new ParcelableKeyRing(mTokenFingerprint, null, null, null)); mKeyList = keyList; mKeyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java index 4521ae659..ad0e11d94 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java @@ -17,8 +17,6 @@ package org.sufficientlysecure.keychain.ui; -import java.util.ArrayList; - import android.app.Activity; import android.content.Intent; import android.database.Cursor; @@ -59,6 +57,8 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Preferences; +import java.util.ArrayList; + public abstract class DecryptFragment extends Fragment implements LoaderManager.LoaderCallbacks { public static final int LOADER_ID_UNIFIED = 0; @@ -145,7 +145,7 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. { ParcelableKeyRing keyEntry = new ParcelableKeyRing(null, - KeyFormattingUtils.convertKeyIdToHex(unknownKeyId)); + KeyFormattingUtils.convertKeyIdToHex(unknownKeyId), null, null); ArrayList selectedEntries = new ArrayList<>(); selectedEntries.add(keyEntry); @@ -320,7 +320,7 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. mSignatureEmail.setText(userIdSplit.email); } else { mSignatureEmail.setText(KeyFormattingUtils.beautifyKeyIdWithPrefix( - getActivity(), mSignatureResult.getKeyId())); + mSignatureResult.getKeyId())); } // NOTE: Don't use revoked and expired fields from database, they don't show @@ -430,7 +430,7 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. mSignatureEmail.setText(userIdSplit.email); } else { mSignatureEmail.setText(KeyFormattingUtils.beautifyKeyIdWithPrefix( - getActivity(), mSignatureResult.getKeyId())); + mSignatureResult.getKeyId())); } switch (mSignatureResult.getResult()) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java index c43894b79..134db8fd4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -68,6 +68,7 @@ import android.widget.Toast; import android.widget.ViewAnimator; import com.cocosw.bottomsheet.BottomSheet; + import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.BuildConfig; @@ -98,24 +99,24 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Preferences; -/** Displays a list of decrypted inputs. - * +/** + * Displays a list of decrypted inputs. + *

* This class has a complex control flow to manage its input URIs. Each URI * which is in mInputUris is also in exactly one of mPendingInputUris, * mCancelledInputUris, mCurrentInputUri, or a key in mInputDataResults. - * + *

* Processing of URIs happens using a looping approach: * - There is always exactly one method running which works on mCurrentInputUri * - Processing starts in cryptoOperation(), which pops a new mCurrentInputUri - * from the list of mPendingInputUris. + * from the list of mPendingInputUris. * - Once a mCurrentInputUri is finished processing, it should be set to null and - * control handed back to cryptoOperation() + * control handed back to cryptoOperation() * - Control flow can move through asynchronous calls, and resume in callbacks - * like onActivityResult() or onPermissionRequestResult(). - * + * like onActivityResult() or onPermissionRequestResult(). */ public class DecryptListFragment - extends QueueingCryptoOperationFragment + extends QueueingCryptoOperationFragment implements OnMenuItemClickListener { public static final String ARG_INPUT_URIS = "input_uris"; @@ -189,7 +190,7 @@ public class DecryptListFragment outState.putParcelableArrayList(ARG_INPUT_URIS, mInputUris); - HashMap results = new HashMap<>(mInputUris.size()); + HashMap results = new HashMap<>(mInputUris.size()); for (Uri uri : mInputUris) { if (mPendingInputUris.contains(uri)) { continue; @@ -219,7 +220,7 @@ public class DecryptListFragment ArrayList inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS); ArrayList cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS); - ParcelableHashMap results = args.getParcelable(ARG_RESULTS); + ParcelableHashMap results = args.getParcelable(ARG_RESULTS); mCanDelete = args.getBoolean(ARG_CAN_DELETE, false); @@ -231,11 +232,11 @@ public class DecryptListFragment private void displayInputUris( ArrayList inputUris, ArrayList cancelledUris, - HashMap results) { + HashMap results) { mInputUris = inputUris; mCurrentInputUri = null; - mInputDataResults = results != null ? results : new HashMap(inputUris.size()); + mInputDataResults = results != null ? results : new HashMap(inputUris.size()); mCancelledInputUris = cancelledUris != null ? cancelledUris : new ArrayList(); mPendingInputUris = new ArrayList<>(); @@ -295,7 +296,7 @@ public class DecryptListFragment String filename = metadata.getFilename(); if (TextUtils.isEmpty(filename)) { String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(metadata.getMimeType()); - filename = "decrypted" + (ext != null ? "."+ext : ""); + filename = "decrypted" + (ext != null ? "." + ext : ""); } // requires >=kitkat @@ -395,7 +396,7 @@ public class DecryptListFragment } - HashMap mIconCache = new HashMap<>(); + HashMap mIconCache = new HashMap<>(); private void processResult(final Uri uri) { @@ -562,7 +563,7 @@ public class DecryptListFragment Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, - new Parcelable[] { internalIntent }); + new Parcelable[]{internalIntent}); startActivity(chooserIntent); @@ -606,7 +607,7 @@ public class DecryptListFragment .putExtra(DisplayTextActivity.EXTRA_METADATA, metadata), BuildConfig.APPLICATION_ID, R.string.view_internal, R.mipmap.ic_launcher); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, - new Parcelable[] { internalIntent }); + new Parcelable[]{internalIntent}); } startActivity(chooserIntent); @@ -633,7 +634,7 @@ public class DecryptListFragment Log.d(Constants.TAG, "mCurrentInputUri=" + mCurrentInputUri); - if ( ! checkAndRequestReadPermission(activity, mCurrentInputUri)) { + if (!checkAndRequestReadPermission(activity, mCurrentInputUri)) { return null; } @@ -645,15 +646,15 @@ public class DecryptListFragment /** * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris. - * + *

* This method returns true on Android < 6, or if permission is already granted. It * requests the permission and returns false otherwise, taking over responsibility * for mCurrentInputUri. - * + *

* see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html */ private boolean checkAndRequestReadPermission(Activity activity, final Uri uri) { - if ( ! ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return true; } @@ -668,7 +669,7 @@ public class DecryptListFragment } requestPermissions( - new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_READ_EXTERNAL_STORAGE); return false; @@ -677,8 +678,8 @@ public class DecryptListFragment @Override public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, - @NonNull int[] grantResults) { + @NonNull String[] permissions, + @NonNull int[] grantResults) { if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); @@ -694,7 +695,7 @@ public class DecryptListFragment Iterator it = mCancelledInputUris.iterator(); while (it.hasNext()) { Uri uri = it.next(); - if ( ! ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { continue; } it.remove(); @@ -712,7 +713,7 @@ public class DecryptListFragment Iterator it = mPendingInputUris.iterator(); while (it.hasNext()) { Uri uri = it.next(); - if ( ! ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { continue; } it.remove(); @@ -767,7 +768,7 @@ public class DecryptListFragment { ParcelableKeyRing keyEntry = new ParcelableKeyRing(null, - KeyFormattingUtils.convertKeyIdToHex(unknownKeyId)); + KeyFormattingUtils.convertKeyIdToHex(unknownKeyId), null, null); ArrayList selectedEntries = new ArrayList<>(); selectedEntries.add(keyEntry); @@ -975,7 +976,7 @@ public class DecryptListFragment String filename; if (metadata == null) { filename = getString(R.string.filename_unknown); - } else if ( ! TextUtils.isEmpty(metadata.getFilename())) { + } else if (!TextUtils.isEmpty(metadata.getFilename())) { filename = metadata.getFilename(); } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), Constants.MIME_TYPE_KEYS)) { filename = getString(R.string.filename_keys); @@ -1227,7 +1228,7 @@ public class DecryptListFragment vSigStatusText = (TextView) itemView.findViewById(R.id.result_signature_text); vSignatureLayout = itemView.findViewById(R.id.result_signature_layout); vSignatureName = (TextView) itemView.findViewById(R.id.result_signature_name); - vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email); + vSignatureMail = (TextView) itemView.findViewById(R.id.result_signature_email); vSignatureAction = (ViewAnimator) itemView.findViewById(R.id.result_signature_action); vFileList = (LinearLayout) itemView.findViewById(R.id.file_list); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index c741f8d88..aa43e1546 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -24,8 +24,6 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import org.sufficientlysecure.keychain.Constants; @@ -34,6 +32,10 @@ import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; import org.sufficientlysecure.keychain.keyimport.FacebookKeyserver; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListener; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysOperationCallback; +import org.sufficientlysecure.keychain.keyimport.processing.LoaderState; +import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; @@ -42,15 +44,14 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Preferences; import java.io.IOException; import java.util.ArrayList; +import java.util.List; -public class ImportKeysActivity extends BaseActivity - implements CryptoOperationHelper.Callback { +public class ImportKeysActivity extends BaseActivity implements ImportKeysListener { public static final String ACTION_IMPORT_KEY = OpenKeychainIntents.IMPORT_KEY; public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = OpenKeychainIntents.IMPORT_KEY_FROM_KEYSERVER; @@ -79,13 +80,8 @@ public class ImportKeysActivity extends BaseActivity public static final String TAG_FRAG_LIST = "frag_list"; public static final String TAG_FRAG_TOP = "frag_top"; - // for CryptoOperationHelper.Callback - private ParcelableHkpKeyserver mKeyserver; - private ArrayList mKeyList; - - private CryptoOperationHelper mOperationHelper; - private boolean mFreshIntent; + private CryptoOperationHelper mOpHelper; @Override protected void onCreate(Bundle savedInstanceState) { @@ -95,12 +91,6 @@ public class ImportKeysActivity extends BaseActivity mFreshIntent = true; setFullScreenDialogClose(Activity.RESULT_CANCELED, true); - findViewById(R.id.import_import).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - importSelectedKeys(); - } - }); } @Override @@ -258,17 +248,6 @@ public class ImportKeysActivity extends BaseActivity } } - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - // the only thing we need to take care of for restoring state is - // that the top layout is shown iff it contains a fragment - Fragment topFragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_TOP); - boolean hasTopFragment = topFragment != null; - findViewById(R.id.import_keys_top_layout).setVisibility(hasTopFragment ? View.VISIBLE : View.GONE); - } - /** * Shows the list of keys to be imported. * If the fragment is started with non-null bytes/dataUri/serverQuery, it will immediately @@ -282,6 +261,7 @@ public class ImportKeysActivity extends BaseActivity */ private void startListFragment(byte[] bytes, Uri dataUri, String serverQuery, Preferences.CloudSearchPrefs cloudSearchPrefs) { + Fragment listFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, cloudSearchPrefs); @@ -291,11 +271,11 @@ public class ImportKeysActivity extends BaseActivity } private void startTopFileFragment() { - findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE); - Fragment importFileFragment = ImportKeysFileFragment.newInstance(); - getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_top_container, importFileFragment, TAG_FRAG_TOP) - .commit(); + FragmentManager fM = getSupportFragmentManager(); + if (fM.findFragmentByTag(TAG_FRAG_TOP) == null) { + Fragment importFileFragment = ImportKeysFileFragment.newInstance(); + fM.beginTransaction().add(importFileFragment, TAG_FRAG_TOP).commit(); + } } /** @@ -309,12 +289,13 @@ public class ImportKeysActivity extends BaseActivity */ private void startTopCloudFragment(String query, boolean disableQueryEdit, Preferences.CloudSearchPrefs cloudSearchPrefs) { - findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE); - Fragment importCloudFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, - cloudSearchPrefs); - getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_top_container, importCloudFragment, TAG_FRAG_TOP) - .commit(); + + FragmentManager fM = getSupportFragmentManager(); + if (fM.findFragmentByTag(TAG_FRAG_TOP) == null) { + Fragment importCloudFragment = ImportKeysCloudFragment.newInstance(query, + disableQueryEdit, cloudSearchPrefs); + fM.beginTransaction().add(importCloudFragment, TAG_FRAG_TOP).commit(); + } } private boolean isFingerprintValid(String fingerprint) { @@ -327,96 +308,68 @@ public class ImportKeysActivity extends BaseActivity } } - public void loadCallback(final ImportKeysListFragment.LoaderState loaderState) { - FragmentManager fragMan = getSupportFragmentManager(); - ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST); - keyListFragment.loadNew(loaderState); - } - - private void importSelectedKeys() { - - FragmentManager fragMan = getSupportFragmentManager(); - ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST); - - if (keyListFragment.getSelectedEntries().size() == 0) { - Notify.create(this, R.string.error_nothing_import_selected, Notify.Style.ERROR) - .show((ViewGroup) findViewById(R.id.import_snackbar)); - return; - } - - mOperationHelper = new CryptoOperationHelper<>( - 1, this, this, R.string.progress_importing - ); - - ImportKeysListFragment.LoaderState ls = keyListFragment.getLoaderState(); - if (ls instanceof ImportKeysListFragment.BytesLoaderState) { - Log.d(Constants.TAG, "importKeys started"); - - // get DATA from selected key entries - IteratorWithSize selectedEntries = keyListFragment.getSelectedData(); - - // instead of giving the entries by Intent extra, cache them into a - // file to prevent Java Binder problems on heavy imports - // read FileImportCache for more info. - try { - // We parcel this iteratively into a file - anything we can - // display here, we should be able to import. - ParcelableFileCache cache = - new ParcelableFileCache<>(this, "key_import.pcl"); - cache.writeCache(selectedEntries); - - mKeyList = null; - mKeyserver = null; - mOperationHelper.cryptoOperation(); - - } catch (IOException e) { - Log.e(Constants.TAG, "Problem writing cache file", e); - Notify.create(this, "Problem writing cache file!", Notify.Style.ERROR) - .show((ViewGroup) findViewById(R.id.import_snackbar)); - } - } else if (ls instanceof ImportKeysListFragment.CloudLoaderState) { - ImportKeysListFragment.CloudLoaderState sls = - (ImportKeysListFragment.CloudLoaderState) ls; - - // get selected key entries - ArrayList keys = new ArrayList<>(); - { - // change the format into ParcelableKeyRing - ArrayList entries = keyListFragment.getSelectedEntries(); - for (ImportKeysListEntry entry : entries) { - keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), - entry.getKeyIdHex(), entry.getKeybaseName(), entry.getFbUsername())); - } - } - - mKeyList = keys; - mKeyserver = sls.mCloudPrefs.keyserver; - mOperationHelper.cryptoOperation(); - - } - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mOperationHelper != null && - mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + if (mOpHelper != null && + mOpHelper.handleActivityResult(requestCode, resultCode, data)) { return; } super.onActivityResult(requestCode, resultCode, data); } - /** - * Defines how the result of this activity is returned. - * Is overwritten in RemoteImportKeysActivity - */ - protected void handleResult(ImportKeyResult result) { + @Override + public void onBackPressed() { + FragmentManager fM = getSupportFragmentManager(); + ImportKeysListFragment listFragment = + (ImportKeysListFragment) fM.findFragmentByTag(TAG_FRAG_LIST); + + if ((listFragment == null) || listFragment.onBackPressed()) { + super.onBackPressed(); + } + } + + @Override + public void loadKeys(LoaderState loaderState) { + FragmentManager fM = getSupportFragmentManager(); + ((ImportKeysListFragment) fM.findFragmentByTag(TAG_FRAG_LIST)).loadState(loaderState); + } + + @Override + public void importKeys(List entries) { + List keyRings = new ArrayList<>(); + for (ImportKeysListEntry e : entries) { + keyRings.add(e.getParcelableKeyRing()); + } + // instead of giving the entries by Intent extra, cache them into a + // file to prevent Java Binder problems on heavy imports + // read FileImportCache for more info. + try { + // We parcel this iteratively into a file - anything we can + // display here, we should be able to import. + ParcelableFileCache cache = + new ParcelableFileCache<>(this, ImportOperation.CACHE_FILE_NAME); + cache.writeCache(entries.size(), keyRings.iterator()); + } catch (IOException e) { + Log.e(Constants.TAG, "Problem writing cache file", e); + Notify.create(this, "Problem writing cache file!", Notify.Style.ERROR).show(); + return; + } + + ImportKeyringParcel inputParcel = new ImportKeyringParcel(null, null); + ImportKeysOperationCallback callback = new ImportKeysOperationCallback(this, inputParcel); + mOpHelper = new CryptoOperationHelper<>(1, this, callback, R.string.progress_importing); + mOpHelper.cryptoOperation(); + } + + @Override + public void handleResult(ImportKeyResult result) { String intentAction = getIntent().getAction(); - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(intentAction) - || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(intentAction)) { + if (ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(intentAction) + || ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(intentAction)) { Intent intent = new Intent(); intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); - setResult(RESULT_OK, intent); + setResult(Activity.RESULT_OK, intent); finish(); } else if (result.isOkNew() || result.isOkUpdated()) { // User has successfully imported a key, hide first time dialog @@ -427,35 +380,8 @@ public class ImportKeysActivity extends BaseActivity intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); } else { - result.createNotify(ImportKeysActivity.this) - .show((ViewGroup) findViewById(R.id.import_snackbar)); + result.createNotify(this).show(); } } - // methods from CryptoOperationHelper.Callback - - @Override - public ImportKeyringParcel createOperationInput() { - return new ImportKeyringParcel(mKeyList, mKeyserver); - } - - @Override - public void onCryptoOperationSuccess(ImportKeyResult result) { - handleResult(result); - } - - @Override - public void onCryptoOperationCancelled() { - // do nothing - } - - @Override - public void onCryptoOperationError(ImportKeyResult result) { - handleResult(result); - } - - @Override - public boolean onCryptoSetProgress(String msg, int progress, int max) { - return false; - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java index fb0217cda..7458b2f1c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java @@ -20,39 +20,55 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.database.MatrixCursor; import android.os.Bundle; import android.preference.PreferenceActivity; +import android.provider.BaseColumns; import android.support.v4.app.Fragment; -import android.view.KeyEvent; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.view.MenuItemCompat.OnActionExpandListener; +import android.support.v4.widget.CursorAdapter; +import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v7.widget.SearchView; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.ArrayAdapter; -import android.widget.AutoCompleteTextView; -import android.widget.TextView; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.processing.CloudLoaderState; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListener; import org.sufficientlysecure.keychain.util.ContactHelper; -import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.Preferences.CloudSearchPrefs; +import java.util.ArrayList; import java.util.List; +import static android.support.v7.widget.SearchView.OnQueryTextListener; +import static android.support.v7.widget.SearchView.OnSuggestionListener; + /** * Consists of the search bar, search button, and search settings button */ public class ImportKeysCloudFragment extends Fragment { + public static final String ARG_QUERY = "query"; public static final String ARG_DISABLE_QUERY_EDIT = "disable_query_edit"; public static final String ARG_CLOUD_SEARCH_PREFS = "cloud_search_prefs"; - private ImportKeysActivity mImportActivity; + private static final String CURSOR_SUGGESTION = "suggestion"; - private AutoCompleteTextView mQueryEditText; + private Activity mActivity; + private ImportKeysListener mCallback; + + private List mNamesAndEmails; + private SimpleCursorAdapter mSearchAdapter; + + private List mCurrentSuggestions = new ArrayList<>(); /** * Creates new instance of this fragment @@ -63,8 +79,8 @@ public class ImportKeysCloudFragment extends Fragment { * preferences. */ public static ImportKeysCloudFragment newInstance(String query, boolean disableQueryEdit, - Preferences.CloudSearchPrefs - cloudSearchPrefs) { + CloudSearchPrefs cloudSearchPrefs) { + ImportKeysCloudFragment frag = new ImportKeysCloudFragment(); Bundle args = new Bundle(); @@ -77,92 +93,120 @@ public class ImportKeysCloudFragment extends Fragment { return frag; } - /** - * Inflate the layout for this fragment - */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.import_keys_cloud_fragment, container, false); + public View onCreateView(LayoutInflater i, ViewGroup c, Bundle savedInstanceState) { + ContactHelper contactHelper = new ContactHelper(mActivity); + mNamesAndEmails = contactHelper.getContactNames(); + mNamesAndEmails.addAll(contactHelper.getContactMails()); - mQueryEditText = (AutoCompleteTextView) view.findViewById(R.id.cloud_import_server_query); + mSearchAdapter = new SimpleCursorAdapter(mActivity, + R.layout.import_keys_cloud_suggestions_item, null, new String[]{CURSOR_SUGGESTION}, + new int[]{android.R.id.text1}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); - ContactHelper contactHelper = new ContactHelper(getActivity()); - List namesAndEmails = contactHelper.getContactNames(); - namesAndEmails.addAll(contactHelper.getContactMails()); - mQueryEditText.setThreshold(3); - mQueryEditText.setAdapter( - new ArrayAdapter<> - (getActivity(), android.R.layout.simple_spinner_dropdown_item, - namesAndEmails - ) - ); - - View searchButton = view.findViewById(R.id.cloud_import_server_search); - searchButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - search(mQueryEditText.getText().toString()); - } - }); - - mQueryEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - search(mQueryEditText.getText().toString()); - - // Don't return true to let the keyboard close itself after pressing search - return false; - } - return false; - } - }); - - View configButton = view.findViewById(R.id.cloud_import_server_config_button); - configButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(mImportActivity, SettingsActivity.class); - intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, SettingsActivity.CloudSearchPrefsFragment.class.getName()); - startActivity(intent); - } - }); - - return view; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // set displayed values - if (getArguments() != null) { - String query = getArguments().getString(ARG_QUERY); - if (query != null) { - mQueryEditText.setText(query, TextView.BufferType.EDITABLE); - - Log.d(Constants.TAG, "query: " + query); - } else { - // open keyboard - mQueryEditText.requestFocus(); - toggleKeyboard(true); - } - - if (getArguments().getBoolean(ARG_DISABLE_QUERY_EDIT, false)) { - mQueryEditText.setEnabled(false); - } - } + setHasOptionsMenu(true); + return null; } @Override public void onAttach(Activity activity) { super.onAttach(activity); - mImportActivity = (ImportKeysActivity) activity; + mActivity = activity; + + try { + mCallback = (ImportKeysListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement ImportKeysListener"); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.import_keys_cloud_fragment, menu); + + MenuItem searchItem = menu.findItem(R.id.menu_import_keys_cloud_search); + final SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + searchView.setSuggestionsAdapter(mSearchAdapter); + + searchView.setOnSuggestionListener(new OnSuggestionListener() { + @Override + public boolean onSuggestionSelect(int position) { + searchView.setQuery(mCurrentSuggestions.get(position), true); + return true; + } + + @Override + public boolean onSuggestionClick(int position) { + searchView.setQuery(mCurrentSuggestions.get(position), true); + return true; + } + }); + + searchView.setOnQueryTextListener(new OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + search(searchView.getQuery().toString().trim()); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + updateAdapter(newText); + return false; + } + }); + + MenuItemCompat.setOnActionExpandListener(searchItem, new OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + mActivity.finish(); + return true; + } + }); + + searchItem.expandActionView(); + + super.onCreateOptionsMenu(menu, inflater); + } + + private void updateAdapter(String query) { + mCurrentSuggestions.clear(); + MatrixCursor c = new MatrixCursor(new String[]{BaseColumns._ID, CURSOR_SUGGESTION}); + for (int i = 0; i < mNamesAndEmails.size(); i++) { + String s = mNamesAndEmails.get(i); + if (s.toLowerCase().startsWith(query.toLowerCase())) { + mCurrentSuggestions.add(s); + c.addRow(new Object[]{i, s}); + } + + } + mSearchAdapter.changeCursor(c); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + switch (itemId) { + case R.id.menu_import_keys_cloud_settings: + Intent intent = new Intent(mActivity, SettingsActivity.class); + intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, + SettingsActivity.CloudSearchPrefsFragment.class.getName()); + startActivity(intent); + return true; + } + + return super.onOptionsItemSelected(item); } private void search(String query) { - Preferences.CloudSearchPrefs cloudSearchPrefs + CloudSearchPrefs cloudSearchPrefs = getArguments().getParcelable(ARG_CLOUD_SEARCH_PREFS); // no explicit search preferences passed @@ -170,8 +214,7 @@ public class ImportKeysCloudFragment extends Fragment { cloudSearchPrefs = Preferences.getPreferences(getActivity()).getCloudSearchPrefs(); } - mImportActivity.loadCallback( - new ImportKeysListFragment.CloudLoaderState(query, cloudSearchPrefs)); + mCallback.loadKeys(new CloudLoaderState(query, cloudSearchPrefs)); toggleKeyboard(false); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java index 133cf299f..22223d6af 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java @@ -17,46 +17,41 @@ package org.sufficientlysecure.keychain.ui; -import android.Manifest; import android.app.Activity; -import android.content.ContentResolver; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.keyimport.processing.BytesLoaderState; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListener; import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.ui.ImportKeysListFragment.BytesLoaderState; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.util.PermissionsUtil; import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; public class ImportKeysFileFragment extends Fragment { - private ImportKeysActivity mImportActivity; - private View mBrowse; - private View mClipboardButton; + + private Activity mActivity; + private ImportKeysListener mCallback; private Uri mCurrentUri; private static final int REQUEST_CODE_FILE = 0x00007003; - private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12; /** * Creates new instance of this fragment @@ -70,52 +65,60 @@ public class ImportKeysFileFragment extends Fragment { return frag; } - /** - * Inflate the layout for this fragment - */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.import_keys_file_fragment, container, false); - - mBrowse = view.findViewById(R.id.import_keys_file_browse); - - mBrowse.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // open .asc or .gpg files - // setting it to text/plain prevents Cyanogenmod's file manager from selecting asc - // or gpg types! - FileHelper.openDocument(ImportKeysFileFragment.this, - Uri.fromFile(Constants.Path.APP_DIR), "*/*", false, REQUEST_CODE_FILE); - } - }); - - mClipboardButton = view.findViewById(R.id.import_clipboard_button); - mClipboardButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity()); - String sendText = ""; - if (clipboardText != null) { - sendText = clipboardText.toString(); - sendText = PgpHelper.getPgpKeyContent(sendText); - if (sendText == null) { - Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show(); - return; - } - mImportActivity.loadCallback(new BytesLoaderState(sendText.getBytes(), null)); - } - } - }); - - return view; + public View onCreateView(LayoutInflater i, ViewGroup c, Bundle savedInstanceState) { + setHasOptionsMenu(true); + return null; } @Override public void onAttach(Activity activity) { super.onAttach(activity); - mImportActivity = (ImportKeysActivity) activity; + mActivity = activity; + + try { + mCallback = (ImportKeysListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement ImportKeysListener"); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.import_keys_file_fragment, menu); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + switch (itemId) { + case R.id.menu_import_keys_file_open: + // open .asc or .gpg files + // setting it to text/plain prevents Cyanogenmod's file manager from selecting asc + // or gpg types! + FileHelper.openDocument(ImportKeysFileFragment.this, + Uri.fromFile(Constants.Path.APP_DIR), "*/*", false, REQUEST_CODE_FILE); + return true; + case R.id.menu_import_keys_file_paste: + CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity()); + String sendText = ""; + if (clipboardText != null) { + sendText = clipboardText.toString(); + sendText = PgpHelper.getPgpKeyContent(sendText); + if (sendText == null) { + Notify.create(mActivity, R.string.error_bad_data, Style.ERROR).show(); + } else { + mCallback.loadKeys(new BytesLoaderState(sendText.getBytes(), null)); + } + } + return true; + } + + return super.onOptionsItemSelected(item); } @Override @@ -125,90 +128,49 @@ public class ImportKeysFileFragment extends Fragment { if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { mCurrentUri = data.getData(); - if (checkAndRequestReadPermission(mCurrentUri)) { + if (PermissionsUtil.checkAndRequestReadPermission(this, mCurrentUri)) { startImportingKeys(); } } break; } - default: super.onActivityResult(requestCode, resultCode, data); - - break; } } private void startImportingKeys() { boolean isEncrypted; try { - isEncrypted = FileHelper.isEncryptedFile(mImportActivity, mCurrentUri); + isEncrypted = FileHelper.isEncryptedFile(mActivity, mCurrentUri); } catch (IOException e) { Log.e(Constants.TAG, "Error opening file", e); - Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show(); + Notify.create(mActivity, R.string.error_bad_data, Style.ERROR).show(); return; } if (isEncrypted) { - Intent intent = new Intent(mImportActivity, DecryptActivity.class); + Intent intent = new Intent(mActivity, DecryptActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.setData(mCurrentUri); startActivity(intent); } else { - mImportActivity.loadCallback(new BytesLoaderState(null, mCurrentUri)); + mCallback.loadKeys(new BytesLoaderState(null, mCurrentUri)); } } - /** - * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris. - *

- * This method returns true on Android < 6, or if permission is already granted. It - * requests the permission and returns false otherwise. - *

- * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html - */ - private boolean checkAndRequestReadPermission(final Uri uri) { - if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - return true; - } - - // Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return true; - } - - if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) { - return true; - } - - requestPermissions( - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERMISSION_READ_EXTERNAL_STORAGE); - - return false; - } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - return; - } - - boolean permissionWasGranted = grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED; - - if (permissionWasGranted) { + if (PermissionsUtil.checkReadPermissionResult(mActivity, requestCode, grantResults)) { startImportingKeys(); } else { - Toast.makeText(getActivity(), R.string.error_denied_storage_permission, Toast.LENGTH_LONG).show(); - getActivity().setResult(Activity.RESULT_CANCELED); - getActivity().finish(); + mActivity.setResult(Activity.RESULT_CANCELED); + mActivity.finish(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java index 4d4219f56..c20243a70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -18,47 +18,45 @@ package org.sufficientlysecure.keychain.ui; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import android.Manifest; import android.app.Activity; -import android.content.ContentResolver; -import android.content.pm.PackageManager; +import android.databinding.DataBindingUtil; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; -import android.support.v4.app.ListFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; -import android.support.v4.content.ContextCompat; import android.support.v4.content.Loader; -import android.support.v4.util.LongSparseArray; -import android.view.MotionEvent; +import android.support.v7.widget.LinearLayoutManager; +import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnTouchListener; -import android.widget.ListView; -import android.widget.Toast; +import android.view.View.OnClickListener; +import android.view.ViewGroup; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.databinding.ImportKeysListBasicItemBinding; +import org.sufficientlysecure.keychain.databinding.ImportKeysListFragmentBinding; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.keyimport.processing.AsyncTaskResultWrapper; +import org.sufficientlysecure.keychain.keyimport.processing.BytesLoaderState; +import org.sufficientlysecure.keychain.keyimport.processing.CloudLoaderState; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListCloudLoader; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListLoader; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListener; +import org.sufficientlysecure.keychain.keyimport.processing.LoaderState; import org.sufficientlysecure.keychain.operations.results.GetKeyResult; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; -import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter; -import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListCloudLoader; -import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader; -import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; +import org.sufficientlysecure.keychain.ui.util.PermissionsUtil; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.Preferences.CloudSearchPrefs; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; -public class ImportKeysListFragment extends ListFragment implements +import java.util.ArrayList; + +public class ImportKeysListFragment extends Fragment implements LoaderManager.LoaderCallbacks>> { private static final String ARG_DATA_URI = "uri"; @@ -67,74 +65,26 @@ public class ImportKeysListFragment extends ListFragment implements public static final String ARG_NON_INTERACTIVE = "non_interactive"; public static final String ARG_CLOUD_SEARCH_PREFS = "cloud_search_prefs"; - private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12; + private FragmentActivity mActivity; + private ImportKeysListener mListener; - private Activity mActivity; - private ImportKeysAdapter mAdapter; + private ImportKeysListFragmentBinding mBinding; + private ImportKeysListBasicItemBinding mBindingBasic; private ParcelableProxy mParcelableProxy; + private ImportKeysAdapter mAdapter; private LoaderState mLoaderState; + public static final int STATUS_FIRST = 0; + public static final int STATUS_LOADING = 1; + public static final int STATUS_LOADED = 2; + public static final int STATUS_EMPTY = 3; + private static final int LOADER_ID_BYTES = 0; private static final int LOADER_ID_CLOUD = 1; - private LongSparseArray mCachedKeyData; - private boolean mNonInteractive; - private boolean mShowingOrbotDialog; - public LoaderState getLoaderState() { - return mLoaderState; - } - - public List getData() { - return mAdapter.getData(); - } - - /** - * Returns an Iterator (with size) of the selected data items. - * This iterator is sort of a tradeoff, it's slightly more complex than an - * ArrayList would have been, but we save some memory by just returning - * relevant elements on demand. - */ - public IteratorWithSize getSelectedData() { - final ArrayList entries = getSelectedEntries(); - final Iterator it = entries.iterator(); - return new IteratorWithSize() { - - @Override - public int getSize() { - return entries.size(); - } - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public ParcelableKeyRing next() { - // throws NoSuchElementException if it doesn't exist, but that's not our problem - return mCachedKeyData.get(it.next().hashCode()); - } - - @Override - public void remove() { - it.remove(); - } - }; - } - - public ArrayList getSelectedEntries() { - if (mAdapter != null) { - return mAdapter.getSelectedEntries(); - } else { - Log.e(Constants.TAG, "Adapter not initialized, returning empty list"); - return new ArrayList<>(); - } - - } - /** * Creates an interactive ImportKeyListFragment which reads keyrings from bytes, or file specified * by dataUri, or searches a keyserver for serverQuery, if parameter is not null, in that order @@ -148,7 +98,8 @@ public class ImportKeysListFragment extends ListFragment implements * @return fragment with arguments set based on passed parameters */ public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery, - Preferences.CloudSearchPrefs cloudSearchPrefs) { + CloudSearchPrefs cloudSearchPrefs) { + return newInstance(bytes, dataUri, serverQuery, false, cloudSearchPrefs); } @@ -168,8 +119,7 @@ public class ImportKeysListFragment extends ListFragment implements Uri dataUri, String serverQuery, boolean nonInteractive, - Preferences.CloudSearchPrefs cloudSearchPrefs) { - ImportKeysListFragment frag = new ImportKeysListFragment(); + CloudSearchPrefs cloudSearchPrefs) { Bundle args = new Bundle(); args.putByteArray(ARG_BYTES, bytes); @@ -178,115 +128,70 @@ public class ImportKeysListFragment extends ListFragment implements args.putBoolean(ARG_NON_INTERACTIVE, nonInteractive); args.putParcelable(ARG_CLOUD_SEARCH_PREFS, cloudSearchPrefs); + ImportKeysListFragment frag = new ImportKeysListFragment(); frag.setArguments(args); - return frag; } - static public class LoaderState { - } - - static public class BytesLoaderState extends LoaderState { - public byte[] mKeyBytes; - public Uri mDataUri; - - BytesLoaderState(byte[] keyBytes, Uri dataUri) { - mKeyBytes = keyBytes; - mDataUri = dataUri; - } - } - - static public class CloudLoaderState extends LoaderState { - Preferences.CloudSearchPrefs mCloudPrefs; - String mServerQuery; - - CloudLoaderState(String serverQuery, Preferences.CloudSearchPrefs cloudPrefs) { - mServerQuery = serverQuery; - mCloudPrefs = cloudPrefs; - } - } - - /** - * Define Adapter and Loader on create of Activity - */ @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + mBinding = DataBindingUtil.inflate(inflater, R.layout.import_keys_list_fragment, container, false); + mBinding.setStatus(STATUS_FIRST); + mBindingBasic = mBinding.basic; + View view = mBinding.getRoot(); mActivity = getActivity(); - // Give some text to display if there is no data. - setEmptyText(mActivity.getString(R.string.error_nothing_import)); - - // Create an empty adapter we will use to display the loaded data. - mAdapter = new ImportKeysAdapter(mActivity); - setListAdapter(mAdapter); - Bundle args = getArguments(); Uri dataUri = args.getParcelable(ARG_DATA_URI); byte[] bytes = args.getByteArray(ARG_BYTES); String query = args.getString(ARG_SERVER_QUERY); - mNonInteractive = args.getBoolean(ARG_NON_INTERACTIVE, false); + boolean nonInteractive = args.getBoolean(ARG_NON_INTERACTIVE, false); + mBindingBasic.setNonInteractive(nonInteractive); - getListView().setOnTouchListener(new OnTouchListener() { + // Create an empty adapter we will use to display the loaded data. + mAdapter = new ImportKeysAdapter(mActivity, mListener, nonInteractive); + mBinding.recyclerView.setAdapter(mAdapter); + mBinding.recyclerView.setLayoutManager(new LinearLayoutManager(mActivity)); + + if (dataUri != null || bytes != null) { + loadState(new BytesLoaderState(bytes, dataUri)); + } else if (query != null) { + CloudSearchPrefs cloudSearchPrefs + = args.getParcelable(ARG_CLOUD_SEARCH_PREFS); + if (cloudSearchPrefs == null) { + cloudSearchPrefs = Preferences.getPreferences(mActivity).getCloudSearchPrefs(); + } + loadState(new CloudLoaderState(query, cloudSearchPrefs)); + } + + mBinding.basic.importKeys.setOnClickListener(new OnClickListener() { @Override - public boolean onTouch(View v, MotionEvent event) { - if (!mAdapter.isEmpty()) { - mActivity.onTouchEvent(event); - } - return false; + public void onClick(View view) { + mListener.importKeys(mAdapter.getEntries()); } }); - getListView().setFastScrollEnabled(true); - - if (dataUri != null || bytes != null) { - mLoaderState = new BytesLoaderState(bytes, dataUri); - } else if (query != null) { - Preferences.CloudSearchPrefs cloudSearchPrefs - = args.getParcelable(ARG_CLOUD_SEARCH_PREFS); - if (cloudSearchPrefs == null) { - cloudSearchPrefs = Preferences.getPreferences(getActivity()).getCloudSearchPrefs(); + mBinding.basic.listKeys.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + mBinding.setAdvanced(true); } + }); - mLoaderState = new CloudLoaderState(query, cloudSearchPrefs); - } - - if (dataUri != null && ! checkAndRequestReadPermission(dataUri)) { - return; - } - - restartLoaders(); + return view; } - /** - * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris. - * - * This method returns true on Android < 6, or if permission is already granted. It - * requests the permission and returns false otherwise. - * - * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html - */ - private boolean checkAndRequestReadPermission(final Uri uri) { - if ( ! ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - return true; + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mListener = (ImportKeysListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement ImportKeysListener"); } - - // Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return true; - } - - if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) { - return true; - } - - requestPermissions( - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERMISSION_READ_EXTERNAL_STORAGE); - - return false; } @Override @@ -294,140 +199,99 @@ public class ImportKeysListFragment extends ListFragment implements @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - return; - } - - boolean permissionWasGranted = grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED; - - if (permissionWasGranted) { - // permission granted -> load key + if (PermissionsUtil.checkReadPermissionResult(mActivity, requestCode, grantResults)) { restartLoaders(); } else { - Toast.makeText(getActivity(), R.string.error_denied_storage_permission, Toast.LENGTH_LONG).show(); - getActivity().setResult(Activity.RESULT_CANCELED); - getActivity().finish(); + mActivity.setResult(Activity.RESULT_CANCELED); + mActivity.finish(); } } - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - - if (mNonInteractive) { - return; + /** + * User may want to go back to single card view if he's now in full key list + * Check if we are in full key list and if this import operation supports basic mode + * + * @return true if activity's back pressed can be performed + */ + public boolean onBackPressed() { + boolean advanced = mBinding.getAdvanced(); + if (advanced && mLoaderState.isBasicModeSupported()) { + mBinding.setAdvanced(false); + return false; } - - // Select checkbox! - // Update underlying data and notify adapter of change. The adapter will - // update the view automatically. - - ImportKeysListEntry entry = mAdapter.getItem(position); - entry.setSelected(!entry.isSelected()); - mAdapter.notifyDataSetChanged(); + return true; } - public void loadNew(LoaderState loaderState) { + public void loadState(LoaderState loaderState) { mLoaderState = loaderState; if (mLoaderState instanceof BytesLoaderState) { BytesLoaderState ls = (BytesLoaderState) mLoaderState; - if ( ls.mDataUri != null && ! checkAndRequestReadPermission(ls.mDataUri)) { + if (ls.mDataUri != null && + !PermissionsUtil.checkAndRequestReadPermission(this, ls.mDataUri)) { return; } } + mBinding.setAdvanced(!mLoaderState.isBasicModeSupported()); restartLoaders(); } - public void destroyLoader() { - if (getLoaderManager().getLoader(LOADER_ID_BYTES) != null) { - getLoaderManager().destroyLoader(LOADER_ID_BYTES); - } - if (getLoaderManager().getLoader(LOADER_ID_CLOUD) != null) { - getLoaderManager().destroyLoader(LOADER_ID_CLOUD); - } - if (getView() != null) { - setListShown(true); - } - } - private void restartLoaders() { + LoaderManager loaderManager = getLoaderManager(); + if (mLoaderState instanceof BytesLoaderState) { - // Start out with a progress indicator. - setListShown(false); - - getLoaderManager().restartLoader(LOADER_ID_BYTES, null, this); + loaderManager.restartLoader(LOADER_ID_BYTES, null, this); } else if (mLoaderState instanceof CloudLoaderState) { - // Start out with a progress indicator. - setListShown(false); - - getLoaderManager().restartLoader(LOADER_ID_CLOUD, null, this); + loaderManager.restartLoader(LOADER_ID_CLOUD, null, this); } } @Override - public Loader>> - onCreateLoader(int id, Bundle args) { + public Loader>> onCreateLoader( + int id, Bundle args) { + + Loader>> loader = null; switch (id) { case LOADER_ID_BYTES: { - return new ImportKeysListLoader(mActivity, (BytesLoaderState) mLoaderState); + loader = new ImportKeysListLoader(mActivity, (BytesLoaderState) mLoaderState); + break; } case LOADER_ID_CLOUD: { - CloudLoaderState ls = (CloudLoaderState) mLoaderState; - return new ImportKeysListCloudLoader(getActivity(), ls.mServerQuery, ls.mCloudPrefs, + loader = new ImportKeysListCloudLoader(mActivity, (CloudLoaderState) mLoaderState, mParcelableProxy); + break; } - - default: - return null; } + + if (loader != null) { + mBinding.setStatus(STATUS_LOADING); + } + + return loader; } @Override - public void onLoadFinished(Loader>> loader, - AsyncTaskResultWrapper> data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) + public void onLoadFinished( + Loader>> loader, + AsyncTaskResultWrapper> data) { - Log.d(Constants.TAG, "data: " + data.getResult()); - - // swap in the real data! mAdapter.setData(data.getResult()); - mAdapter.notifyDataSetChanged(); + int size = mAdapter.getItemCount(); - setListAdapter(mAdapter); - - // The list should now be shown. - if (isResumed()) { - setListShown(true); - } else { - setListShownNoAnimation(true); - } - - // free old cached key data - mCachedKeyData = null; + mBinding.setNumber(size); + mBinding.setStatus(size > 0 ? STATUS_LOADED : STATUS_EMPTY); GetKeyResult getKeyResult = (GetKeyResult) data.getOperationResult(); switch (loader.getId()) { case LOADER_ID_BYTES: - - if (getKeyResult.success()) { - // No error - mCachedKeyData = ((ImportKeysListLoader) loader).getParcelableRings(); - } else { - getKeyResult.createNotify(getActivity()).show(); + if (!getKeyResult.success()) { + getKeyResult.createNotify(mActivity).show(); } break; - case LOADER_ID_CLOUD: - - if (getKeyResult.success()) { - // No error - } else if (getKeyResult.isPending()) { + if (getKeyResult.isPending()) { if (getKeyResult.getRequiredInputParcel().mType == RequiredInputParcel.RequiredInputType.ENABLE_ORBOT) { if (mShowingOrbotDialog) { @@ -461,8 +325,7 @@ public class ImportKeysListFragment extends ListFragment implements } }; - if (OrbotHelper.putOrbotInRequiredState(dialogActions, - getActivity())) { + if (OrbotHelper.putOrbotInRequiredState(dialogActions, mActivity)) { // looks like we didn't have to show the // dialog after all mShowingOrbotDialog = false; @@ -473,30 +336,18 @@ public class ImportKeysListFragment extends ListFragment implements new Handler().post(showOrbotDialog); mShowingOrbotDialog = true; } - } else { - getKeyResult.createNotify(getActivity()).show(); + } else if (!getKeyResult.success()) { + getKeyResult.createNotify(mActivity).show(); } break; - - default: - break; } } @Override - public void onLoaderReset(Loader>> loader) { - switch (loader.getId()) { - case LOADER_ID_BYTES: - // Clear the data in the adapter. - mAdapter.clear(); - break; - case LOADER_ID_CLOUD: - // Clear the data in the adapter. - mAdapter.clear(); - break; - default: - break; - } + public void onLoaderReset( + Loader>> loader) { + + mAdapter.clearData(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java index 4c5151fe9..fa91be2e4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java @@ -198,7 +198,7 @@ public class ImportKeysProxyActivity extends FragmentActivity } public void importKeys(String fingerprint) { - ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null); + ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null, null); ArrayList selectedEntries = new ArrayList<>(); selectedEntries.add(keyEntry); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 6b88f4e12..e2ccd041b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -54,6 +54,7 @@ import android.widget.ViewAnimator; import com.getbase.floatingactionbutton.FloatingActionButton; import com.getbase.floatingactionbutton.FloatingActionsMenu; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; @@ -78,6 +79,7 @@ import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Preferences; + import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import se.emilsjolander.stickylistheaders.StickyListHeadersListView; @@ -245,7 +247,7 @@ public class KeyListFragment extends LoaderFragment @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { + boolean checked) { if (checked) { mAdapter.setNewSelection(position, true); } else { @@ -334,7 +336,7 @@ public class KeyListFragment extends LoaderFragment headerCursor.addRow(row); Cursor dataCursor = data; - data = new MergeCursor(new Cursor[] { + data = new MergeCursor(new Cursor[]{ headerCursor, dataCursor }); } @@ -576,7 +578,7 @@ public class KeyListFragment extends LoaderFragment while (cursor.moveToNext()) { byte[] blob = cursor.getBlob(0);//fingerprint column is 0 String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); - ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null); + ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null, null); keyList.add(keyEntry); } mKeyList = keyList; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java index 30612857c..410e511fa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java @@ -30,6 +30,7 @@ import android.widget.NumberPicker; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract; @@ -142,7 +143,7 @@ public class SafeSlingerActivity extends BaseActivity // We parcel this iteratively into a file - anything we can // display here, we should be able to import. ParcelableFileCache cache = - new ParcelableFileCache<>(this, "key_import.pcl"); + new ParcelableFileCache<>(this, ImportOperation.CACHE_FILE_NAME); cache.writeCache(it.size(), it.iterator()); mOperationHelper = diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 7961ab4b6..96f620a4c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -19,11 +19,6 @@ package org.sufficientlysecure.keychain.ui; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; - import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; @@ -89,10 +84,10 @@ import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; +import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; -import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Style; @@ -104,6 +99,11 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + public class ViewKeyActivity extends BaseSecurityTokenActivity implements LoaderManager.LoaderCallbacks, @@ -115,7 +115,9 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements @Retention(RetentionPolicy.SOURCE) @IntDef({REQUEST_QR_FINGERPRINT, REQUEST_BACKUP, REQUEST_CERTIFY, REQUEST_DELETE}) - private @interface RequestType {} + private @interface RequestType { + } + static final int REQUEST_QR_FINGERPRINT = 1; static final int REQUEST_BACKUP = 2; static final int REQUEST_CERTIFY = 3; @@ -565,7 +567,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements private void startBackupActivity() { Intent intent = new Intent(this, BackupActivity.class); - intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[] { mMasterKeyId }); + intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[]{mMasterKeyId}); intent.putExtra(BackupActivity.EXTRA_SECRET, true); startActivity(intent); } @@ -722,7 +724,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements manager.beginTransaction() .addToBackStack("security_token") .replace(R.id.view_key_fragment, frag) - // if this is called while the activity wasn't resumed, just forget it happened + // if this is called while the activity wasn't resumed, just forget it happened .commitAllowingStateLoss(); } }); @@ -1105,7 +1107,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); - ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null); + ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null, null); ArrayList entries = new ArrayList<>(); entries.add(keyEntry); mKeyList = entries; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index 0be64629b..110ecbe9e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -225,7 +225,8 @@ public class ViewKeyAdvActivity extends BaseActivity implements // get key id from MASTER_KEY_ID long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - getSupportActionBar().setSubtitle(KeyFormattingUtils.beautifyKeyIdWithPrefix(this, masterKeyId)); + String formattedKeyId = KeyFormattingUtils.beautifyKeyIdWithPrefix(masterKeyId); + getSupportActionBar().setSubtitle(formattedKeyId); mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; boolean isRevoked = data.getInt(INDEX_IS_REVOKED) > 0; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java index c39881e5b..d540ba0cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java @@ -237,7 +237,8 @@ public class ViewKeyAdvCertsFragment extends LoaderFragment implements TextView wSignerName = (TextView) view.findViewById(R.id.signerName); TextView wSignStatus = (TextView) view.findViewById(R.id.signStatus); - String signerKeyId = KeyFormattingUtils.beautifyKeyIdWithPrefix(getActivity(), cursor.getLong(mIndexSignerKeyId)); + String signerKeyId = KeyFormattingUtils.beautifyKeyIdWithPrefix( + cursor.getLong(mIndexSignerKeyId)); OpenPgpUtils.UserId userId = KeyRing.splitUserId(cursor.getString(mIndexSignerUserId)); if (userId.name != null) { wSignerName.setText(userId.name); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java index a6d311134..c330cc9cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java @@ -17,261 +17,281 @@ package org.sufficientlysecure.keychain.ui.adapter; -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.graphics.Color; -import android.os.Build; +import android.content.Intent; +import android.databinding.DataBindingUtil; +import android.support.v4.app.FragmentActivity; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.databinding.ImportKeysListItemBinding; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListener; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysOperationCallback; +import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysResultListener; import org.sufficientlysecure.keychain.operations.ImportOperation; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.ViewKeyActivity; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableFileCache; +import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; +import java.util.Date; import java.util.List; -import java.util.Map; -public class ImportKeysAdapter extends ArrayAdapter { - protected LayoutInflater mInflater; - protected Activity mActivity; +public class ImportKeysAdapter extends RecyclerView.Adapter implements ImportKeysResultListener { - protected List mData; + private FragmentActivity mActivity; + private ImportKeysResultListener mListener; + private boolean mNonInteractive; - static class ViewHolder { - public TextView mainUserId; - public TextView mainUserIdRest; - public TextView keyId; - public TextView fingerprint; - public TextView algorithm; - public ImageView status; - public View userIdsDivider; - public LinearLayout userIdsList; - public CheckBox checkBox; - } + private List mData; + private KeyState[] mKeyStates; + private int mCurrent; + + private ProviderHelper mProviderHelper; + + public ImportKeysAdapter(FragmentActivity activity, ImportKeysListener listener, + boolean nonInteractive) { - public ImportKeysAdapter(Activity activity) { - super(activity, -1); mActivity = activity; - mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mListener = listener; + mNonInteractive = nonInteractive; + + mProviderHelper = new ProviderHelper(activity); } - @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setData(List data) { + mData = data; - clear(); - if (data != null) { - this.mData = data; + mKeyStates = new KeyState[data.size()]; + for (int i = 0; i < mKeyStates.length; i++) { + mKeyStates[i] = new KeyState(); - // add data to extended ArrayAdapter - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - addAll(data); - } else { - for (ImportKeysListEntry entry : data) { - add(entry); + ImportKeysListEntry entry = mData.get(i); + long keyId = KeyFormattingUtils.convertKeyIdHexToKeyId(entry.getKeyIdHex()); + try { + KeyRing keyRing; + if (entry.isSecretKey()) { + keyRing = mProviderHelper.getCanonicalizedSecretKeyRing(keyId); + } else { + keyRing = mProviderHelper.getCachedPublicKeyRing(keyId); } + mKeyStates[i].mAlreadyPresent = true; + mKeyStates[i].mVerified = keyRing.getVerified() > 0; + } catch (ProviderHelper.NotFoundException | PgpKeyNotFoundException ignored) { } } + + // If there is only one key, get it automatically + if (mData.size() == 1) { + mCurrent = 0; + getKey(mData.get(0), true); + } + + notifyDataSetChanged(); } - public List getData() { - return mData; + public void clearData() { + mData = null; + mKeyStates = null; + notifyDataSetChanged(); } - /** This method returns a list of all selected entries, with public keys sorted + /** + * This method returns a list of all selected entries, with public keys sorted * before secret keys, see ImportOperation for specifics. + * * @see ImportOperation */ - public ArrayList getSelectedEntries() { + public List getEntries() { ArrayList result = new ArrayList<>(); ArrayList secrets = new ArrayList<>(); if (mData == null) { return result; } for (ImportKeysListEntry entry : mData) { - if (entry.isSelected()) { - // add this entry to either the secret or the public list - (entry.isSecretKey() ? secrets : result).add(entry); - } + // add this entry to either the secret or the public list + (entry.isSecretKey() ? secrets : result).add(entry); } // add secret keys at the end of the list result.addAll(secrets); return result; } - @Override - public boolean hasStableIds() { - return true; + public class ViewHolder extends RecyclerView.ViewHolder { + public ImportKeysListItemBinding b; + + public ViewHolder(View view) { + super(view); + b = DataBindingUtil.bind(view); + b.setNonInteractive(mNonInteractive); + } } - public View getView(int position, View convertView, ViewGroup parent) { - ImportKeysListEntry entry = mData.get(position); - Highlighter highlighter = new Highlighter(mActivity, entry.getQuery()); - ViewHolder holder; - if (convertView == null) { - holder = new ViewHolder(); - convertView = mInflater.inflate(R.layout.import_keys_list_item, null); - holder.mainUserId = (TextView) convertView.findViewById(R.id.import_item_user_id); - holder.mainUserIdRest = (TextView) convertView.findViewById(R.id.import_item_user_id_email); - holder.keyId = (TextView) convertView.findViewById(R.id.import_item_key_id); - holder.fingerprint = (TextView) convertView.findViewById(R.id.import_item_fingerprint); - holder.algorithm = (TextView) convertView.findViewById(R.id.import_item_algorithm); - holder.status = (ImageView) convertView.findViewById(R.id.import_item_status); - holder.userIdsDivider = convertView.findViewById(R.id.import_item_status_divider); - holder.userIdsList = (LinearLayout) convertView.findViewById(R.id.import_item_user_ids_list); - holder.checkBox = (CheckBox) convertView.findViewById(R.id.import_item_selected); - convertView.setTag(holder); - } else { - holder = (ViewHolder) convertView.getTag(); - } + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(mActivity); + return new ViewHolder(inflater.inflate(R.layout.import_keys_list_item, parent, false)); + } - // main user id - String userId = entry.getUserIds().get(0); - OpenPgpUtils.UserId userIdSplit = KeyRing.splitUserId(userId); + @Override + public void onBindViewHolder(ViewHolder holder, final int position) { + final ImportKeysListItemBinding b = holder.b; + final ImportKeysListEntry entry = mData.get(position); + b.setEntry(entry); - // name - if (userIdSplit.name != null) { - // show red user id if it is a secret key - if (entry.isSecretKey()) { - holder.mainUserId.setText(mActivity.getString(R.string.secret_key) - + " " + userIdSplit.name); - } else { - holder.mainUserId.setText(highlighter.highlight(userIdSplit.name)); - } - } else { - holder.mainUserId.setText(R.string.user_id_no_name); - } + final KeyState keyState = mKeyStates[position]; + final boolean downloaded = keyState.mDownloaded; + final boolean showed = keyState.mShowed; - // email - if (userIdSplit.email != null) { - holder.mainUserIdRest.setVisibility(View.VISIBLE); - holder.mainUserIdRest.setText(highlighter.highlight(userIdSplit.email)); - } else { - holder.mainUserIdRest.setVisibility(View.GONE); - } - - holder.keyId.setText(KeyFormattingUtils.beautifyKeyIdWithPrefix(getContext(), entry.getKeyIdHex())); - - // don't show full fingerprint on key import - holder.fingerprint.setVisibility(View.GONE); - - if (entry.getAlgorithm() != null) { - holder.algorithm.setText(entry.getAlgorithm()); - holder.algorithm.setVisibility(View.VISIBLE); - } else { - holder.algorithm.setVisibility(View.GONE); - } - - if (entry.isRevoked()) { - KeyFormattingUtils.setStatusImage(getContext(), holder.status, null, State.REVOKED, R.color.key_flag_gray); - } else if (entry.isExpired()) { - KeyFormattingUtils.setStatusImage(getContext(), holder.status, null, State.EXPIRED, R.color.key_flag_gray); - } - - if (entry.isRevoked() || entry.isExpired()) { - holder.status.setVisibility(View.VISIBLE); - - // no more space for algorithm display - holder.algorithm.setVisibility(View.GONE); - - holder.mainUserId.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); - holder.mainUserIdRest.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); - holder.keyId.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); - } else { - holder.status.setVisibility(View.GONE); - holder.algorithm.setVisibility(View.VISIBLE); - - if (entry.isSecretKey()) { - holder.mainUserId.setTextColor(Color.RED); - } else { - holder.mainUserId.setTextColor(FormattingUtils.getColorFromAttr(mActivity, R.attr.colorText)); - } - - holder.mainUserIdRest.setTextColor(FormattingUtils.getColorFromAttr(mActivity, R.attr.colorText)); - holder.keyId.setTextColor(FormattingUtils.getColorFromAttr(mActivity, R.attr.colorText)); - } - - if (entry.getUserIds().size() == 1) { - holder.userIdsList.setVisibility(View.GONE); - holder.userIdsDivider.setVisibility(View.GONE); - } else { - holder.userIdsList.setVisibility(View.VISIBLE); - holder.userIdsDivider.setVisibility(View.VISIBLE); - - // destroyLoader view from holder - holder.userIdsList.removeAllViews(); - - // we want conventional gpg UserIDs first, then Keybase ”proofs” - HashMap> mergedUserIds = entry.getMergedUserIds(); - ArrayList>> sortedIds = new ArrayList>>(mergedUserIds.entrySet()); - java.util.Collections.sort(sortedIds, new java.util.Comparator>>() { - @Override - public int compare(Map.Entry> entry1, Map.Entry> entry2) { - - // sort keybase UserIds after non-Keybase - boolean e1IsKeybase = entry1.getKey().contains(":"); - boolean e2IsKeybase = entry2.getKey().contains(":"); - if (e1IsKeybase != e2IsKeybase) { - return (e1IsKeybase) ? 1 : -1; - } - return entry1.getKey().compareTo(entry2.getKey()); - } - }); - - for (Map.Entry> pair : sortedIds) { - String cUserId = pair.getKey(); - HashSet cEmails = pair.getValue(); - - TextView uidView = (TextView) mInflater.inflate( - R.layout.import_keys_list_entry_user_id, null); - uidView.setText(highlighter.highlight(cUserId)); - uidView.setPadding(0, 0, FormattingUtils.dpToPx(getContext(), 8), 0); - - if (entry.isRevoked() || entry.isExpired()) { - uidView.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); + b.card.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!downloaded) { + mCurrent = position; + getKey(entry, true); } else { - uidView.setTextColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorText)); - } - - holder.userIdsList.addView(uidView); - - for (String email : cEmails) { - TextView emailView = (TextView) mInflater.inflate( - R.layout.import_keys_list_entry_user_id, null); - emailView.setPadding( - FormattingUtils.dpToPx(getContext(), 16), 0, - FormattingUtils.dpToPx(getContext(), 8), 0); - emailView.setText(highlighter.highlight(email)); - - if (entry.isRevoked() || entry.isExpired()) { - emailView.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); - } else { - emailView.setTextColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorText)); - } - - holder.userIdsList.addView(emailView); + changeState(position, !showed); } } + }); + + b.extra.importKey.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + getKey(entry, false); + } + }); + + b.extra.showKey.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + long keyId = KeyFormattingUtils.convertKeyIdHexToKeyId(entry.getKeyIdHex()); + Intent intent = new Intent(mActivity, ViewKeyActivity.class); + intent.setData(KeyRings.buildGenericKeyRingUri(keyId)); + mActivity.startActivity(intent); + } + }); + + b.extraContainer.setVisibility(showed ? View.VISIBLE : View.GONE); + } + + @Override + public int getItemCount() { + return mData != null ? mData.size() : 0; + } + + public void getKey(ImportKeysListEntry entry, boolean skipSave) { + ImportKeyringParcel inputParcel = prepareKeyOperation(entry, skipSave); + ImportKeysResultListener listener = skipSave ? this : mListener; + ImportKeysOperationCallback cb = new ImportKeysOperationCallback(listener, inputParcel); + int message = skipSave ? R.string.progress_downloading : R.string.progress_importing; + CryptoOperationHelper opHelper = new CryptoOperationHelper<>(1, mActivity, cb, message); + opHelper.cryptoOperation(); + } + + private ImportKeyringParcel prepareKeyOperation(ImportKeysListEntry entry, boolean skipSave) { + ArrayList keysList = null; + ParcelableHkpKeyserver keyserver = null; + + ParcelableKeyRing keyRing = entry.getParcelableKeyRing(); + if (keyRing.mBytes != null) { + // instead of giving the entries by Intent extra, cache them into a + // file to prevent Java Binder problems on heavy imports + // read FileImportCache for more info. + try { + // We parcel this iteratively into a file - anything we can + // display here, we should be able to import. + ParcelableFileCache cache = + new ParcelableFileCache<>(mActivity, ImportOperation.CACHE_FILE_NAME); + cache.writeCache(keyRing); + } catch (IOException e) { + Log.e(Constants.TAG, "Problem writing cache file", e); + Notify.create(mActivity, "Problem writing cache file!", Notify.Style.ERROR).show(); + } + } else { + keysList = new ArrayList<>(); + keysList.add(keyRing); + keyserver = entry.getKeyserver(); } - holder.checkBox.setChecked(entry.isSelected()); + return new ImportKeyringParcel(keysList, keyserver, skipSave); + } - return convertView; + @Override + public void handleResult(ImportKeyResult result) { + boolean resultStatus = result.success(); + Log.e(Constants.TAG, "getKey result: " + resultStatus); + if (resultStatus) { + ArrayList canKeyRings = result.mCanonicalizedKeyRings; + if (canKeyRings.size() == 1) { + CanonicalizedKeyRing keyRing = canKeyRings.get(0); + Log.e(Constants.TAG, "Key ID: " + keyRing.getMasterKeyId() + + "| isRev: " + keyRing.isRevoked() + "| isExp: " + keyRing.isExpired()); + + ImportKeysListEntry entry = mData.get(mCurrent); + entry.setUpdated(result.isOkUpdated()); + + mergeEntryWithKey(entry, keyRing); + + mKeyStates[mCurrent].mDownloaded = true; + changeState(mCurrent, true); + } else { + throw new RuntimeException("getKey retrieved more than one key (" + + canKeyRings.size() + ")"); + } + } else { + result.createNotify(mActivity).show(); + } + } + + private void mergeEntryWithKey(ImportKeysListEntry entry, CanonicalizedKeyRing keyRing) { + entry.setRevoked(keyRing.isRevoked()); + entry.setExpired(keyRing.isExpired()); + + Date expectedDate = entry.getDate(); + Date creationDate = keyRing.getCreationDate(); + if (expectedDate == null) { + entry.setDate(creationDate); + } else if (!expectedDate.equals(creationDate)) { + throw new AssertionError("Creation date doesn't match the expected one"); + } + entry.setKeyId(keyRing.getMasterKeyId()); + + ArrayList realUserIdsPlusKeybase = keyRing.getUnorderedUserIds(); + realUserIdsPlusKeybase.addAll(entry.getKeybaseUserIds()); + entry.setUserIds(realUserIdsPlusKeybase); + } + + private class KeyState { + public boolean mAlreadyPresent = false; + public boolean mVerified = false; + + public boolean mDownloaded = false; + public boolean mShowed = false; + } + + private void changeState(int position, boolean showed) { + KeyState keyState = mKeyStates[position]; + keyState.mShowed = showed; + notifyItemChanged(position); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindings.java new file mode 100644 index 000000000..7d0eea5b8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindings.java @@ -0,0 +1,68 @@ +package org.sufficientlysecure.keychain.ui.bindings; + +import android.content.Context; +import android.content.res.Resources; +import android.databinding.BindingAdapter; +import android.graphics.Color; +import android.text.format.DateFormat; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.Highlighter; + +import java.util.Date; + +public class ImportKeysBindings { + + @BindingAdapter({"app:keyUserId", "app:keySecret", "app:keyRevokedOrExpired", "app:query"}) + public static void setUserId(TextView textView, CharSequence userId, boolean secret, + boolean revokedOrExpired, String query) { + + Context context = textView.getContext(); + Resources resources = context.getResources(); + + if (userId == null) + userId = resources.getString(R.string.user_id_no_name); + + if (secret) { + userId = resources.getString(R.string.secret_key) + " " + userId; + } else { + Highlighter highlighter = ImportKeysBindingsUtils.getHighlighter(context, query); + userId = highlighter.highlight(userId); + } + textView.setText(userId); + textView.setTextColor(ImportKeysBindingsUtils.getColor(context, revokedOrExpired)); + + if (secret) { + textView.setTextColor(Color.RED); + } + } + + @BindingAdapter({"app:keyUserEmail", "app:keyRevokedOrExpired", "app:query"}) + public static void setUserEmail(TextView textView, CharSequence userEmail, + boolean revokedOrExpired, String query) { + + Context context = textView.getContext(); + + if (userEmail == null) + userEmail = ""; + + Highlighter highlighter = ImportKeysBindingsUtils.getHighlighter(context, query); + textView.setText(highlighter.highlight(userEmail)); + textView.setTextColor(ImportKeysBindingsUtils.getColor(context, revokedOrExpired)); + } + + @BindingAdapter({"app:keyCreation", "app:keyRevokedOrExpired"}) + public static void setCreation(TextView textView, Date creationDate, boolean revokedOrExpired) { + Context context = textView.getContext(); + + String text = ""; + if (creationDate != null) { + text = DateFormat.getDateFormat(context).format(creationDate); + } + + textView.setText(text); + textView.setTextColor(ImportKeysBindingsUtils.getColor(context, revokedOrExpired)); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java new file mode 100644 index 000000000..7ee8089f3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java @@ -0,0 +1,32 @@ +package org.sufficientlysecure.keychain.ui.bindings; + +import android.content.Context; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.util.LruCache; + +public class ImportKeysBindingsUtils { + + private static LruCache highlighterCache = new LruCache<>(1); + + public static Highlighter getHighlighter(Context context, String query) { + Highlighter highlighter = highlighterCache.get(query); + if (highlighter == null) { + highlighter = new Highlighter(context, query); + highlighterCache.put(query, highlighter); + } + + return highlighter; + } + + public static int getColor(Context context, boolean revokedOrExpired) { + if (revokedOrExpired) { + return context.getResources().getColor(R.color.key_flag_gray); + } else { + return FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysExtraBindings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysExtraBindings.java new file mode 100644 index 000000000..0039bd6d6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysExtraBindings.java @@ -0,0 +1,85 @@ +package org.sufficientlysecure.keychain.ui.bindings; + +import android.content.Context; +import android.content.res.Resources; +import android.databinding.BindingAdapter; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; + +public class ImportKeysExtraBindings { + + @BindingAdapter({"app:keyRevoked", "app:keyExpired"}) + public static void setStatus(ImageView imageView, boolean revoked, boolean expired) { + Context context = imageView.getContext(); + + if (revoked) { + KeyFormattingUtils.setStatusImage(context, imageView, null, + KeyFormattingUtils.State.REVOKED, R.color.key_flag_gray); + } else if (expired) { + KeyFormattingUtils.setStatusImage(context, imageView, null, + KeyFormattingUtils.State.EXPIRED, R.color.key_flag_gray); + } + } + + @BindingAdapter({"app:keyId"}) + public static void setKeyId(TextView textView, String keyId) { + Context context = textView.getContext(); + String text; + if (keyId != null) { + text = KeyFormattingUtils.beautifyKeyId(keyId); + } else { + Resources resources = context.getResources(); + text = resources.getString(R.string.unknown); + } + textView.setText(text); + } + + @BindingAdapter({"app:keyUserIds", "app:query"}) + public static void setUserIds(LinearLayout linearLayout, ArrayList userIds, String query) { + + linearLayout.removeAllViews(); + + if (userIds != null) { + Context context = linearLayout.getContext(); + Highlighter highlighter = ImportKeysBindingsUtils.getHighlighter(context, query); + + ArrayList>> uIds = userIds; + for (Map.Entry> pair : uIds) { + String name = pair.getKey(); + HashSet emails = pair.getValue(); + + LayoutInflater inflater = LayoutInflater.from(context); + + TextView uidView = (TextView) inflater.inflate( + R.layout.import_keys_list_entry_user_id, null); + uidView.setText(highlighter.highlight(name)); + uidView.setPadding(0, 0, FormattingUtils.dpToPx(context, 8), 0); + uidView.setTextColor(FormattingUtils.getColorFromAttr(context, R.attr.colorText)); + linearLayout.addView(uidView); + + for (String email : emails) { + TextView emailView = (TextView) inflater.inflate( + R.layout.import_keys_list_entry_user_id, null); + emailView.setPadding( + FormattingUtils.dpToPx(context, 16), 0, + FormattingUtils.dpToPx(context, 8), 0); + emailView.setText(highlighter.highlight(email)); + emailView.setTextColor(FormattingUtils.getColorFromAttr(context, R.attr.colorText)); + linearLayout.addView(emailView); + } + } + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java index ccaa9d408..2ccf1856f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java @@ -18,7 +18,7 @@ public class Highlighter { mQuery = query; } - public Spannable highlight(String text) { + public Spannable highlight(CharSequence text) { Spannable highlight = Spannable.Factory.getInstance().newSpannable(text); if (mQuery == null) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index 64a960d7b..18fe9c1a0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -30,13 +30,13 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.ViewAnimator; -import org.openintents.openpgp.OpenPgpDecryptionResult; -import org.openintents.openpgp.OpenPgpSignatureResult; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.nist.NISTNamedCurves; import org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.util.encoders.Hex; +import org.openintents.openpgp.OpenPgpDecryptionResult; +import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; @@ -46,6 +46,7 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve; import org.sufficientlysecure.keychain.util.Log; +import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.DigestException; import java.security.MessageDigest; @@ -99,7 +100,7 @@ public class KeyFormattingUtils { default: { if (context != null) { - algorithmStr = context.getResources().getString(R.string.unknown_algorithm); + algorithmStr = context.getResources().getString(R.string.unknown); } else { algorithmStr = "unknown"; } @@ -154,7 +155,7 @@ public class KeyFormattingUtils { default: { if (context != null) { - algorithmStr = context.getResources().getString(R.string.unknown_algorithm); + algorithmStr = context.getResources().getString(R.string.unknown); } else { algorithmStr = "unknown"; } @@ -189,7 +190,7 @@ public class KeyFormattingUtils { */ } if (context != null) { - return context.getResources().getString(R.string.unknown_algorithm); + return context.getResources().getString(R.string.unknown); } else { return "unknown"; } @@ -208,7 +209,7 @@ public class KeyFormattingUtils { return name; } if (context != null) { - return context.getResources().getString(R.string.unknown_algorithm); + return context.getResources().getString(R.string.unknown); } else { return "unknown"; } @@ -272,6 +273,10 @@ public class KeyFormattingUtils { return hexString; } + public static long convertKeyIdHexToKeyId(String hex) { + return new BigInteger(hex.substring(2), 16).longValue(); + } + public static long convertFingerprintToKeyId(byte[] fingerprint) { return ByteBuffer.wrap(fingerprint, 12, 8).getLong(); } @@ -312,12 +317,12 @@ public class KeyFormattingUtils { return beautifyKeyId(convertKeyIdToHex(keyId)); } - public static String beautifyKeyIdWithPrefix(Context context, String idHex) { + public static String beautifyKeyIdWithPrefix(String idHex) { return "Key ID: " + beautifyKeyId(idHex); } - public static String beautifyKeyIdWithPrefix(Context context, long keyId) { - return beautifyKeyIdWithPrefix(context, convertKeyIdToHex(keyId)); + public static String beautifyKeyIdWithPrefix(long keyId) { + return beautifyKeyIdWithPrefix(convertKeyIdToHex(keyId)); } public static SpannableStringBuilder colorizeFingerprint(String fingerprint) { @@ -434,14 +439,19 @@ public class KeyFormattingUtils { public interface StatusHolder { ImageView getEncryptionStatusIcon(); + TextView getEncryptionStatusText(); ImageView getSignatureStatusIcon(); + TextView getSignatureStatusText(); View getSignatureLayout(); + TextView getSignatureUserName(); + TextView getSignatureUserEmail(); + ViewAnimator getSignatureAction(); boolean hasEncrypt(); @@ -450,7 +460,7 @@ public class KeyFormattingUtils { @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result, - boolean processingkeyLookup) { + boolean processingkeyLookup) { if (holder.hasEncrypt()) { OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PermissionsUtil.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PermissionsUtil.java new file mode 100644 index 000000000..e2e965779 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PermissionsUtil.java @@ -0,0 +1,89 @@ +package org.sufficientlysecure.keychain.ui.util; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.R; + + +public class PermissionsUtil { + + private static final int PERMISSION_READ_EXTERNAL_STORAGE = 1; + + /** + * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris. + *

+ * This method returns true on Android < 6, or if permission is already granted. It + * requests the permission and returns false otherwise. + *

+ * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html + */ + @SuppressLint("NewApi") // Api level is checked in checkReadPermission + public static boolean checkAndRequestReadPermission(Activity activity, Uri uri) { + boolean result = checkReadPermission(activity, uri); + if (!result) { + activity.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSION_READ_EXTERNAL_STORAGE); + } + return result; + } + + public static boolean checkAndRequestReadPermission(Fragment fragment, Uri uri) { + boolean result = checkReadPermission(fragment.getContext(), uri); + if (!result) { + fragment.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSION_READ_EXTERNAL_STORAGE); + } + return result; + } + + private static boolean checkReadPermission(Context context, Uri uri) { + if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + return true; + } + + // Additional check due to: + // https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) { + return true; + } + + return false; + } + + public static boolean checkReadPermissionResult(Context context, + int requestCode, + int[] grantResults) { + + if (requestCode != PERMISSION_READ_EXTERNAL_STORAGE) { + return false; + } + + boolean permissionWasGranted = grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED; + + if (permissionWasGranted) { + return true; + } else { + Toast.makeText(context, R.string.error_denied_storage_permission, Toast.LENGTH_LONG) + .show(); + + return false; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java index 0d00b4937..94fae4fdc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java @@ -57,7 +57,8 @@ public class EmailKeyHelper { // Put them in a list and import ArrayList keys = new ArrayList<>(entries.size()); for (ImportKeysListEntry entry : entries) { - keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), entry.getKeyIdHex())); + keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), entry.getKeyIdHex(), null, + null)); } mKeyList = keys; mKeyserver = keyserver; @@ -97,7 +98,7 @@ public class EmailKeyHelper { Set keys = new HashSet<>(); try { for (ImportKeysListEntry key : keyServer.search(mail, proxy)) { - if (key.isRevoked() || key.isExpired()) continue; + if (key.isRevokedOrExpired()) continue; for (String userId : key.getUserIds()) { if (userId.toLowerCase().contains(mail.toLowerCase(Locale.ENGLISH))) { keys.add(key); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IteratorWithSize.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IteratorWithSize.java new file mode 100644 index 000000000..96cf5a47e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IteratorWithSize.java @@ -0,0 +1,17 @@ +package org.sufficientlysecure.keychain.util; + +import java.util.Iterator; + +/** + * An extended iterator interface, which knows the total number of its entries beforehand. + */ +public interface IteratorWithSize extends Iterator { + + /** + * Returns the total number of entries in this iterator. + * + * @return the number of entries in this iterator. + */ + int getSize(); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/LruCache.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/LruCache.java new file mode 100644 index 000000000..3034a2440 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/LruCache.java @@ -0,0 +1,22 @@ +package org.sufficientlysecure.keychain.util; + +import java.util.LinkedHashMap; +import java.util.Map; + + +// Source: http://stackoverflow.com/a/1953516 +public class LruCache extends LinkedHashMap { + + private final int maxEntries; + + public LruCache(final int maxEntries) { + super(maxEntries + 1, 1.0f, true); + this.maxEntries = maxEntries; + } + + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return super.size() > maxEntries; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java index eabbf83b8..d16b2c9c9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java @@ -52,12 +52,40 @@ public class ParcelableFileCache { mFilename = filename; } - public void writeCache(IteratorWithSize it) throws IOException { - writeCache(it.getSize(), it); + public void writeCache(int numEntries, Iterator it) throws IOException { + DataOutputStream oos = getOutputStream(); + + try { + oos.writeInt(numEntries); + while (it.hasNext()) { + writeParcelable(it.next(), oos); + } + } finally { + oos.close(); + } } - public void writeCache(int numEntries, Iterator it) throws IOException { + public void writeCache(E obj) throws IOException { + DataOutputStream oos = getOutputStream(); + try { + oos.writeInt(1); + writeParcelable(obj, oos); + } finally { + oos.close(); + } + } + + private void writeParcelable(E obj, DataOutputStream oos) throws IOException { + Parcel p = Parcel.obtain(); // creating empty parcel object + p.writeParcelable(obj, 0); // saving bundle as parcel + byte[] buf = p.marshall(); + oos.writeInt(buf.length); + oos.write(buf); + p.recycle(); + } + + private DataOutputStream getOutputStream() throws IOException { File cacheDir = mContext.getCacheDir(); if (cacheDir == null) { // https://groups.google.com/forum/#!topic/android-developers/-694j87eXVU @@ -65,29 +93,12 @@ public class ParcelableFileCache { } File tempFile = new File(mContext.getCacheDir(), mFilename); - - - DataOutputStream oos = new DataOutputStream(new FileOutputStream(tempFile)); - - try { - oos.writeInt(numEntries); - - while (it.hasNext()) { - Parcel p = Parcel.obtain(); // creating empty parcel object - p.writeParcelable(it.next(), 0); // saving bundle as parcel - byte[] buf = p.marshall(); - oos.writeInt(buf.length); - oos.write(buf); - p.recycle(); - } - } finally { - oos.close(); - } - + return new DataOutputStream(new FileOutputStream(tempFile)); } /** * Reads from cache file and deletes it afterward. Convenience function for readCache(boolean). + * * @return an IteratorWithSize object containing entries read from the cache file * @throws IOException */ @@ -97,10 +108,11 @@ public class ParcelableFileCache { /** * Reads entries from a cache file and returns an IteratorWithSize object containing the entries + * * @param deleteAfterRead if true, the cache file will be deleted after being read * @return an IteratorWithSize object containing entries read from the cache file * @throws IOException if cache directory/parcel import file does not exist, or a read error - * occurs + * occurs */ public IteratorWithSize readCache(final boolean deleteAfterRead) throws IOException { @@ -205,7 +217,6 @@ public class ParcelableFileCache { } public boolean delete() throws IOException { - File cacheDir = mContext.getCacheDir(); if (cacheDir == null) { // https://groups.google.com/forum/#!topic/android-developers/-694j87eXVU @@ -216,12 +227,4 @@ public class ParcelableFileCache { return tempFile.delete(); } - /** As the name implies, this is an extended iterator interface, which - * knows the total number of its entries beforehand. - */ - public static interface IteratorWithSize extends Iterator { - /** Returns the number of total entries in this iterator. */ - int getSize(); - } - } diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_content_paste_white_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_content_paste_white_24dp.png new file mode 100644 index 000000000..a704a193e Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_content_paste_white_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_content_paste_white_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_content_paste_white_24dp.png new file mode 100644 index 000000000..40d05206e Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_content_paste_white_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_content_paste_white_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_content_paste_white_24dp.png new file mode 100644 index 000000000..8015d5588 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_content_paste_white_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_content_paste_white_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_content_paste_white_24dp.png new file mode 100644 index 000000000..3b6283fd2 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_content_paste_white_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_content_paste_white_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_content_paste_white_24dp.png new file mode 100644 index 000000000..42e5cfbc0 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_content_paste_white_24dp.png differ diff --git a/OpenKeychain/src/main/res/layout/import_keys_activity.xml b/OpenKeychain/src/main/res/layout/import_keys_activity.xml index 6602c4b8f..d3e3ea376 100644 --- a/OpenKeychain/src/main/res/layout/import_keys_activity.xml +++ b/OpenKeychain/src/main/res/layout/import_keys_activity.xml @@ -1,8 +1,7 @@ + android:layout_height="match_parent"> - - - - - - - - + android:layout_height="wrap_content" /> - - - - - - - + diff --git a/OpenKeychain/src/main/res/layout/import_keys_cloud_fragment.xml b/OpenKeychain/src/main/res/layout/import_keys_cloud_fragment.xml deleted file mode 100644 index 1fc8cfdcb..000000000 --- a/OpenKeychain/src/main/res/layout/import_keys_cloud_fragment.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/import_keys_cloud_suggestions_item.xml b/OpenKeychain/src/main/res/layout/import_keys_cloud_suggestions_item.xml new file mode 100644 index 000000000..bd3b9e02a --- /dev/null +++ b/OpenKeychain/src/main/res/layout/import_keys_cloud_suggestions_item.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/import_keys_file_fragment.xml b/OpenKeychain/src/main/res/layout/import_keys_file_fragment.xml deleted file mode 100644 index c1b9eb276..000000000 --- a/OpenKeychain/src/main/res/layout/import_keys_file_fragment.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/import_keys_list_basic_item.xml b/OpenKeychain/src/main/res/layout/import_keys_list_basic_item.xml new file mode 100644 index 000000000..fc2f3ff26 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/import_keys_list_basic_item.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + +