@@ -864,6 +864,11 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Keychain.Transparent"
|
||||
android:label="@string/app_name" />
|
||||
<activity
|
||||
android:name=".remote.ui.dialog.RemoteDeduplicateActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Keychain.Transparent"
|
||||
android:label="@string/app_name" />
|
||||
<activity
|
||||
android:name=".remote.ui.RemoteSelectPubKeyActivity"
|
||||
android:exported="false"
|
||||
|
||||
@@ -62,6 +62,10 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
return getRing().getPublicKey().getFingerprint();
|
||||
}
|
||||
|
||||
public byte[] getRawPrimaryUserId() throws PgpKeyNotFoundException {
|
||||
return getPublicKey().getRawPrimaryUserId();
|
||||
}
|
||||
|
||||
public String getPrimaryUserId() throws PgpKeyNotFoundException {
|
||||
return getPublicKey().getPrimaryUserId();
|
||||
}
|
||||
@@ -136,6 +140,15 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
}
|
||||
}
|
||||
|
||||
public long getSigningId() throws PgpKeyNotFoundException {
|
||||
for(CanonicalizedPublicKey key : publicKeyIterator()) {
|
||||
if (key.canSign() && key.isValid()) {
|
||||
return key.getKeyId();
|
||||
}
|
||||
}
|
||||
throw new PgpKeyNotFoundException("No valid signing key found!");
|
||||
}
|
||||
|
||||
public void encode(OutputStream stream) throws IOException {
|
||||
getRing().encode(stream);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
@@ -27,8 +34,6 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing {
|
||||
|
||||
@@ -84,6 +89,69 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing {
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns a minimized version of this key.
|
||||
*
|
||||
* The minimized version includes:
|
||||
* - the master key
|
||||
* - the current best signing key (if any)
|
||||
* - one encryption key (if any)
|
||||
* - the user id that matches the userIdToKeep parameter, or the primary user id if none matches
|
||||
* each with their most recent binding certificates
|
||||
*/
|
||||
public CanonicalizedPublicKeyRing minimize(@Nullable String userIdToKeep) throws IOException, PgpKeyNotFoundException {
|
||||
CanonicalizedPublicKey masterKey = getPublicKey();
|
||||
PGPPublicKey masterPubKey = masterKey.getPublicKey();
|
||||
boolean userIdStrippedOk = false;
|
||||
if (userIdToKeep != null) {
|
||||
try {
|
||||
masterPubKey = PGPPublicKeyUtils.keepOnlyUserId(masterPubKey, userIdToKeep);
|
||||
userIdStrippedOk = true;
|
||||
} catch (NoSuchElementException e) {
|
||||
// will be handled because userIdStrippedOk is false
|
||||
}
|
||||
}
|
||||
|
||||
if (!userIdStrippedOk) {
|
||||
byte[] rawPrimaryUserId = getRawPrimaryUserId();
|
||||
masterPubKey = PGPPublicKeyUtils.keepOnlyRawUserId(masterPubKey, rawPrimaryUserId);
|
||||
}
|
||||
|
||||
masterPubKey = PGPPublicKeyUtils.keepOnlySelfCertsForUserIds(masterPubKey);
|
||||
masterPubKey = PGPPublicKeyUtils.removeAllUserAttributes(masterPubKey);
|
||||
masterPubKey = PGPPublicKeyUtils.removeAllDirectKeyCerts(masterPubKey);
|
||||
|
||||
PGPPublicKeyRing resultRing = new PGPPublicKeyRing(masterPubKey.getEncoded(), new JcaKeyFingerprintCalculator());
|
||||
|
||||
Long encryptId;
|
||||
try {
|
||||
encryptId = getEncryptId();
|
||||
// only add if this key doesn't coincide with master key
|
||||
if (encryptId != getMasterKeyId()) {
|
||||
CanonicalizedPublicKey encryptKey = getPublicKey(encryptId);
|
||||
PGPPublicKey encryptPubKey = encryptKey.getPublicKey();
|
||||
resultRing = PGPPublicKeyRing.insertPublicKey(resultRing, encryptPubKey);
|
||||
}
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
// no encryption key: can't be reasonably minimized
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
long signingId = getSigningId();
|
||||
// only add if this key doesn't coincide with master or encryption key
|
||||
if (signingId != encryptId && signingId != getMasterKeyId()) {
|
||||
CanonicalizedPublicKey signingKey = getPublicKey(signingId);
|
||||
PGPPublicKey signingPubKey = signingKey.getPublicKey();
|
||||
resultRing = PGPPublicKeyRing.insertPublicKey(resultRing, signingPubKey);
|
||||
}
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
// no signing key: can't be reasonably minimized
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CanonicalizedPublicKeyRing(resultRing, getVerified());
|
||||
}
|
||||
|
||||
/** Create a dummy secret ring from this key */
|
||||
public UncachedKeyRing createDivertSecretRing (byte[] cardAid, long[] subKeyIds) {
|
||||
PGPSecretKeyRing secRing = PGPSecretKeyRing.constructDummyFromPublic(getRing(), cardAid);
|
||||
|
||||
@@ -125,32 +125,37 @@ public class OpenPgpSignatureResultBuilder {
|
||||
}
|
||||
setSignatureKeyCertified(signingRing.getVerified() > 0);
|
||||
|
||||
ArrayList<String> allUserIds = signingRing.getUnorderedUserIds();
|
||||
ArrayList<String> confirmedUserIds;
|
||||
try {
|
||||
ArrayList<String> allUserIds = signingRing.getUnorderedUserIds();
|
||||
ArrayList<String> confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId());
|
||||
setUserIds(allUserIds, confirmedUserIds);
|
||||
|
||||
if (mSenderAddress != null) {
|
||||
if (userIdListContainsAddress(mSenderAddress, confirmedUserIds)) {
|
||||
mSenderStatusResult = SenderStatusResult.USER_ID_CONFIRMED;
|
||||
} else if (userIdListContainsAddress(mSenderAddress, allUserIds)) {
|
||||
mSenderStatusResult = SenderStatusResult.USER_ID_UNCONFIRMED;
|
||||
} else {
|
||||
mSenderStatusResult = SenderStatusResult.USER_ID_MISSING;
|
||||
}
|
||||
} else {
|
||||
mSenderStatusResult = SenderStatusResult.UNKNOWN;
|
||||
}
|
||||
|
||||
confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId());
|
||||
} catch (NotFoundException e) {
|
||||
throw new IllegalStateException("Key didn't exist anymore for user id query!", e);
|
||||
}
|
||||
setUserIds(allUserIds, confirmedUserIds);
|
||||
|
||||
mSenderStatusResult = processSenderStatusResult(allUserIds, confirmedUserIds);
|
||||
|
||||
// either master key is expired/revoked or this specific subkey is expired/revoked
|
||||
setKeyExpired(signingRing.isExpired() || signingKey.isExpired());
|
||||
setKeyRevoked(signingRing.isRevoked() || signingKey.isRevoked());
|
||||
}
|
||||
|
||||
private SenderStatusResult processSenderStatusResult(
|
||||
ArrayList<String> allUserIds, ArrayList<String> confirmedUserIds) {
|
||||
if (mSenderAddress == null) {
|
||||
return SenderStatusResult.UNKNOWN;
|
||||
}
|
||||
|
||||
if (userIdListContainsAddress(mSenderAddress, confirmedUserIds)) {
|
||||
return SenderStatusResult.USER_ID_CONFIRMED;
|
||||
} else if (userIdListContainsAddress(mSenderAddress, allUserIds)) {
|
||||
return SenderStatusResult.USER_ID_UNCONFIRMED;
|
||||
} else {
|
||||
return SenderStatusResult.USER_ID_MISSING;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean userIdListContainsAddress(String senderAddress, ArrayList<String> confirmedUserIds) {
|
||||
for (String rawUserId : confirmedUserIds) {
|
||||
UserId userId = OpenPgpUtils.splitUserId(rawUserId);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector;
|
||||
import org.sufficientlysecure.keychain.util.Utf8Util;
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked") // BouncyCastle doesn't do generics here :(
|
||||
class PGPPublicKeyUtils {
|
||||
|
||||
static PGPPublicKey keepOnlyRawUserId(PGPPublicKey masterPublicKey, byte[] rawUserIdToKeep) {
|
||||
boolean elementToKeepFound = false;
|
||||
|
||||
Iterator<byte[]> it = masterPublicKey.getRawUserIDs();
|
||||
while (it.hasNext()) {
|
||||
byte[] rawUserId = it.next();
|
||||
if (Arrays.equals(rawUserId, rawUserIdToKeep)) {
|
||||
elementToKeepFound = true;
|
||||
} else {
|
||||
masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, rawUserId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!elementToKeepFound) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return masterPublicKey;
|
||||
}
|
||||
|
||||
static PGPPublicKey keepOnlyUserId(PGPPublicKey masterPublicKey, String userIdToKeep) {
|
||||
boolean elementToKeepFound = false;
|
||||
|
||||
Iterator<byte[]> it = masterPublicKey.getRawUserIDs();
|
||||
while (it.hasNext()) {
|
||||
byte[] rawUserId = it.next();
|
||||
String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId);
|
||||
if (userId.contains(userIdToKeep)) {
|
||||
elementToKeepFound = true;
|
||||
} else {
|
||||
masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, rawUserId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!elementToKeepFound) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return masterPublicKey;
|
||||
}
|
||||
|
||||
static PGPPublicKey keepOnlySelfCertsForUserIds(PGPPublicKey masterPubKey) {
|
||||
long masterKeyId = masterPubKey.getKeyID();
|
||||
|
||||
Iterator<byte[]> it = masterPubKey.getRawUserIDs();
|
||||
while (it.hasNext()) {
|
||||
byte[] rawUserId = it.next();
|
||||
masterPubKey = keepOnlySelfCertsForRawUserId(masterPubKey, masterKeyId, rawUserId);
|
||||
}
|
||||
|
||||
return masterPubKey;
|
||||
}
|
||||
|
||||
private static PGPPublicKey keepOnlySelfCertsForRawUserId(
|
||||
PGPPublicKey masterPubKey, long masterKeyId, byte[] rawUserId) {
|
||||
Iterator<PGPSignature> it = masterPubKey.getSignaturesForID(rawUserId);
|
||||
while (it.hasNext()) {
|
||||
PGPSignature sig = it.next();
|
||||
if (sig.getKeyID() != masterKeyId) {
|
||||
masterPubKey = PGPPublicKey.removeCertification(masterPubKey, rawUserId, sig);
|
||||
}
|
||||
}
|
||||
return masterPubKey;
|
||||
}
|
||||
|
||||
static PGPPublicKey removeAllUserAttributes(PGPPublicKey masterPubKey) {
|
||||
Iterator<PGPUserAttributeSubpacketVector> it = masterPubKey.getUserAttributes();
|
||||
|
||||
while (it.hasNext()) {
|
||||
masterPubKey = PGPPublicKey.removeCertification(masterPubKey, it.next());
|
||||
}
|
||||
|
||||
return masterPubKey;
|
||||
}
|
||||
|
||||
static PGPPublicKey removeAllDirectKeyCerts(PGPPublicKey masterPubKey) {
|
||||
Iterator<PGPSignature> it = masterPubKey.getSignaturesOfType(PGPSignature.DIRECT_KEY);
|
||||
|
||||
while (it.hasNext()) {
|
||||
masterPubKey = PGPPublicKey.removeCertification(masterPubKey, it.next());
|
||||
}
|
||||
|
||||
return masterPubKey;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,6 @@
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -105,6 +105,24 @@ public class UncachedPublicKey {
|
||||
*
|
||||
*/
|
||||
public String getPrimaryUserId() {
|
||||
byte[] found = getRawPrimaryUserId();
|
||||
if (found != null) {
|
||||
return Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(found);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the primary user id, as indicated by the public key's self certificates.
|
||||
*
|
||||
* This is an expensive operation, since potentially a lot of certificates (and revocations)
|
||||
* have to be checked, and even then the result is NOT guaranteed to be constant through a
|
||||
* canonicalization operation.
|
||||
*
|
||||
* Returns null if there is no primary user id (as indicated by certificates)
|
||||
*
|
||||
*/
|
||||
public byte[] getRawPrimaryUserId() {
|
||||
byte[] found = null;
|
||||
PGPSignature foundSig = null;
|
||||
// noinspection unchecked
|
||||
@@ -161,11 +179,7 @@ public class UncachedPublicKey {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
return Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(found);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
|
||||
|
||||
public class AutocryptPeerDataAccessObject {
|
||||
private final SimpleContentResolverInterface mQueryInterface;
|
||||
private final String packageName;
|
||||
|
||||
|
||||
public AutocryptPeerDataAccessObject(Context context, String packageName) {
|
||||
this.packageName = packageName;
|
||||
|
||||
final ContentResolver contentResolver = context.getContentResolver();
|
||||
mQueryInterface = new SimpleContentResolverInterface() {
|
||||
@Override
|
||||
public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri contentUri, ContentValues values) {
|
||||
return contentResolver.insert(contentUri, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) {
|
||||
return contentResolver.update(contentUri, values, where, selectionArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri contentUri, String where, String[] selectionArgs) {
|
||||
return contentResolver.delete(contentUri, where, selectionArgs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public AutocryptPeerDataAccessObject(SimpleContentResolverInterface queryInterface, String packageName) {
|
||||
mQueryInterface = queryInterface;
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public Long getMasterKeyIdForAutocryptPeer(String autocryptId) {
|
||||
Cursor cursor = mQueryInterface.query(
|
||||
ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int masterKeyIdColumn = cursor.getColumnIndex(ApiAutocryptPeer.MASTER_KEY_ID);
|
||||
return cursor.getLong(masterKeyIdColumn);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Date getLastSeen(String autocryptId) {
|
||||
Cursor cursor = mQueryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId),
|
||||
null, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_SEEN);
|
||||
return new Date(lastUpdated);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Date getLastSeenKey(String autocryptId) {
|
||||
Cursor cursor = mQueryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId),
|
||||
null, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_SEEN_KEY);
|
||||
return new Date(lastUpdated);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updateToResetState(String autocryptId, Date effectiveDate) {
|
||||
updateAutocryptState(autocryptId, effectiveDate, null, ApiAutocryptPeer.RESET);
|
||||
}
|
||||
|
||||
public void updateToSelectedState(String autocryptId, long masterKeyId) {
|
||||
updateAutocryptState(autocryptId, new Date(), masterKeyId, ApiAutocryptPeer.SELECTED);
|
||||
}
|
||||
|
||||
public void updateToGossipState(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
updateAutocryptState(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP);
|
||||
}
|
||||
|
||||
public void updateToMutualState(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
updateAutocryptState(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.MUTUAL);
|
||||
}
|
||||
|
||||
public void updateToAvailableState(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
updateAutocryptState(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.AVAILABLE);
|
||||
}
|
||||
|
||||
private void updateAutocryptState(String autocryptId, Date date, Long masterKeyId, int status) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId);
|
||||
cv.put(ApiAutocryptPeer.LAST_SEEN, date.getTime());
|
||||
if (masterKeyId != null) {
|
||||
cv.put(ApiAutocryptPeer.LAST_SEEN_KEY, masterKeyId);
|
||||
}
|
||||
cv.put(ApiAutocryptPeer.STATE, status);
|
||||
mQueryInterface.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null);
|
||||
}
|
||||
|
||||
public void delete(String autocryptId) {
|
||||
mQueryInterface.delete(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null);
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
@@ -602,6 +603,7 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
android.util.Log.e(Constants.TAG, "Could not delete file!", e);
|
||||
return false;
|
||||
}
|
||||
mContentResolver.delete(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),null, null);
|
||||
int deletedRows = mContentResolver.delete(KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null);
|
||||
return deletedRows > 0;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,15 @@ public class KeychainContract {
|
||||
String IDENTIFIER = "identifier";
|
||||
}
|
||||
|
||||
interface ApiAutocryptPeerColumns {
|
||||
String PACKAGE_NAME = "package_name";
|
||||
String IDENTIFIER = "identifier";
|
||||
String LAST_SEEN = "last_updated";
|
||||
String LAST_SEEN_KEY = "last_seen_key";
|
||||
String STATE = "state";
|
||||
String MASTER_KEY_ID = "master_key_id";
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PROVIDER_AUTHORITY;
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_INTERNAL = Uri
|
||||
@@ -121,6 +130,11 @@ public class KeychainContract {
|
||||
public static final String BASE_API_APPS = "api_apps";
|
||||
public static final String PATH_ALLOWED_KEYS = "allowed_keys";
|
||||
|
||||
public static final String PATH_BY_PACKAGE_NAME = "by_package_name";
|
||||
public static final String PATH_BY_KEY_ID = "by_key_id";
|
||||
|
||||
public static final String BASE_AUTOCRYPT_PEERS = "autocrypt_peers";
|
||||
|
||||
public static class KeyRings implements BaseColumns, KeysColumns, UserPacketsColumns {
|
||||
public static final String MASTER_KEY_ID = KeysColumns.MASTER_KEY_ID;
|
||||
public static final String IS_REVOKED = KeysColumns.IS_REVOKED;
|
||||
@@ -133,6 +147,7 @@ public class KeychainContract {
|
||||
public static final String HAS_CERTIFY = "has_certify";
|
||||
public static final String HAS_AUTHENTICATE = "has_authenticate";
|
||||
public static final String HAS_DUPLICATE_USER_ID = "has_duplicate_user_id";
|
||||
public static final String API_KNOWN_TO_PACKAGE_NAMES = "known_to_apps";
|
||||
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
@@ -332,6 +347,29 @@ public class KeychainContract {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiAutocryptPeer implements ApiAutocryptPeerColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_AUTOCRYPT_PEERS).build();
|
||||
|
||||
public static final int RESET = 0;
|
||||
public static final int GOSSIP = 1;
|
||||
public static final int SELECTED = 2;
|
||||
public static final int AVAILABLE = 3;
|
||||
public static final int MUTUAL = 4;
|
||||
|
||||
public static Uri buildByKeyUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(uri.getPathSegments().get(1)).build();
|
||||
}
|
||||
|
||||
public static Uri buildByPackageNameAndAutocryptId(String packageName, String autocryptPeer) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName).appendPath(autocryptPeer).build();
|
||||
}
|
||||
|
||||
public static Uri buildByMasterKeyId(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(Long.toString(masterKeyId)).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Certs implements CertsColumns, BaseColumns {
|
||||
public static final String USER_ID = UserPacketsColumns.USER_ID;
|
||||
public static final String NAME = UserPacketsColumns.NAME;
|
||||
|
||||
@@ -33,6 +33,7 @@ import android.provider.BaseColumns;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAllowedKeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeerColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
|
||||
@@ -52,7 +53,7 @@ import org.sufficientlysecure.keychain.util.Log;
|
||||
*/
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "openkeychain.db";
|
||||
private static final int DATABASE_VERSION = 22;
|
||||
private static final int DATABASE_VERSION = 23;
|
||||
private Context mContext;
|
||||
|
||||
public interface Tables {
|
||||
@@ -65,6 +66,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
String API_APPS = "api_apps";
|
||||
String API_ALLOWED_KEYS = "api_allowed_keys";
|
||||
String OVERRIDDEN_WARNINGS = "overridden_warnings";
|
||||
String API_AUTOCRYPT_PEERS = "api_autocrypt_peers";
|
||||
}
|
||||
|
||||
private static final String CREATE_KEYRINGS_PUBLIC =
|
||||
@@ -156,6 +158,20 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_API_AUTOCRYPT_PEERS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.API_AUTOCRYPT_PEERS + " ("
|
||||
+ ApiAutocryptPeerColumns.PACKAGE_NAME + " TEXT NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.IDENTIFIER + " TEXT NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.LAST_SEEN + " INTEGER NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.LAST_SEEN_KEY + " INTEGER NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.STATE + " INTEGER NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.MASTER_KEY_ID + " INTEGER NULL, "
|
||||
+ "PRIMARY KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ", "
|
||||
+ ApiAutocryptPeerColumns.IDENTIFIER + "), "
|
||||
+ "FOREIGN KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ") REFERENCES "
|
||||
+ Tables.API_APPS + "(" + ApiAppsColumns.PACKAGE_NAME + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_API_APPS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " ("
|
||||
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
@@ -199,6 +215,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
||||
db.execSQL(CREATE_OVERRIDDEN_WARNINGS);
|
||||
db.execSQL(CREATE_API_AUTOCRYPT_PEERS);
|
||||
|
||||
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
|
||||
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
||||
@@ -318,8 +335,17 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
case 21:
|
||||
db.execSQL("ALTER TABLE updated_keys ADD COLUMN seen_on_keyservers INTEGER;");
|
||||
|
||||
if (oldVersion == 18 || oldVersion == 19 || oldVersion == 20 || oldVersion == 21) {
|
||||
// no consolidate for now, often crashes!
|
||||
case 22:
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS api_autocrypt_peers ("
|
||||
+ "package_name TEXT NOT NULL, "
|
||||
+ "identifier TEXT NOT NULL, "
|
||||
+ "last_updated INTEGER NOT NULL, "
|
||||
+ "master_key_id INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(package_name, identifier), "
|
||||
+ "FOREIGN KEY(package_name) REFERENCES api_apps(package_name) ON DELETE CASCADE"
|
||||
+ ")");
|
||||
|
||||
if (oldVersion == 18 || oldVersion == 19 || oldVersion == 20 || oldVersion == 21 || oldVersion == 22) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,20 +22,22 @@ import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
|
||||
|
||||
public class KeychainExternalContract {
|
||||
public static final int KEY_STATUS_UNVERIFIED = 1;
|
||||
public static final int KEY_STATUS_VERIFIED = 2;
|
||||
|
||||
// this is in KeychainExternalContract already, but we want to be double
|
||||
// sure this isn't mixed up with the internal one!
|
||||
public static final String CONTENT_AUTHORITY_EXTERNAL = Constants.PROVIDER_AUTHORITY + ".exported";
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_EXTERNAL = Uri
|
||||
.parse("content://" + CONTENT_AUTHORITY_EXTERNAL);
|
||||
|
||||
public static final String BASE_EMAIL_STATUS = "email_status";
|
||||
public static final String BASE_AUTOCRYPT_STATUS = "autocrypt_status";
|
||||
|
||||
public static final int KEY_STATUS_UNAVAILABLE = 0;
|
||||
public static final int KEY_STATUS_UNVERIFIED = 1;
|
||||
public static final int KEY_STATUS_VERIFIED = 2;
|
||||
|
||||
public static class EmailStatus implements BaseColumns {
|
||||
public static final String EMAIL_ADDRESS = "email_address";
|
||||
@@ -50,7 +52,33 @@ public class KeychainExternalContract {
|
||||
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status";
|
||||
}
|
||||
|
||||
private KeychainExternalContract() {
|
||||
public static class AutocryptStatus implements BaseColumns {
|
||||
public static final String ADDRESS = "address";
|
||||
|
||||
public static final String UID_ADDRESS = "uid_address";
|
||||
public static final String UID_KEY_STATUS = "uid_key_status";
|
||||
public static final String UID_MASTER_KEY_ID = "uid_master_key_id";
|
||||
public static final String UID_CANDIDATES = "uid_candidates";
|
||||
|
||||
public static final String AUTOCRYPT_MASTER_KEY_ID = "autocrypt_master_key_id";
|
||||
public static final String AUTOCRYPT_KEY_STATUS = "autocrypt_key_status";
|
||||
public static final String AUTOCRYPT_PEER_STATE = "autocrypt_peer_state";
|
||||
public static final String AUTOCRYPT_LAST_SEEN = "autocrypt_last_seen";
|
||||
public static final String AUTOCRYPT_LAST_SEEN_KEY = "autocrypt_last_seen_key";
|
||||
|
||||
public static final int AUTOCRYPT_PEER_RESET = ApiAutocryptPeer.RESET;
|
||||
public static final int AUTOCRYPT_PEER_GOSSIP = ApiAutocryptPeer.GOSSIP;
|
||||
public static final int AUTOCRYPT_PEER_SELECTED = ApiAutocryptPeer.SELECTED;
|
||||
public static final int AUTOCRYPT_PEER_AVAILABLE = ApiAutocryptPeer.AVAILABLE;
|
||||
public static final int AUTOCRYPT_PEER_MUTUAL = ApiAutocryptPeer.MUTUAL;
|
||||
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon()
|
||||
.appendPath(BASE_AUTOCRYPT_STATUS).build();
|
||||
|
||||
public static final String CONTENT_TYPE =
|
||||
"vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status";
|
||||
}
|
||||
|
||||
private KeychainExternalContract() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,14 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
@@ -35,6 +42,7 @@ import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
@@ -45,11 +53,6 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColu
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class KeychainProvider extends ContentProvider {
|
||||
|
||||
private static final int KEY_RINGS_UNIFIED = 101;
|
||||
@@ -78,6 +81,10 @@ public class KeychainProvider extends ContentProvider {
|
||||
private static final int UPDATED_KEYS = 500;
|
||||
private static final int UPDATED_KEYS_SPECIFIC = 501;
|
||||
|
||||
private static final int AUTOCRYPT_PEERS_BY_MASTER_KEY_ID = 601;
|
||||
private static final int AUTOCRYPT_PEERS_BY_PACKAGE_NAME = 602;
|
||||
private static final int AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID = 603;
|
||||
|
||||
protected UriMatcher mUriMatcher;
|
||||
|
||||
/**
|
||||
@@ -190,6 +197,22 @@ public class KeychainProvider extends ContentProvider {
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/"
|
||||
+ KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS);
|
||||
|
||||
/**
|
||||
* Trust Identity access
|
||||
*
|
||||
* <pre>
|
||||
* trust_ids/by_key_id/_
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" +
|
||||
KeychainContract.PATH_BY_KEY_ID + "/*", AUTOCRYPT_PEERS_BY_MASTER_KEY_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" +
|
||||
KeychainContract.PATH_BY_PACKAGE_NAME + "/*", AUTOCRYPT_PEERS_BY_PACKAGE_NAME);
|
||||
matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" +
|
||||
KeychainContract.PATH_BY_PACKAGE_NAME + "/*/*", AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID);
|
||||
|
||||
|
||||
/**
|
||||
* to access table containing last updated dates of keys
|
||||
*/
|
||||
@@ -321,6 +344,9 @@ public class KeychainProvider extends ContentProvider {
|
||||
projectionMap.put(KeyRings.IS_EXPIRED,
|
||||
"(" + Tables.KEYS + "." + Keys.EXPIRY + " IS NOT NULL AND " + Tables.KEYS + "." + Keys.EXPIRY
|
||||
+ " < " + new Date().getTime() / 1000 + ") AS " + KeyRings.IS_EXPIRED);
|
||||
projectionMap.put(KeyRings.API_KNOWN_TO_PACKAGE_NAMES,
|
||||
"GROUP_CONCAT(aTI." + ApiAutocryptPeer.PACKAGE_NAME + ") AS "
|
||||
+ KeyRings.API_KNOWN_TO_PACKAGE_NAMES);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
if (projection == null) {
|
||||
@@ -389,6 +415,11 @@ public class KeychainProvider extends ContentProvider {
|
||||
+ " AND ( kC." + Keys.EXPIRY + " IS NULL OR kC." + Keys.EXPIRY
|
||||
+ " >= " + new Date().getTime() / 1000 + " )"
|
||||
+ ")" : "")
|
||||
+ (plist.contains(KeyRings.API_KNOWN_TO_PACKAGE_NAMES) ?
|
||||
" LEFT JOIN " + Tables.API_AUTOCRYPT_PEERS + " AS aTI ON ("
|
||||
+"aTI." + Keys.MASTER_KEY_ID
|
||||
+ " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ ")" : "")
|
||||
);
|
||||
qb.appendWhere(Tables.KEYS + "." + Keys.RANK + " = 0");
|
||||
// in case there are multiple verifying certificates
|
||||
@@ -635,6 +666,45 @@ public class KeychainProvider extends ContentProvider {
|
||||
break;
|
||||
}
|
||||
|
||||
case AUTOCRYPT_PEERS_BY_MASTER_KEY_ID:
|
||||
case AUTOCRYPT_PEERS_BY_PACKAGE_NAME:
|
||||
case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: {
|
||||
if (selection != null || selectionArgs != null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
projectionMap.put(ApiAutocryptPeer._ID, "oid AS " + ApiAutocryptPeer._ID);
|
||||
projectionMap.put(ApiAutocryptPeer.PACKAGE_NAME, ApiAutocryptPeer.PACKAGE_NAME);
|
||||
projectionMap.put(ApiAutocryptPeer.IDENTIFIER, ApiAutocryptPeer.IDENTIFIER);
|
||||
projectionMap.put(ApiAutocryptPeer.MASTER_KEY_ID, ApiAutocryptPeer.MASTER_KEY_ID);
|
||||
projectionMap.put(ApiAutocryptPeer.LAST_SEEN, ApiAutocryptPeer.LAST_SEEN);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(Tables.API_AUTOCRYPT_PEERS);
|
||||
|
||||
if (match == AUTOCRYPT_PEERS_BY_MASTER_KEY_ID) {
|
||||
long masterKeyId = Long.parseLong(uri.getLastPathSegment());
|
||||
|
||||
selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " = ?";
|
||||
selectionArgs = new String[] { Long.toString(masterKeyId) };
|
||||
} else if (match == AUTOCRYPT_PEERS_BY_PACKAGE_NAME) {
|
||||
String packageName = uri.getPathSegments().get(2);
|
||||
|
||||
selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = ?";
|
||||
selectionArgs = new String[] { packageName };
|
||||
} else { // AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID
|
||||
String packageName = uri.getPathSegments().get(2);
|
||||
String autocryptPeer = uri.getPathSegments().get(3);
|
||||
|
||||
selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " +
|
||||
Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.IDENTIFIER + " = ?";
|
||||
selectionArgs = new String[] { packageName, autocryptPeer };
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case UPDATED_KEYS:
|
||||
case UPDATED_KEYS_SPECIFIC: {
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
@@ -835,6 +905,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
int count;
|
||||
final int match = mUriMatcher.match(uri);
|
||||
|
||||
ContentResolver contentResolver = getContext().getContentResolver();
|
||||
switch (match) {
|
||||
// dangerous
|
||||
case KEY_RINGS_UNIFIED: {
|
||||
@@ -849,7 +920,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
}
|
||||
// corresponding keys and userIds are deleted by ON DELETE CASCADE
|
||||
count = db.delete(Tables.KEY_RINGS_PUBLIC, selection, selectionArgs);
|
||||
uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1));
|
||||
contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)), null);
|
||||
break;
|
||||
}
|
||||
case KEY_RING_SECRET: {
|
||||
@@ -859,10 +930,43 @@ public class KeychainProvider extends ContentProvider {
|
||||
selection += " AND (" + additionalSelection + ")";
|
||||
}
|
||||
count = db.delete(Tables.KEY_RINGS_SECRET, selection, selectionArgs);
|
||||
uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1));
|
||||
contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)), null);
|
||||
break;
|
||||
}
|
||||
|
||||
case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: {
|
||||
String packageName = uri.getPathSegments().get(2);
|
||||
String autocryptPeer = uri.getPathSegments().get(3);
|
||||
|
||||
String selection = ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " + ApiAutocryptPeer.IDENTIFIER + " = ?";
|
||||
selectionArgs = new String[] { packageName, autocryptPeer };
|
||||
|
||||
Cursor cursor = db.query(Tables.API_AUTOCRYPT_PEERS, new String[] { ApiAutocryptPeer.MASTER_KEY_ID },
|
||||
selection, selectionArgs, null, null, null);
|
||||
Long masterKeyId = null;
|
||||
if (cursor != null && cursor.moveToNext() && !cursor.isNull(0)) {
|
||||
masterKeyId = cursor.getLong(0);
|
||||
}
|
||||
|
||||
count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs);
|
||||
|
||||
if (masterKeyId != null) {
|
||||
contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(masterKeyId), null);
|
||||
}
|
||||
contentResolver.notifyChange(
|
||||
ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptPeer), null);
|
||||
break;
|
||||
}
|
||||
|
||||
case AUTOCRYPT_PEERS_BY_MASTER_KEY_ID:
|
||||
String selection = ApiAutocryptPeer.MASTER_KEY_ID + " = " + uri.getLastPathSegment();
|
||||
if (!TextUtils.isEmpty(additionalSelection)) {
|
||||
selection += " AND (" + additionalSelection + ")";
|
||||
}
|
||||
count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs);
|
||||
contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getLastPathSegment()), null);
|
||||
break;
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME: {
|
||||
count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, additionalSelection),
|
||||
selectionArgs);
|
||||
@@ -878,9 +982,6 @@ public class KeychainProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -892,6 +993,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")");
|
||||
|
||||
final SQLiteDatabase db = getDb().getWritableDatabase();
|
||||
ContentResolver contentResolver = getContext().getContentResolver();
|
||||
|
||||
int count = 0;
|
||||
try {
|
||||
@@ -929,13 +1031,48 @@ public class KeychainProvider extends ContentProvider {
|
||||
db.update(Tables.UPDATED_KEYS, values, null, null);
|
||||
break;
|
||||
}
|
||||
case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: {
|
||||
Long masterKeyId = values.getAsLong(ApiAutocryptPeer.MASTER_KEY_ID);
|
||||
if (masterKeyId == null) {
|
||||
throw new IllegalArgumentException("master_key_id must be a non-null value!");
|
||||
}
|
||||
|
||||
ContentValues actualValues = new ContentValues();
|
||||
String packageName = uri.getPathSegments().get(2);
|
||||
actualValues.put(ApiAutocryptPeer.PACKAGE_NAME, packageName);
|
||||
actualValues.put(ApiAutocryptPeer.IDENTIFIER, uri.getLastPathSegment());
|
||||
actualValues.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId);
|
||||
|
||||
Long newLastSeen = values.getAsLong(ApiAutocryptPeer.LAST_SEEN);
|
||||
if (newLastSeen != null) {
|
||||
actualValues.put(ApiAutocryptPeer.LAST_SEEN, newLastSeen);
|
||||
}
|
||||
|
||||
if (values.containsKey(ApiAutocryptPeer.LAST_SEEN_KEY)) {
|
||||
actualValues.put(ApiAutocryptPeer.LAST_SEEN_KEY,
|
||||
values.getAsLong(ApiAutocryptPeer.LAST_SEEN_KEY));
|
||||
}
|
||||
if (values.containsKey(ApiAutocryptPeer.STATE)) {
|
||||
actualValues.put(ApiAutocryptPeer.STATE,
|
||||
values.getAsLong(ApiAutocryptPeer.STATE));
|
||||
}
|
||||
|
||||
contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(masterKeyId), null);
|
||||
|
||||
try {
|
||||
db.replace(Tables.API_AUTOCRYPT_PEERS, null, actualValues);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.d(Constants.TAG, "Constraint exception on update! Entry already existing?", e);
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.os.Build;
|
||||
import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemoteBackupActivity;
|
||||
import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemoteErrorActivity;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemoteImportKeysActivity;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemotePassphraseDialogActivity;
|
||||
@@ -97,6 +98,16 @@ public class ApiPendingIntentFactory {
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createDeduplicatePendingIntent(String packageName, Intent data, ArrayList<String> duplicateEmails) {
|
||||
Intent intent = new Intent(mContext, RemoteDeduplicateActivity.class);
|
||||
|
||||
intent.putExtra(RemoteDeduplicateActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
intent.putExtra(RemoteDeduplicateActivity.EXTRA_DUPLICATE_EMAILS, duplicateEmails);
|
||||
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
|
||||
PendingIntent createImportFromKeyserverPendingIntent(Intent data, long masterKeyId) {
|
||||
Intent intent = new Intent(mContext, RemoteImportKeysActivity.class);
|
||||
intent.setAction(RemoteImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT);
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.security.AccessControlException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,6 +29,7 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
@@ -39,19 +42,25 @@ import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
|
||||
import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class KeychainExternalProvider extends ContentProvider implements SimpleContentResolverInterface {
|
||||
private static final int EMAIL_STATUS = 101;
|
||||
private static final int EMAIL_STATUS_INTERNAL = 102;
|
||||
|
||||
private static final int AUTOCRYPT_STATUS = 201;
|
||||
private static final int AUTOCRYPT_STATUS_INTERNAL = 202;
|
||||
|
||||
private static final int API_APPS = 301;
|
||||
private static final int API_APPS_BY_PACKAGE_NAME = 302;
|
||||
|
||||
@@ -72,7 +81,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
|
||||
String authority = KeychainExternalContract.CONTENT_AUTHORITY_EXTERNAL;
|
||||
|
||||
/**
|
||||
/*
|
||||
* list email_status
|
||||
*
|
||||
* <pre>
|
||||
@@ -80,7 +89,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS, EMAIL_STATUS);
|
||||
matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS + "/*", EMAIL_STATUS_INTERNAL);
|
||||
|
||||
matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS, AUTOCRYPT_STATUS);
|
||||
matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS + "/*", AUTOCRYPT_STATUS_INTERNAL);
|
||||
|
||||
// can only query status of calling app - for internal use only!
|
||||
matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME);
|
||||
@@ -140,16 +151,8 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
String callingPackageName = mApiPermissionHelper.getCurrentCallingPackage();
|
||||
|
||||
switch (match) {
|
||||
case EMAIL_STATUS_INTERNAL:
|
||||
if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) {
|
||||
throw new AccessControlException("This URI can only be called internally!");
|
||||
}
|
||||
|
||||
// override package name to use any external
|
||||
// callingPackageName = uri.getLastPathSegment();
|
||||
|
||||
case EMAIL_STATUS: {
|
||||
boolean callerIsAllowed = (match == EMAIL_STATUS_INTERNAL) || mApiPermissionHelper.isAllowedIgnoreErrors();
|
||||
boolean callerIsAllowed = mApiPermissionHelper.isAllowedIgnoreErrors();
|
||||
if (!callerIsAllowed) {
|
||||
throw new AccessControlException("An application must register before use of KeychainExternalProvider!");
|
||||
}
|
||||
@@ -215,6 +218,105 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
break;
|
||||
}
|
||||
|
||||
case AUTOCRYPT_STATUS_INTERNAL:
|
||||
if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) {
|
||||
throw new AccessControlException("This URI can only be called internally!");
|
||||
}
|
||||
|
||||
// override package name to use any external
|
||||
callingPackageName = uri.getLastPathSegment();
|
||||
|
||||
case AUTOCRYPT_STATUS: {
|
||||
boolean callerIsAllowed = (match == AUTOCRYPT_STATUS_INTERNAL) || mApiPermissionHelper.isAllowedIgnoreErrors();
|
||||
if (!callerIsAllowed) {
|
||||
throw new AccessControlException("An application must register before use of KeychainExternalProvider!");
|
||||
}
|
||||
|
||||
db.execSQL("CREATE TEMPORARY TABLE " + TEMP_TABLE_QUERIED_ADDRESSES + " (" + TEMP_TABLE_COLUMN_ADDRES + " TEXT);");
|
||||
ContentValues cv = new ContentValues();
|
||||
for (String address : selectionArgs) {
|
||||
cv.put(TEMP_TABLE_COLUMN_ADDRES, address);
|
||||
db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv);
|
||||
}
|
||||
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
projectionMap.put(AutocryptStatus._ID, "email AS _id");
|
||||
projectionMap.put(AutocryptStatus.ADDRESS, // this is actually the queried address
|
||||
TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES + " AS " + AutocryptStatus.ADDRESS);
|
||||
|
||||
projectionMap.put(AutocryptStatus.UID_ADDRESS,
|
||||
Tables.USER_PACKETS + "." + UserPackets.USER_ID + " AS " + AutocryptStatus.UID_ADDRESS);
|
||||
// we take the minimum (>0) here, where "1" is "verified by known secret key", "2" is "self-certified"
|
||||
projectionMap.put(AutocryptStatus.UID_KEY_STATUS, "CASE ( MIN (certs_user_id." + Certs.VERIFIED + " ) ) "
|
||||
// remap to keep this provider contract independent from our internal representation
|
||||
+ " WHEN " + Certs.VERIFIED_SELF + " THEN " + KeychainExternalContract.KEY_STATUS_UNVERIFIED
|
||||
+ " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED
|
||||
+ " WHEN NULL THEN NULL"
|
||||
+ " END AS " + AutocryptStatus.UID_KEY_STATUS);
|
||||
projectionMap.put(AutocryptStatus.UID_MASTER_KEY_ID,
|
||||
Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " AS " + AutocryptStatus.UID_MASTER_KEY_ID);
|
||||
projectionMap.put(AutocryptStatus.UID_CANDIDATES,
|
||||
"COUNT(DISTINCT " + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID +
|
||||
") AS " + AutocryptStatus.UID_CANDIDATES);
|
||||
|
||||
projectionMap.put(AutocryptStatus.AUTOCRYPT_KEY_STATUS, "CASE ( MIN (certs_autocrypt_peer." + Certs.VERIFIED + " ) ) "
|
||||
// remap to keep this provider contract independent from our internal representation
|
||||
+ " WHEN " + Certs.VERIFIED_SELF + " THEN " + KeychainExternalContract.KEY_STATUS_UNVERIFIED
|
||||
+ " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED
|
||||
+ " WHEN NULL THEN NULL"
|
||||
+ " END AS " + AutocryptStatus.AUTOCRYPT_KEY_STATUS);
|
||||
projectionMap.put(AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID,
|
||||
Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " AS " + AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID);
|
||||
projectionMap.put(AutocryptStatus.AUTOCRYPT_PEER_STATE, Tables.API_AUTOCRYPT_PEERS + "." +
|
||||
ApiAutocryptPeer.STATE + " AS " + AutocryptStatus.AUTOCRYPT_LAST_SEEN_KEY);
|
||||
projectionMap.put(AutocryptStatus.AUTOCRYPT_LAST_SEEN, Tables.API_AUTOCRYPT_PEERS + "." +
|
||||
ApiAutocryptPeer.LAST_SEEN + " AS " + AutocryptStatus.AUTOCRYPT_LAST_SEEN);
|
||||
projectionMap.put(AutocryptStatus.AUTOCRYPT_LAST_SEEN_KEY, Tables.API_AUTOCRYPT_PEERS + "." +
|
||||
ApiAutocryptPeer.LAST_SEEN_KEY + " AS " + AutocryptStatus.AUTOCRYPT_LAST_SEEN_KEY);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
if (projection == null) {
|
||||
throw new IllegalArgumentException("Please provide a projection!");
|
||||
}
|
||||
|
||||
qb.setTables(
|
||||
TEMP_TABLE_QUERIED_ADDRESSES
|
||||
+ " LEFT JOIN " + Tables.USER_PACKETS + " ON ("
|
||||
+ Tables.USER_PACKETS + "." + UserPackets.USER_ID + " IS NOT NULL"
|
||||
+ " AND " + Tables.USER_PACKETS + "." + UserPackets.EMAIL + " LIKE " + TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES
|
||||
+ ")"
|
||||
+ " LEFT JOIN " + Tables.CERTS + " AS certs_user_id ON ("
|
||||
+ Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = certs_user_id." + Certs.MASTER_KEY_ID
|
||||
+ " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = certs_user_id." + Certs.RANK
|
||||
+ ")"
|
||||
+ " LEFT JOIN " + Tables.API_AUTOCRYPT_PEERS + " ON ("
|
||||
+ Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.IDENTIFIER + " LIKE queried_addresses.address"
|
||||
+ " AND " + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = \"" + callingPackageName + "\""
|
||||
+ ")"
|
||||
+ " LEFT JOIN " + Tables.CERTS + " AS certs_autocrypt_peer ON ("
|
||||
+ Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " = certs_autocrypt_peer." + Certs.MASTER_KEY_ID
|
||||
+ ")"
|
||||
);
|
||||
// in case there are multiple verifying certificates
|
||||
groupBy = TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES;
|
||||
|
||||
// can't have an expired master key for the uid candidate
|
||||
qb.appendWhere("(EXISTS (SELECT * FROM " + Tables.KEYS + " WHERE "
|
||||
+ Tables.KEYS + "." + Keys.KEY_ID + " = " + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
|
||||
+ " AND " + Tables.KEYS + "." + Keys.IS_REVOKED + " = 0"
|
||||
+ " AND NOT " + "(" + Tables.KEYS + "." + Keys.EXPIRY + " IS NOT NULL AND " + Tables.KEYS + "." + Keys.EXPIRY
|
||||
+ " < " + new Date().getTime() / 1000 + ")"
|
||||
+ ")) OR " + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " IS NULL");
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = AutocryptStatus.ADDRESS;
|
||||
}
|
||||
|
||||
// uri to watch is all /key_rings/
|
||||
uri = KeyRings.CONTENT_URI;
|
||||
break;
|
||||
}
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME: {
|
||||
String requestedPackageName = uri.getLastPathSegment();
|
||||
checkIfPackageBelongsToCaller(getContext(), requestedPackageName);
|
||||
@@ -244,6 +346,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
if (cursor != null) {
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
if (Constants.DEBUG_LOG_DB_QUERIES) {
|
||||
DatabaseUtils.dumpCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG,
|
||||
@@ -277,7 +382,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String additionalSelection, String[] selectionArgs) {
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,14 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate;
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt;
|
||||
import org.openintents.openpgp.IOpenPgpService;
|
||||
import org.openintents.openpgp.OpenPgpDecryptionResult;
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.OpenPgpMetadata;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult.AutocryptPeerResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.operations.BackupOperation;
|
||||
@@ -62,12 +65,17 @@ import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository;
|
||||
import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.AutocryptState;
|
||||
import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResult;
|
||||
import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResultStatus;
|
||||
import org.sufficientlysecure.keychain.service.BackupKeyringParcel;
|
||||
@@ -82,9 +90,10 @@ public class OpenPgpService extends Service {
|
||||
public static final int API_VERSION_WITHOUT_SIGNATURE_ONLY_FLAG = 8;
|
||||
public static final int API_VERSION_WITH_DECRYPTION_RESULT = 8;
|
||||
public static final int API_VERSION_WITH_RESULT_NO_SIGNATURE = 8;
|
||||
public static final int API_VERSION_WITH_AUTOCRYPT = 12;
|
||||
|
||||
public static final List<Integer> SUPPORTED_VERSIONS =
|
||||
Collections.unmodifiableList(Arrays.asList(7, 8, 9, 10, 11));
|
||||
Collections.unmodifiableList(Arrays.asList(7, 8, 9, 10, 11, 12));
|
||||
|
||||
private ApiPermissionHelper mApiPermissionHelper;
|
||||
private KeyRepository mKeyRepository;
|
||||
@@ -95,10 +104,9 @@ public class OpenPgpService extends Service {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this));
|
||||
mKeyRepository = KeyRepository.createDatabaseInteractor(this);
|
||||
mApiDao = new ApiDataAccessObject(this);
|
||||
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, mApiDao);
|
||||
mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext());
|
||||
mKeyIdExtractor = OpenPgpServiceKeyIdExtractor.getInstance(getContentResolver(), mApiPendingIntentFactory);
|
||||
}
|
||||
@@ -192,7 +200,7 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
|
||||
private Intent encryptAndSignImpl(Intent data, InputStream inputStream,
|
||||
OutputStream outputStream, boolean sign) {
|
||||
OutputStream outputStream, boolean sign, boolean isQueryAutocryptStatus) {
|
||||
try {
|
||||
boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
String originalFilename = data.getStringExtra(OpenPgpApi.EXTRA_ORIGINAL_FILENAME);
|
||||
@@ -231,11 +239,10 @@ public class OpenPgpService extends Service {
|
||||
KeyIdResult keyIdResult = mKeyIdExtractor.returnKeyIdsFromIntent(data, false,
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
|
||||
boolean isDryRun = data.getBooleanExtra(OpenPgpApi.EXTRA_DRY_RUN, false);
|
||||
boolean isOpportunistic = data.getBooleanExtra(OpenPgpApi.EXTRA_OPPORTUNISTIC_ENCRYPTION, false);
|
||||
KeyIdResultStatus keyIdResultStatus = keyIdResult.getStatus();
|
||||
if (isDryRun) {
|
||||
return getDryRunStatusResult(keyIdResult);
|
||||
if (isQueryAutocryptStatus) {
|
||||
return getAutocryptStatusResult(keyIdResult);
|
||||
}
|
||||
|
||||
if (keyIdResult.hasKeySelectionPendingIntent()) {
|
||||
@@ -296,39 +303,56 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Intent getDryRunStatusResult(KeyIdResult keyIdResult) {
|
||||
switch (keyIdResult.getStatus()) {
|
||||
case MISSING: {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_ERROR,
|
||||
new OpenPgpError(OpenPgpError.OPPORTUNISTIC_MISSING_KEYS, "missing keys in opportunistic mode"));
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
|
||||
return result;
|
||||
private Intent getAutocryptStatusResult(KeyIdResult keyIdResult) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
|
||||
AutocryptState combinedAutocryptState = keyIdResult.getCombinedAutocryptState();
|
||||
if (combinedAutocryptState == null) {
|
||||
switch (keyIdResult.getStatus()) {
|
||||
case NO_KEYS:
|
||||
case NO_KEYS_ERROR:
|
||||
case MISSING: {
|
||||
result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_UNAVAILABLE);
|
||||
break;
|
||||
}
|
||||
case DUPLICATE: {
|
||||
if (keyIdResult.hasKeySelectionPendingIntent()) {
|
||||
result.putExtra(OpenPgpApi.RESULT_INTENT, keyIdResult.getKeySelectionPendingIntent());
|
||||
}
|
||||
result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE);
|
||||
break;
|
||||
}
|
||||
case OK: {
|
||||
result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case NO_KEYS:
|
||||
case NO_KEYS_ERROR: {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_ERROR,
|
||||
new OpenPgpError(OpenPgpError.NO_USER_IDS, "empty recipient list"));
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
switch (combinedAutocryptState) {
|
||||
case EXTERNAL:
|
||||
case SELECTED:
|
||||
case GOSSIP:
|
||||
case RESET: {
|
||||
result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE);
|
||||
break;
|
||||
}
|
||||
case DUPLICATE: {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, false);
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
return result;
|
||||
case AVAILABLE: {
|
||||
result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_AVAILABLE);
|
||||
break;
|
||||
}
|
||||
case OK: {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, keyIdResult.isAllKeysConfirmed());
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
return result;
|
||||
case MUTUAL: {
|
||||
result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_MUTUAL);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IllegalStateException("unhandled case!");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Intent decryptAndVerifyImpl(Intent data, InputStream inputStream, OutputStream outputStream,
|
||||
@@ -361,6 +385,10 @@ public class OpenPgpService extends Service {
|
||||
byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE);
|
||||
String senderAddress = data.getStringExtra(OpenPgpApi.EXTRA_SENDER_ADDRESS);
|
||||
|
||||
AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(
|
||||
getBaseContext(), mApiPermissionHelper.getCurrentCallingPackage());
|
||||
updateAutocryptPeerStateFromIntent(data, autocryptPeerentityDao);
|
||||
|
||||
PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mKeyRepository, progressable);
|
||||
|
||||
long inputLength = data.getLongExtra(OpenPgpApi.EXTRA_DATA_LENGTH, InputData.UNKNOWN_FILESIZE);
|
||||
@@ -436,7 +464,7 @@ public class OpenPgpService extends Service {
|
||||
if (prioritySecurityProblem.isIdentifiable()) {
|
||||
String identifier = prioritySecurityProblem.getIdentifier();
|
||||
boolean isOverridden = OverriddenWarningsRepository.createOverriddenWarningsRepository(this)
|
||||
.isWarningOverridden(identifier);
|
||||
.isWarningOverridden(identifier);
|
||||
result.putExtra(OpenPgpApi.RESULT_OVERRIDE_CRYPTO_WARNING, isOverridden);
|
||||
}
|
||||
}
|
||||
@@ -446,6 +474,53 @@ public class OpenPgpService extends Service {
|
||||
mApiPendingIntentFactory.createSecurityProblemIntent(packageName, securityProblem, supportOverride));
|
||||
}
|
||||
|
||||
private String updateAutocryptPeerStateFromIntent(Intent data, AutocryptPeerDataAccessObject autocryptPeerDao)
|
||||
throws PgpGeneralException, IOException {
|
||||
String autocryptPeerId = data.getStringExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID);
|
||||
AutocryptPeerUpdate autocryptPeerUpdate = data.getParcelableExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE);
|
||||
if (autocryptPeerUpdate == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long newMasterKeyId;
|
||||
if (autocryptPeerUpdate.hasKeyData()) {
|
||||
UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(autocryptPeerUpdate.getKeyData());
|
||||
if (uncachedKeyRing.isSecret()) {
|
||||
Log.e(Constants.TAG, "Found secret key in autocrypt id! - Ignoring");
|
||||
return null;
|
||||
}
|
||||
// this will merge if the key already exists - no worries!
|
||||
KeyWritableRepository.createDatabaseReadWriteInteractor(this).savePublicKeyRing(uncachedKeyRing);
|
||||
newMasterKeyId = uncachedKeyRing.getMasterKeyId();
|
||||
} else {
|
||||
newMasterKeyId = null;
|
||||
}
|
||||
|
||||
Date lastSeen = autocryptPeerDao.getLastSeen(autocryptPeerId);
|
||||
Date effectiveDate = autocryptPeerUpdate.getEffectiveDate();
|
||||
if (newMasterKeyId == null) {
|
||||
if (effectiveDate.after(lastSeen)) {
|
||||
autocryptPeerDao.updateToResetState(autocryptPeerId, effectiveDate);
|
||||
}
|
||||
return autocryptPeerId;
|
||||
}
|
||||
|
||||
Date lastSeenKey = autocryptPeerDao.getLastSeenKey(autocryptPeerId);
|
||||
if (lastSeenKey != null && effectiveDate.before(lastSeenKey)) {
|
||||
return autocryptPeerId;
|
||||
}
|
||||
|
||||
if (lastSeen == null || effectiveDate.after(lastSeen)) {
|
||||
if (autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL) {
|
||||
autocryptPeerDao.updateToMutualState(autocryptPeerId, effectiveDate, newMasterKeyId);
|
||||
} else {
|
||||
autocryptPeerDao.updateToAvailableState(autocryptPeerId, effectiveDate, newMasterKeyId);
|
||||
}
|
||||
}
|
||||
|
||||
return autocryptPeerId;
|
||||
}
|
||||
|
||||
private void processDecryptionResultForResultIntent(int targetApiVersion, Intent result,
|
||||
OpenPgpDecryptionResult decryptionResult) {
|
||||
if (targetApiVersion < API_VERSION_WITH_DECRYPTION_RESULT) {
|
||||
@@ -528,9 +603,41 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
String autocryptPeerentity = data.getStringExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID);
|
||||
if (autocryptPeerentity != null) {
|
||||
if (targetApiVersion < API_VERSION_WITH_AUTOCRYPT) {
|
||||
throw new IllegalStateException("API version conflict, autocrypt is supported v12 and up!");
|
||||
}
|
||||
signatureResult = processAutocryptPeerInfoToSignatureResult(signatureResult, autocryptPeerentity);
|
||||
}
|
||||
|
||||
result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult);
|
||||
}
|
||||
|
||||
private OpenPgpSignatureResult processAutocryptPeerInfoToSignatureResult(OpenPgpSignatureResult signatureResult,
|
||||
String autocryptPeerentity) {
|
||||
boolean hasValidSignature =
|
||||
signatureResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_CONFIRMED ||
|
||||
signatureResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_UNCONFIRMED;
|
||||
if (!hasValidSignature) {
|
||||
return signatureResult;
|
||||
}
|
||||
|
||||
AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(),
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
Long autocryptPeerMasterKeyId = autocryptPeerentityDao.getMasterKeyIdForAutocryptPeer(autocryptPeerentity);
|
||||
|
||||
long masterKeyId = signatureResult.getKeyId();
|
||||
if (autocryptPeerMasterKeyId == null) {
|
||||
autocryptPeerentityDao.updateToGossipState(autocryptPeerentity, new Date(), masterKeyId);
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.NEW);
|
||||
} else if (masterKeyId == autocryptPeerMasterKeyId) {
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.OK);
|
||||
} else {
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getKeyImpl(Intent data, OutputStream outputStream) {
|
||||
try {
|
||||
long masterKeyId = data.getLongExtra(OpenPgpApi.EXTRA_KEY_ID, 0);
|
||||
@@ -544,6 +651,11 @@ public class OpenPgpService extends Service {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
|
||||
if (data.getBooleanExtra(OpenPgpApi.EXTRA_MINIMIZE, false)) {
|
||||
String userIdToKeep = data.getStringExtra(OpenPgpApi.EXTRA_MINIMIZE_USER_ID);
|
||||
keyRing = keyRing.minimize(userIdToKeep);
|
||||
}
|
||||
|
||||
boolean requestedKeyData = outputStream != null;
|
||||
if (requestedKeyData) {
|
||||
boolean requestAsciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, false);
|
||||
@@ -669,6 +781,27 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private Intent updateAutocryptPeerImpl(Intent data) {
|
||||
try {
|
||||
if (!data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID) ||
|
||||
!data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE)) {
|
||||
throw new IllegalArgumentException("need to specify both autocrypt_peer_id and autocrypt_peer_update!");
|
||||
}
|
||||
|
||||
AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(),
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
|
||||
updateAutocryptPeerStateFromIntent(data, autocryptPeerentityDao);
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
Log.d(Constants.TAG, "exception in updateAutocryptPeerImpl", e);
|
||||
return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Intent checkPermissionImpl(@NonNull Intent data) {
|
||||
Intent permissionIntent = mApiPermissionHelper.isAllowedOrReturnIntent(data);
|
||||
if (permissionIntent != null) {
|
||||
@@ -816,11 +949,13 @@ public class OpenPgpService extends Service {
|
||||
case OpenPgpApi.ACTION_DETACHED_SIGN: {
|
||||
return signImpl(data, inputStream, outputStream, false);
|
||||
}
|
||||
case OpenPgpApi.ACTION_ENCRYPT: {
|
||||
return encryptAndSignImpl(data, inputStream, outputStream, false);
|
||||
case OpenPgpApi.ACTION_QUERY_AUTOCRYPT_STATUS: {
|
||||
return encryptAndSignImpl(data, inputStream, outputStream, false, true);
|
||||
}
|
||||
case OpenPgpApi.ACTION_ENCRYPT:
|
||||
case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: {
|
||||
return encryptAndSignImpl(data, inputStream, outputStream, true);
|
||||
boolean enableSign = action.equals(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
|
||||
return encryptAndSignImpl(data, inputStream, outputStream, enableSign, false);
|
||||
}
|
||||
case OpenPgpApi.ACTION_DECRYPT_VERIFY: {
|
||||
return decryptAndVerifyImpl(data, inputStream, outputStream, false, progressable);
|
||||
@@ -840,6 +975,9 @@ public class OpenPgpService extends Service {
|
||||
case OpenPgpApi.ACTION_BACKUP: {
|
||||
return backupImpl(data, outputStream);
|
||||
}
|
||||
case OpenPgpApi.ACTION_UPDATE_AUTOCRYPT_PEER: {
|
||||
return updateAutocryptPeerImpl(data);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package org.sufficientlysecure.keychain.remote;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentResolver;
|
||||
@@ -16,22 +15,31 @@ import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
|
||||
class OpenPgpServiceKeyIdExtractor {
|
||||
@VisibleForTesting
|
||||
static final String[] PROJECTION_KEY_SEARCH = {
|
||||
"email_address",
|
||||
"master_key_id",
|
||||
"email_status",
|
||||
static final String[] PROJECTION_MAIL_STATUS = {
|
||||
AutocryptStatus.ADDRESS,
|
||||
AutocryptStatus.UID_MASTER_KEY_ID,
|
||||
AutocryptStatus.UID_KEY_STATUS,
|
||||
AutocryptStatus.UID_CANDIDATES,
|
||||
AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS,
|
||||
AutocryptStatus.AUTOCRYPT_PEER_STATE
|
||||
};
|
||||
private static final int INDEX_EMAIL_ADDRESS = 0;
|
||||
private static final int INDEX_MASTER_KEY_ID = 1;
|
||||
private static final int INDEX_EMAIL_STATUS = 2;
|
||||
private static final int INDEX_USER_ID_STATUS = 2;
|
||||
private static final int INDEX_USER_ID_CANDIDATES = 3;
|
||||
private static final int INDEX_AUTOCRYPT_MASTER_KEY_ID = 4;
|
||||
private static final int INDEX_AUTOCRYPT_KEY_STATUS = 5;
|
||||
private static final int INDEX_AUTOCRYPT_PEER_STATE = 6;
|
||||
|
||||
|
||||
private final ApiPendingIntentFactory apiPendingIntentFactory;
|
||||
@@ -58,7 +66,7 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
for (long keyId : data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS_SELECTED)) {
|
||||
encryptKeyIds.add(keyId);
|
||||
}
|
||||
result = createKeysOkResult(encryptKeyIds, false);
|
||||
result = createKeysOkResult(encryptKeyIds, false, null);
|
||||
} else if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS) || askIfNoUserIdsProvided) {
|
||||
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
|
||||
result = returnKeyIdsFromEmails(data, userIds, callingPackageName);
|
||||
@@ -85,32 +93,54 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
HashSet<Long> keyIds = new HashSet<>();
|
||||
ArrayList<String> missingEmails = new ArrayList<>();
|
||||
ArrayList<String> duplicateEmails = new ArrayList<>();
|
||||
AutocryptState combinedAutocryptState = null;
|
||||
|
||||
if (hasAddresses) {
|
||||
HashMap<String, UserIdStatus> keyRows = getStatusMapForQueriedAddresses(encryptionAddresses, callingPackageName);
|
||||
HashMap<String, AddressQueryResult> userIdEntries = getStatusMapForQueriedAddresses(
|
||||
encryptionAddresses, callingPackageName);
|
||||
|
||||
boolean anyKeyNotVerified = false;
|
||||
for (Entry<String, UserIdStatus> entry : keyRows.entrySet()) {
|
||||
String queriedAddress = entry.getKey();
|
||||
UserIdStatus userIdStatus = entry.getValue();
|
||||
for (String queriedAddress : encryptionAddresses) {
|
||||
AddressQueryResult addressQueryResult = userIdEntries.get(queriedAddress);
|
||||
if (addressQueryResult == null) {
|
||||
throw new IllegalStateException("No result for address - shouldn't happen!");
|
||||
}
|
||||
|
||||
if (addressQueryResult.autocryptMasterKeyId != null) {
|
||||
keyIds.add(addressQueryResult.autocryptMasterKeyId);
|
||||
|
||||
if (addressQueryResult.autocryptKeyStatus != KeychainExternalContract.KEY_STATUS_VERIFIED) {
|
||||
anyKeyNotVerified = true;
|
||||
}
|
||||
|
||||
if (combinedAutocryptState == null) {
|
||||
combinedAutocryptState = addressQueryResult.autocryptState;
|
||||
} else {
|
||||
combinedAutocryptState = combinedAutocryptState.combineWith(addressQueryResult.autocryptState);
|
||||
}
|
||||
|
||||
if (userIdStatus.masterKeyId == null) {
|
||||
missingEmails.add(queriedAddress);
|
||||
continue;
|
||||
}
|
||||
|
||||
keyIds.add(userIdStatus.masterKeyId);
|
||||
if (addressQueryResult.uidMasterKeyId != null) {
|
||||
keyIds.add(addressQueryResult.uidMasterKeyId);
|
||||
combinedAutocryptState = AutocryptState.EXTERNAL;
|
||||
|
||||
if (userIdStatus.hasDuplicate) {
|
||||
duplicateEmails.add(queriedAddress);
|
||||
if (addressQueryResult.uidHasMultipleCandidates) {
|
||||
duplicateEmails.add(queriedAddress);
|
||||
}
|
||||
|
||||
if (addressQueryResult.uidKeyStatus != KeychainExternalContract.KEY_STATUS_VERIFIED) {
|
||||
anyKeyNotVerified = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!userIdStatus.verified) {
|
||||
anyKeyNotVerified = true;
|
||||
}
|
||||
missingEmails.add(queriedAddress);
|
||||
}
|
||||
|
||||
if (keyRows.size() != encryptionAddresses.length) {
|
||||
if (userIdEntries.size() != encryptionAddresses.length) {
|
||||
Log.e(Constants.TAG, "Number of rows doesn't match number of retrieved rows! Probably a bug?");
|
||||
}
|
||||
|
||||
@@ -122,14 +152,14 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
}
|
||||
|
||||
if (!duplicateEmails.isEmpty()) {
|
||||
return createDuplicateKeysResult(data, keyIds, missingEmails, duplicateEmails);
|
||||
return createDuplicateKeysResult(data, callingPackageName, duplicateEmails);
|
||||
}
|
||||
|
||||
if (keyIds.isEmpty()) {
|
||||
return createNoKeysResult(data, keyIds, missingEmails, duplicateEmails);
|
||||
}
|
||||
|
||||
return createKeysOkResult(keyIds, allKeysConfirmed);
|
||||
return createKeysOkResult(keyIds, allKeysConfirmed, combinedAutocryptState);
|
||||
}
|
||||
|
||||
/** This method queries the KeychainExternalProvider for all addresses given in encryptionUserIds.
|
||||
@@ -138,10 +168,10 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
* verification status exist, the first one is returned and marked as having a duplicate.
|
||||
*/
|
||||
@NonNull
|
||||
private HashMap<String, UserIdStatus> getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
|
||||
HashMap<String,UserIdStatus> keyRows = new HashMap<>();
|
||||
Uri queryUri = EmailStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build();
|
||||
Cursor cursor = contentResolver.query(queryUri, PROJECTION_KEY_SEARCH, null, encryptionUserIds, null);
|
||||
private HashMap<String, AddressQueryResult> getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
|
||||
HashMap<String,AddressQueryResult> keyRows = new HashMap<>();
|
||||
Uri queryUri = AutocryptStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build();
|
||||
Cursor cursor = contentResolver.query(queryUri, PROJECTION_MAIL_STATUS, null, encryptionUserIds, null);
|
||||
if (cursor == null) {
|
||||
throw new IllegalStateException("Internal error, received null cursor!");
|
||||
}
|
||||
@@ -149,24 +179,21 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
String queryAddress = cursor.getString(INDEX_EMAIL_ADDRESS);
|
||||
Long masterKeyId = cursor.isNull(INDEX_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
boolean isVerified = cursor.getInt(INDEX_EMAIL_STATUS) == KeychainExternalContract.KEY_STATUS_VERIFIED;
|
||||
UserIdStatus userIdStatus = new UserIdStatus(masterKeyId, isVerified);
|
||||
Long uidMasterKeyId =
|
||||
cursor.isNull(INDEX_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
int uidKeyStatus = cursor.getInt(INDEX_USER_ID_STATUS);
|
||||
boolean uidHasMultipleCandidates = cursor.getInt(INDEX_USER_ID_CANDIDATES) > 1;
|
||||
|
||||
boolean seenBefore = keyRows.containsKey(queryAddress);
|
||||
if (!seenBefore) {
|
||||
keyRows.put(queryAddress, userIdStatus);
|
||||
continue;
|
||||
}
|
||||
Long autocryptMasterKeyId =
|
||||
cursor.isNull(INDEX_AUTOCRYPT_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_AUTOCRYPT_MASTER_KEY_ID);
|
||||
int autocryptKeyStatus = cursor.getInt(INDEX_AUTOCRYPT_KEY_STATUS);
|
||||
int autocryptPeerStatus = cursor.getInt(INDEX_AUTOCRYPT_PEER_STATE);
|
||||
|
||||
UserIdStatus previousUserIdStatus = keyRows.get(queryAddress);
|
||||
if (previousUserIdStatus.masterKeyId == null) {
|
||||
keyRows.put(queryAddress, userIdStatus);
|
||||
} else if (!previousUserIdStatus.verified && userIdStatus.verified) {
|
||||
keyRows.put(queryAddress, userIdStatus);
|
||||
} else if (previousUserIdStatus.verified == userIdStatus.verified) {
|
||||
previousUserIdStatus.hasDuplicate = true;
|
||||
}
|
||||
AddressQueryResult status = new AddressQueryResult(
|
||||
uidMasterKeyId, uidKeyStatus, uidHasMultipleCandidates, autocryptMasterKeyId,
|
||||
autocryptKeyStatus, AutocryptState.fromDbValue(autocryptPeerStatus));
|
||||
|
||||
keyRows.put(queryAddress, status);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
@@ -174,14 +201,65 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
return keyRows;
|
||||
}
|
||||
|
||||
private static class UserIdStatus {
|
||||
private final Long masterKeyId;
|
||||
private final boolean verified;
|
||||
private boolean hasDuplicate;
|
||||
private static class AddressQueryResult {
|
||||
private final Long uidMasterKeyId;
|
||||
private final int uidKeyStatus;
|
||||
private boolean uidHasMultipleCandidates;
|
||||
private final Long autocryptMasterKeyId;
|
||||
private final int autocryptKeyStatus;
|
||||
private final AutocryptState autocryptState;
|
||||
|
||||
UserIdStatus(Long masterKeyId, boolean verified) {
|
||||
this.masterKeyId = masterKeyId;
|
||||
this.verified = verified;
|
||||
AddressQueryResult(Long uidMasterKeyId, int uidKeyStatus, boolean uidHasMultipleCandidates, Long autocryptMasterKeyId,
|
||||
int autocryptKeyStatus, AutocryptState autocryptState) {
|
||||
this.uidMasterKeyId = uidMasterKeyId;
|
||||
this.uidKeyStatus = uidKeyStatus;
|
||||
this.uidHasMultipleCandidates = uidHasMultipleCandidates;
|
||||
this.autocryptMasterKeyId = autocryptMasterKeyId;
|
||||
this.autocryptKeyStatus = autocryptKeyStatus;
|
||||
this.autocryptState = autocryptState;
|
||||
}
|
||||
}
|
||||
|
||||
enum AutocryptState {
|
||||
EXTERNAL, RESET, GOSSIP, SELECTED, AVAILABLE, MUTUAL;
|
||||
|
||||
static AutocryptState fromDbValue(int state) {
|
||||
switch (state) {
|
||||
case ApiAutocryptPeer.RESET:
|
||||
return RESET;
|
||||
case ApiAutocryptPeer.AVAILABLE:
|
||||
return AVAILABLE;
|
||||
case ApiAutocryptPeer.SELECTED:
|
||||
return SELECTED;
|
||||
case ApiAutocryptPeer.GOSSIP:
|
||||
return GOSSIP;
|
||||
case ApiAutocryptPeer.MUTUAL:
|
||||
return MUTUAL;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
public AutocryptState combineWith(AutocryptState other) {
|
||||
if (this == EXTERNAL || other == EXTERNAL) {
|
||||
return EXTERNAL;
|
||||
}
|
||||
if (this == RESET || other == RESET) {
|
||||
return RESET;
|
||||
}
|
||||
if (this == GOSSIP || other == GOSSIP) {
|
||||
return GOSSIP;
|
||||
}
|
||||
if (this == SELECTED || other == SELECTED) {
|
||||
return SELECTED;
|
||||
}
|
||||
if (this == AVAILABLE || other == AVAILABLE) {
|
||||
return AVAILABLE;
|
||||
}
|
||||
if (this == MUTUAL && other == MUTUAL) {
|
||||
return MUTUAL;
|
||||
}
|
||||
throw new IllegalStateException("Bug: autocrypt states can't be combined!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +269,7 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
private final HashSet<Long> mExplicitKeyIds;
|
||||
private final KeyIdResultStatus mStatus;
|
||||
private final boolean mAllKeysConfirmed;
|
||||
private final AutocryptState mCombinedAutocryptState;
|
||||
|
||||
private KeyIdResult(PendingIntent keySelectionPendingIntent, KeyIdResultStatus keyIdResultStatus) {
|
||||
mKeySelectionPendingIntent = keySelectionPendingIntent;
|
||||
@@ -198,13 +277,17 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
mAllKeysConfirmed = false;
|
||||
mStatus = keyIdResultStatus;
|
||||
mExplicitKeyIds = null;
|
||||
mCombinedAutocryptState = null;
|
||||
}
|
||||
private KeyIdResult(HashSet<Long> keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus) {
|
||||
|
||||
private KeyIdResult(HashSet<Long> keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus,
|
||||
AutocryptState combinedAutocryptState) {
|
||||
mKeySelectionPendingIntent = null;
|
||||
mUserKeyIds = keyIds;
|
||||
mAllKeysConfirmed = allKeysConfirmed;
|
||||
mStatus = keyIdResultStatus;
|
||||
mExplicitKeyIds = null;
|
||||
mCombinedAutocryptState = combinedAutocryptState;
|
||||
}
|
||||
|
||||
private KeyIdResult(KeyIdResult keyIdResult, HashSet<Long> explicitKeyIds) {
|
||||
@@ -213,6 +296,7 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
mAllKeysConfirmed = keyIdResult.mAllKeysConfirmed;
|
||||
mStatus = keyIdResult.mStatus;
|
||||
mExplicitKeyIds = explicitKeyIds;
|
||||
mCombinedAutocryptState = keyIdResult.mCombinedAutocryptState;
|
||||
}
|
||||
|
||||
boolean hasKeySelectionPendingIntent() {
|
||||
@@ -254,14 +338,19 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
KeyIdResultStatus getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
public AutocryptState getCombinedAutocryptState() {
|
||||
return mCombinedAutocryptState;
|
||||
}
|
||||
}
|
||||
|
||||
enum KeyIdResultStatus {
|
||||
OK, MISSING, DUPLICATE, NO_KEYS, NO_KEYS_ERROR
|
||||
}
|
||||
|
||||
private KeyIdResult createKeysOkResult(HashSet<Long> encryptKeyIds, boolean allKeysConfirmed) {
|
||||
return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK);
|
||||
private KeyIdResult createKeysOkResult(HashSet<Long> encryptKeyIds, boolean allKeysConfirmed,
|
||||
AutocryptState combinedAutocryptState) {
|
||||
return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK, combinedAutocryptState);
|
||||
}
|
||||
|
||||
private KeyIdResult createNoKeysResult(Intent data,
|
||||
@@ -273,11 +362,9 @@ class OpenPgpServiceKeyIdExtractor {
|
||||
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.NO_KEYS);
|
||||
}
|
||||
|
||||
private KeyIdResult createDuplicateKeysResult(Intent data,
|
||||
HashSet<Long> selectedKeyIds, ArrayList<String> missingEmails, ArrayList<String> duplicateEmails) {
|
||||
long[] keyIdsArray = KeyFormattingUtils.getUnboxedLongArray(selectedKeyIds);
|
||||
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
|
||||
data, keyIdsArray, missingEmails, duplicateEmails, false);
|
||||
private KeyIdResult createDuplicateKeysResult(Intent data, String packageName, ArrayList<String> duplicateEmails) {
|
||||
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createDeduplicatePendingIntent(
|
||||
packageName, data, duplicateEmails);
|
||||
|
||||
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.DUPLICATE);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui.dialog;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo;
|
||||
|
||||
|
||||
public class KeyLoader extends AsyncTaskLoader<List<KeyInfo>> {
|
||||
// These are the rows that we will retrieve.
|
||||
private String[] QUERY_PROJECTION = new String[]{
|
||||
KeyRings._ID,
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
KeyRings.CREATION,
|
||||
KeyRings.HAS_ENCRYPT,
|
||||
KeyRings.VERIFIED,
|
||||
KeyRings.NAME,
|
||||
KeyRings.EMAIL,
|
||||
KeyRings.COMMENT,
|
||||
KeyRings.IS_EXPIRED,
|
||||
KeyRings.IS_REVOKED,
|
||||
};
|
||||
private static final int INDEX_MASTER_KEY_ID = 1;
|
||||
private static final int INDEX_CREATION = 2;
|
||||
private static final int INDEX_HAS_ENCRYPT = 3;
|
||||
private static final int INDEX_VERIFIED = 4;
|
||||
private static final int INDEX_NAME = 5;
|
||||
private static final int INDEX_EMAIL = 6;
|
||||
private static final int INDEX_COMMENT = 7;
|
||||
|
||||
private static final String QUERY_WHERE = Tables.KEYS + "." + KeyRings.IS_REVOKED +
|
||||
" = 0 AND " + KeyRings.IS_EXPIRED + " = 0";
|
||||
private static final String QUERY_ORDER = Tables.KEYS + "." + KeyRings.CREATION + " DESC";
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
private final String emailAddress;
|
||||
|
||||
private List<KeyInfo> cachedResult;
|
||||
|
||||
|
||||
KeyLoader(Context context, ContentResolver contentResolver, String emailAddress) {
|
||||
super(context);
|
||||
|
||||
this.contentResolver = contentResolver;
|
||||
this.emailAddress = emailAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyInfo> loadInBackground() {
|
||||
ArrayList<KeyInfo> keyInfos = new ArrayList<>();
|
||||
|
||||
Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsFindByEmailUri(emailAddress),
|
||||
QUERY_PROJECTION, QUERY_WHERE, null, QUERY_ORDER);
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
KeyInfo keyInfo = KeyInfo.fromCursor(cursor);
|
||||
keyInfos.add(keyInfo);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(keyInfos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(List<KeyInfo> keySubkeyStatus) {
|
||||
cachedResult = keySubkeyStatus;
|
||||
|
||||
if (isStarted()) {
|
||||
super.deliverResult(keySubkeyStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (cachedResult != null) {
|
||||
deliverResult(cachedResult);
|
||||
}
|
||||
|
||||
if (takeContentChanged() || cachedResult == null) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
super.onStopLoading();
|
||||
|
||||
cachedResult = null;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
public abstract static class KeyInfo {
|
||||
public abstract long getMasterKeyId();
|
||||
public abstract long getCreationDate();
|
||||
public abstract boolean getHasEncrypt();
|
||||
public abstract boolean getIsVerified();
|
||||
|
||||
@Nullable
|
||||
public abstract String getName();
|
||||
@Nullable
|
||||
public abstract String getEmail();
|
||||
@Nullable
|
||||
public abstract String getComment();
|
||||
|
||||
static KeyInfo fromCursor(Cursor cursor) {
|
||||
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
long creationDate = cursor.getLong(INDEX_CREATION) * 1000L;
|
||||
boolean hasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
|
||||
boolean isVerified = cursor.getInt(INDEX_VERIFIED) == 2;
|
||||
|
||||
String name = cursor.getString(INDEX_NAME);
|
||||
String email = cursor.getString(INDEX_EMAIL);
|
||||
String comment = cursor.getString(INDEX_COMMENT);
|
||||
|
||||
return new AutoValue_KeyLoader_KeyInfo(
|
||||
masterKeyId, creationDate, hasEncrypt, isVerified, name, email, comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui.dialog;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Drawable.ConstantState;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.content.res.ResourcesCompat;
|
||||
import android.support.v4.graphics.drawable.DrawableCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.Adapter;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mikepenz.materialdrawer.util.KeyboardUtil;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo;
|
||||
import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicatePresenter.RemoteDeduplicateView;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder;
|
||||
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener;
|
||||
|
||||
|
||||
public class RemoteDeduplicateActivity extends FragmentActivity {
|
||||
public static final String EXTRA_PACKAGE_NAME = "package_name";
|
||||
public static final String EXTRA_DUPLICATE_EMAILS = "duplicate_emails";
|
||||
|
||||
public static final int LOADER_ID_KEYS = 0;
|
||||
|
||||
|
||||
private RemoteDeduplicatePresenter presenter;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
presenter = new RemoteDeduplicatePresenter(getBaseContext(), LOADER_ID_KEYS);
|
||||
|
||||
KeyboardUtil.hideKeyboard(this);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
RemoteDeduplicateDialogFragment frag = new RemoteDeduplicateDialogFragment();
|
||||
frag.show(getSupportFragmentManager(), "requestKeyDialog");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
Intent intent = getIntent();
|
||||
List<String> dupAddresses = intent.getStringArrayListExtra(EXTRA_DUPLICATE_EMAILS);
|
||||
String duplicateAddress = dupAddresses.get(0);
|
||||
String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||
|
||||
presenter.setupFromIntentData(packageName, duplicateAddress);
|
||||
presenter.startLoaders(getSupportLoaderManager());
|
||||
}
|
||||
|
||||
public static class RemoteDeduplicateDialogFragment extends DialogFragment {
|
||||
private RemoteDeduplicatePresenter presenter;
|
||||
private RemoteDeduplicateView mvpView;
|
||||
|
||||
private Button buttonSelect;
|
||||
private Button buttonCancel;
|
||||
private RecyclerView keyChoiceList;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity);
|
||||
CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme);
|
||||
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(theme);
|
||||
@SuppressLint("InflateParams")
|
||||
View view = layoutInflater.inflate(R.layout.api_remote_deduplicate, null, false);
|
||||
alert.setView(view);
|
||||
|
||||
buttonSelect = (Button) view.findViewById(R.id.button_select);
|
||||
buttonCancel = (Button) view.findViewById(R.id.button_cancel);
|
||||
|
||||
keyChoiceList = (RecyclerView) view.findViewById(R.id.duplicate_key_list);
|
||||
keyChoiceList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
keyChoiceList.addItemDecoration(new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST));
|
||||
|
||||
setupListenersForPresenter();
|
||||
mvpView = createMvpView(view, layoutInflater);
|
||||
|
||||
return alert.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
presenter = ((RemoteDeduplicateActivity) getActivity()).presenter;
|
||||
presenter.setView(mvpView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
super.onCancel(dialog);
|
||||
|
||||
if (presenter != null) {
|
||||
presenter.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
|
||||
if (presenter != null) {
|
||||
presenter.setView(null);
|
||||
presenter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private RemoteDeduplicateView createMvpView(View view, LayoutInflater layoutInflater) {
|
||||
final ImageView iconClientApp = (ImageView) view.findViewById(R.id.icon_client_app);
|
||||
final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources());
|
||||
final TextView addressText = (TextView) view.findViewById(R.id.select_key_item_name);
|
||||
keyChoiceList.setAdapter(keyChoiceAdapter);
|
||||
|
||||
return new RemoteDeduplicateView() {
|
||||
@Override
|
||||
public void finish() {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activity.setResult(RESULT_OK, null);
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishAsCancelled() {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activity.setResult(RESULT_CANCELED);
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitleClientIcon(Drawable drawable) {
|
||||
iconClientApp.setImageDrawable(drawable);
|
||||
keyChoiceAdapter.setSelectionDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAddressText(String text) {
|
||||
addressText.setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeyListData(List<KeyInfo> data) {
|
||||
keyChoiceAdapter.setData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActiveItem(Integer position) {
|
||||
keyChoiceAdapter.setActiveItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnableSelectButton(boolean enabled) {
|
||||
buttonSelect.setEnabled(enabled);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupListenersForPresenter() {
|
||||
buttonSelect.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
presenter.onClickSelect();
|
||||
}
|
||||
});
|
||||
|
||||
buttonCancel.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
presenter.onClickCancel();
|
||||
}
|
||||
});
|
||||
|
||||
keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(),
|
||||
new RecyclerItemClickListener.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(View view, int position) {
|
||||
presenter.onKeyItemClick(position);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeyChoiceAdapter extends Adapter<KeyChoiceViewHolder> {
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final Resources resources;
|
||||
private List<KeyInfo> data;
|
||||
private Drawable iconUnselected;
|
||||
private Drawable iconSelected;
|
||||
private Integer activeItem;
|
||||
|
||||
KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) {
|
||||
this.layoutInflater = layoutInflater;
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View keyChoiceItemView = layoutInflater.inflate(R.layout.duplicate_key_item, parent, false);
|
||||
return new KeyChoiceViewHolder(keyChoiceItemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(KeyChoiceViewHolder holder, int position) {
|
||||
KeyInfo keyInfo = data.get(position);
|
||||
Drawable icon = (activeItem != null && position == activeItem) ? iconSelected : iconUnselected;
|
||||
holder.bind(keyInfo, icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data != null ? data.size() : 0;
|
||||
}
|
||||
|
||||
public void setData(List<KeyInfo> data) {
|
||||
this.data = data;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setSelectionDrawable(Drawable drawable) {
|
||||
ConstantState constantState = drawable.getConstantState();
|
||||
if (constantState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
iconSelected = constantState.newDrawable(resources);
|
||||
|
||||
iconUnselected = constantState.newDrawable(resources);
|
||||
DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_300, null));
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setActiveItem(Integer newActiveItem) {
|
||||
Integer prevActiveItem = this.activeItem;
|
||||
this.activeItem = newActiveItem;
|
||||
|
||||
if (prevActiveItem != null) {
|
||||
notifyItemChanged(prevActiveItem);
|
||||
}
|
||||
if (newActiveItem != null) {
|
||||
notifyItemChanged(newActiveItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView vName;
|
||||
private final TextView vCreation;
|
||||
private final ImageView vIcon;
|
||||
|
||||
KeyChoiceViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
vName = (TextView) itemView.findViewById(R.id.key_list_item_name);
|
||||
vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation);
|
||||
vIcon = (ImageView) itemView.findViewById(R.id.key_list_item_icon);
|
||||
}
|
||||
|
||||
void bind(KeyInfo keyInfo, Drawable selectionIcon) {
|
||||
vName.setText(keyInfo.getName());
|
||||
|
||||
Context context = vCreation.getContext();
|
||||
String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(),
|
||||
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME |
|
||||
DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH);
|
||||
vCreation.setText(context.getString(R.string.label_key_created, dateTime));
|
||||
|
||||
vIcon.setImageDrawable(selectionIcon);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui.dialog;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
|
||||
class RemoteDeduplicatePresenter implements LoaderCallbacks<List<KeyInfo>> {
|
||||
private final PackageManager packageManager;
|
||||
private final Context context;
|
||||
private final int loaderId;
|
||||
|
||||
|
||||
private AutocryptPeerDataAccessObject autocryptPeerDao;
|
||||
private String duplicateAddress;
|
||||
|
||||
private RemoteDeduplicateView view;
|
||||
private Integer selectedItem;
|
||||
private List<KeyInfo> keyInfoData;
|
||||
|
||||
|
||||
RemoteDeduplicatePresenter(Context context, int loaderId) {
|
||||
this.context = context;
|
||||
|
||||
packageManager = context.getPackageManager();
|
||||
|
||||
this.loaderId = loaderId;
|
||||
}
|
||||
|
||||
public void setView(RemoteDeduplicateView view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
void setupFromIntentData(String packageName, String duplicateAddress) {
|
||||
try {
|
||||
setPackageInfo(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(Constants.TAG, "Unable to find info of calling app!");
|
||||
view.finishAsCancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
autocryptPeerDao = new AutocryptPeerDataAccessObject(context, packageName);
|
||||
|
||||
this.duplicateAddress = duplicateAddress;
|
||||
view.setAddressText(duplicateAddress);
|
||||
}
|
||||
|
||||
private void setPackageInfo(String packageName) throws NameNotFoundException {
|
||||
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0);
|
||||
Drawable appIcon = packageManager.getApplicationIcon(applicationInfo);
|
||||
|
||||
view.setTitleClientIcon(appIcon);
|
||||
}
|
||||
|
||||
void startLoaders(LoaderManager loaderManager) {
|
||||
loaderManager.restartLoader(loaderId, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<List<KeyInfo>> onCreateLoader(int id, Bundle args) {
|
||||
return new KeyLoader(context, context.getContentResolver(), duplicateAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<KeyInfo>> loader, List<KeyInfo> data) {
|
||||
this.keyInfoData = data;
|
||||
view.setKeyListData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader loader) {
|
||||
view.setKeyListData(null);
|
||||
}
|
||||
|
||||
void onClickSelect() {
|
||||
if (keyInfoData == null) {
|
||||
Log.e(Constants.TAG, "got click on select with no data…?");
|
||||
return;
|
||||
}
|
||||
if (selectedItem == null) {
|
||||
Log.e(Constants.TAG, "got click on select with no selection…?");
|
||||
return;
|
||||
}
|
||||
|
||||
long masterKeyId = keyInfoData.get(selectedItem).getMasterKeyId();
|
||||
autocryptPeerDao.updateToSelectedState(duplicateAddress, masterKeyId);
|
||||
|
||||
view.finish();
|
||||
}
|
||||
|
||||
void onClickCancel() {
|
||||
view.finishAsCancelled();
|
||||
}
|
||||
|
||||
public void onCancel() {
|
||||
view.finishAsCancelled();
|
||||
}
|
||||
|
||||
void onKeyItemClick(int position) {
|
||||
if (selectedItem != null && position == selectedItem) {
|
||||
selectedItem = null;
|
||||
} else {
|
||||
selectedItem = position;
|
||||
}
|
||||
view.setActiveItem(selectedItem);
|
||||
view.setEnableSelectButton(selectedItem != null);
|
||||
}
|
||||
|
||||
interface RemoteDeduplicateView {
|
||||
void finish();
|
||||
void finishAsCancelled();
|
||||
|
||||
void setAddressText(String text);
|
||||
void setTitleClientIcon(Drawable drawable);
|
||||
|
||||
void setKeyListData(List<KeyInfo> data);
|
||||
void setActiveItem(Integer position);
|
||||
void setEnableSelectButton(boolean enabled);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.CertSectionedListAdapter.CertCursor;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter;
|
||||
@@ -39,7 +40,7 @@ import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CertSectionedListAdapter extends SectionCursorAdapter<CertSectionedListAdapter.CertCursor, String,
|
||||
public class CertSectionedListAdapter extends SectionCursorAdapter<CertCursor, String,
|
||||
CertSectionedListAdapter.CertItemViewHolder, CertSectionedListAdapter.CertSectionViewHolder> {
|
||||
|
||||
private CertListListener mListener;
|
||||
@@ -162,11 +163,11 @@ public class CertSectionedListAdapter extends SectionCursorAdapter<CertSectioned
|
||||
}
|
||||
}
|
||||
|
||||
public static class CertCursor extends CursorAdapter.AbstractCursor {
|
||||
public static class CertCursor extends CursorAdapter.SimpleCursor {
|
||||
public static final String[] CERTS_PROJECTION;
|
||||
static {
|
||||
ArrayList<String> projection = new ArrayList<>();
|
||||
projection.addAll(Arrays.asList(AbstractCursor.PROJECTION));
|
||||
projection.addAll(Arrays.asList(SimpleCursor.PROJECTION));
|
||||
projection.addAll(Arrays.asList(
|
||||
KeychainContract.Certs.MASTER_KEY_ID,
|
||||
KeychainContract.Certs.VERIFIED,
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.os.Build.VERSION_CODES;
|
||||
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.ImageView;
|
||||
import android.widget.TextView;
|
||||
@@ -38,6 +39,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.ViewHolder;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.LinkedIdInfo;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.TrustIdInfo;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.UserIdInfo;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
|
||||
@@ -52,15 +54,17 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
private final Context context;
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final boolean isSecret;
|
||||
private final IdentityClickListener identityClickListener;
|
||||
|
||||
private List<IdentityInfo> data;
|
||||
|
||||
|
||||
public IdentityAdapter(Context context, boolean isSecret) {
|
||||
public IdentityAdapter(Context context, boolean isSecret, IdentityClickListener identityClickListener) {
|
||||
super();
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.context = context;
|
||||
this.isSecret = isSecret;
|
||||
this.identityClickListener = identityClickListener;
|
||||
}
|
||||
|
||||
public void setData(List<IdentityInfo> data) {
|
||||
@@ -75,7 +79,11 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
|
||||
int viewType = getItemViewType(position);
|
||||
if (viewType == VIEW_TYPE_USER_ID) {
|
||||
((UserIdViewHolder) holder).bind((UserIdInfo) info);
|
||||
if (info instanceof TrustIdInfo) {
|
||||
((UserIdViewHolder) holder).bind((TrustIdInfo) info);
|
||||
} else {
|
||||
((UserIdViewHolder) holder).bind((UserIdInfo) info);
|
||||
}
|
||||
} else if (viewType == VIEW_TYPE_LINKED_ID) {
|
||||
((LinkedIdViewHolder) holder).bind(context, (LinkedIdInfo) info, isSecret);
|
||||
} else {
|
||||
@@ -86,7 +94,8 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_USER_ID) {
|
||||
return new UserIdViewHolder(layoutInflater.inflate(R.layout.view_key_identity_user_id, parent, false));
|
||||
return new UserIdViewHolder(
|
||||
layoutInflater.inflate(R.layout.view_key_identity_user_id, parent, false), identityClickListener);
|
||||
} else if (viewType == VIEW_TYPE_LINKED_ID) {
|
||||
return new LinkedIdViewHolder(layoutInflater.inflate(R.layout.linked_id_item, parent, false));
|
||||
} else {
|
||||
@@ -97,7 +106,7 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
IdentityInfo info = data.get(position);
|
||||
if (info instanceof UserIdInfo) {
|
||||
if (info instanceof UserIdInfo || info instanceof TrustIdInfo) {
|
||||
return VIEW_TYPE_USER_ID;
|
||||
} else if (info instanceof LinkedIdInfo) {
|
||||
return VIEW_TYPE_LINKED_ID;
|
||||
@@ -189,16 +198,59 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
private final TextView vName;
|
||||
private final TextView vAddress;
|
||||
private final TextView vComment;
|
||||
private final ImageView vIcon;
|
||||
private final ImageView vMore;
|
||||
|
||||
private UserIdViewHolder(View view) {
|
||||
private UserIdViewHolder(View view, final IdentityClickListener identityClickListener) {
|
||||
super(view);
|
||||
|
||||
vName = (TextView) view.findViewById(R.id.user_id_item_name);
|
||||
vAddress = (TextView) view.findViewById(R.id.user_id_item_address);
|
||||
vComment = (TextView) view.findViewById(R.id.user_id_item_comment);
|
||||
|
||||
vIcon = (ImageView) view.findViewById(R.id.trust_id_app_icon);
|
||||
vMore = (ImageView) view.findViewById(R.id.user_id_item_more);
|
||||
|
||||
view.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
identityClickListener.onClickIdentity(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
vMore.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
identityClickListener.onClickIdentityMore(getAdapterPosition(), v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void bind(TrustIdInfo info) {
|
||||
if (info.getUserIdInfo() != null) {
|
||||
bindUserIdInfo(info.getUserIdInfo());
|
||||
} else {
|
||||
vName.setVisibility(View.GONE);
|
||||
vComment.setVisibility(View.GONE);
|
||||
|
||||
vAddress.setText(info.getTrustId());
|
||||
vAddress.setTypeface(null, Typeface.NORMAL);
|
||||
}
|
||||
|
||||
vIcon.setImageDrawable(info.getAppIcon());
|
||||
vMore.setVisibility(View.VISIBLE);
|
||||
|
||||
itemView.setClickable(info.getTrustIdIntent() != null);
|
||||
}
|
||||
|
||||
public void bind(UserIdInfo info) {
|
||||
bindUserIdInfo(info);
|
||||
|
||||
vIcon.setVisibility(View.GONE);
|
||||
vMore.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void bindUserIdInfo(UserIdInfo info) {
|
||||
if (info.getName() != null) {
|
||||
vName.setText(info.getName());
|
||||
} else {
|
||||
@@ -224,8 +276,12 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
vName.setTypeface(null, Typeface.NORMAL);
|
||||
vAddress.setTypeface(null, Typeface.NORMAL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface IdentityClickListener {
|
||||
void onClickIdentity(int position);
|
||||
void onClickIdentityMore(int position, View anchor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,13 @@
|
||||
package org.sufficientlysecure.keychain.ui.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
@@ -38,6 +41,7 @@ import com.futuremind.recyclerviewfastscroll.SectionTitleProvider;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.Highlighter;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
@@ -47,6 +51,8 @@ import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedListAdapter.KeyListCursor, Character,
|
||||
@@ -348,12 +354,13 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
|
||||
public class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder
|
||||
implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
private TextView mMainUserId;
|
||||
private TextView mMainUserIdRest;
|
||||
private TextView mCreationDate;
|
||||
private ImageView mStatus;
|
||||
private View mSlinger;
|
||||
private ImageButton mSlingerButton;
|
||||
private final ImageView mTrustIdIcon;
|
||||
private final TextView mMainUserId;
|
||||
private final TextView mMainUserIdRest;
|
||||
private final TextView mCreationDate;
|
||||
private final ImageView mStatus;
|
||||
private final View mSlinger;
|
||||
private final ImageButton mSlingerButton;
|
||||
|
||||
KeyItemViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
@@ -364,6 +371,7 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
|
||||
mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view);
|
||||
mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button);
|
||||
mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation);
|
||||
mTrustIdIcon = (ImageView) itemView.findViewById(R.id.key_list_item_tid_icon);
|
||||
|
||||
itemView.setClickable(true);
|
||||
itemView.setLongClickable(true);
|
||||
@@ -485,7 +493,23 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
|
||||
} else {
|
||||
mCreationDate.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
{ // set icons
|
||||
List<String> packageNames = keyItem.getTrustIdPackages();
|
||||
|
||||
if (!keyItem.isSecret() && !packageNames.isEmpty()) {
|
||||
String packageName = packageNames.get(0);
|
||||
Drawable drawable = getDrawableForPackageName(packageName);
|
||||
if (drawable != null) {
|
||||
mTrustIdIcon.setImageDrawable(drawable);
|
||||
mTrustIdIcon.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTrustIdIcon.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
mTrustIdIcon.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +586,8 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
|
||||
KeychainContract.KeyRings.VERIFIED,
|
||||
KeychainContract.KeyRings.HAS_ANY_SECRET,
|
||||
KeychainContract.KeyRings.FINGERPRINT,
|
||||
KeychainContract.KeyRings.HAS_ENCRYPT
|
||||
KeychainContract.KeyRings.HAS_ENCRYPT,
|
||||
KeychainContract.KeyRings.API_KNOWN_TO_PACKAGE_NAMES
|
||||
));
|
||||
|
||||
PROJECTION = arr.toArray(new String[arr.size()]);
|
||||
@@ -603,6 +628,15 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
|
||||
int index = getColumnIndexOrThrow(KeychainContract.KeyRings.VERIFIED);
|
||||
return getInt(index) > 0;
|
||||
}
|
||||
|
||||
public List<String> getTrustIdPackages() {
|
||||
int index = getColumnIndexOrThrow(KeyRings.API_KNOWN_TO_PACKAGE_NAMES);
|
||||
String packageNames = getString(index);
|
||||
if (packageNames == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
return Arrays.asList(packageNames.split(","));
|
||||
}
|
||||
}
|
||||
|
||||
public interface KeyListListener {
|
||||
@@ -614,4 +648,24 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
|
||||
|
||||
void onSelectionStateChanged(int selectedCount);
|
||||
}
|
||||
|
||||
private HashMap<String, Drawable> appIconCache = new HashMap<>();
|
||||
|
||||
private Drawable getDrawableForPackageName(String packageName) {
|
||||
if (appIconCache.containsKey(packageName)) {
|
||||
return appIconCache.get(packageName);
|
||||
}
|
||||
|
||||
PackageManager pm = getContext().getPackageManager();
|
||||
try {
|
||||
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
|
||||
|
||||
Drawable appIcon = pm.getApplicationIcon(ai);
|
||||
appIconCache.put(packageName, appIcon);
|
||||
|
||||
return appIcon;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@ import android.os.Handler;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.support.v7.widget.PopupMenu.OnDismissListener;
|
||||
import android.support.v7.widget.PopupMenu.OnMenuItemClickListener;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -44,7 +48,7 @@ import org.sufficientlysecure.keychain.ui.keyview.view.KeyserverStatusView;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.view.SystemContactCardView;
|
||||
|
||||
|
||||
public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView {
|
||||
public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, OnMenuItemClickListener {
|
||||
public static final String ARG_MASTER_KEY_ID = "master_key_id";
|
||||
public static final String ARG_IS_SECRET = "is_secret";
|
||||
|
||||
@@ -67,6 +71,8 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView {
|
||||
KeyHealthPresenter mKeyHealthPresenter;
|
||||
KeyserverStatusPresenter mKeyserverStatusPresenter;
|
||||
|
||||
private Integer displayedContextMenuPosition;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
@@ -161,4 +167,37 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showContextMenu(int position, View anchor) {
|
||||
displayedContextMenuPosition = position;
|
||||
|
||||
PopupMenu menu = new PopupMenu(getContext(), anchor);
|
||||
menu.inflate(R.menu.identity_context_menu);
|
||||
menu.setOnMenuItemClickListener(this);
|
||||
menu.setOnDismissListener(new OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(PopupMenu popupMenu) {
|
||||
displayedContextMenuPosition = null;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (displayedContextMenuPosition == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.autocrypt_forget:
|
||||
int position = displayedContextMenuPosition;
|
||||
displayedContextMenuPosition = null;
|
||||
mIdentitiesPresenter.onClickForgetIdentity(position);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +25,26 @@ import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
|
||||
import org.sufficientlysecure.keychain.linked.UriAttribute;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo;
|
||||
import org.sufficientlysecure.keychain.ui.util.PackageIconGetter;
|
||||
|
||||
|
||||
public class IdentityLoader extends AsyncTaskLoader<List<IdentityInfo>> {
|
||||
@@ -68,6 +76,7 @@ public class IdentityLoader extends AsyncTaskLoader<List<IdentityInfo>> {
|
||||
private static final String USER_IDS_WHERE = UserPackets.IS_REVOKED + " = 0";
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
private final PackageIconGetter packageIconGetter;
|
||||
private final long masterKeyId;
|
||||
private final boolean showLinkedIds;
|
||||
|
||||
@@ -82,6 +91,9 @@ public class IdentityLoader extends AsyncTaskLoader<List<IdentityInfo>> {
|
||||
this.masterKeyId = masterKeyId;
|
||||
this.showLinkedIds = showLinkedIds;
|
||||
|
||||
this.identityObserver = new ForceLoadContentObserver();
|
||||
this.packageIconGetter = PackageIconGetter.getInstance(context);
|
||||
|
||||
this.identityObserver = new ForceLoadContentObserver();
|
||||
}
|
||||
|
||||
@@ -93,10 +105,76 @@ public class IdentityLoader extends AsyncTaskLoader<List<IdentityInfo>> {
|
||||
loadLinkedIds(identities);
|
||||
}
|
||||
loadUserIds(identities);
|
||||
correlateOrAddTrustIds(identities);
|
||||
|
||||
return Collections.unmodifiableList(identities);
|
||||
}
|
||||
|
||||
private static final String[] TRUST_IDS_PROJECTION = new String[] {
|
||||
ApiAutocryptPeer._ID,
|
||||
ApiAutocryptPeer.PACKAGE_NAME,
|
||||
ApiAutocryptPeer.IDENTIFIER,
|
||||
};
|
||||
private static final int INDEX_PACKAGE_NAME = 1;
|
||||
private static final int INDEX_TRUST_ID = 2;
|
||||
|
||||
private void correlateOrAddTrustIds(ArrayList<IdentityInfo> identities) {
|
||||
Cursor cursor = contentResolver.query(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),
|
||||
TRUST_IDS_PROJECTION, null, null, null);
|
||||
if (cursor == null) {
|
||||
Log.e(Constants.TAG, "Error loading trust ids!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
String packageName = cursor.getString(INDEX_PACKAGE_NAME);
|
||||
String autocryptPeer = cursor.getString(INDEX_TRUST_ID);
|
||||
|
||||
Drawable drawable = packageIconGetter.getDrawableForPackageName(packageName);
|
||||
Intent autocryptPeerIntent = getTrustIdActivityIntentIfResolvable(packageName, autocryptPeer);
|
||||
|
||||
UserIdInfo associatedUserIdInfo = findUserIdMatchingTrustId(identities, autocryptPeer);
|
||||
if (associatedUserIdInfo != null) {
|
||||
int position = identities.indexOf(associatedUserIdInfo);
|
||||
TrustIdInfo autocryptPeerInfo = TrustIdInfo.create(associatedUserIdInfo, autocryptPeer, packageName, drawable, autocryptPeerIntent);
|
||||
identities.set(position, autocryptPeerInfo);
|
||||
} else {
|
||||
TrustIdInfo autocryptPeerInfo = TrustIdInfo.create(autocryptPeer, packageName, drawable, autocryptPeerIntent);
|
||||
identities.add(autocryptPeerInfo);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getTrustIdActivityIntentIfResolvable(String packageName, String autocryptPeer) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(packageName + ".AUTOCRYPT_PEER_ACTION");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID, autocryptPeer);
|
||||
|
||||
List<ResolveInfo> resolveInfos = getContext().getPackageManager().queryIntentActivities(intent, 0);
|
||||
if (resolveInfos != null && !resolveInfos.isEmpty()) {
|
||||
return intent;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static UserIdInfo findUserIdMatchingTrustId(List<IdentityInfo> identities, String autocryptPeer) {
|
||||
for (IdentityInfo identityInfo : identities) {
|
||||
if (identityInfo instanceof UserIdInfo) {
|
||||
UserIdInfo userIdInfo = (UserIdInfo) identityInfo;
|
||||
if (autocryptPeer.equals(userIdInfo.getEmail())) {
|
||||
return userIdInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadLinkedIds(ArrayList<IdentityInfo> identities) {
|
||||
Cursor cursor = contentResolver.query(UserPackets.buildLinkedIdsUri(masterKeyId),
|
||||
USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null);
|
||||
@@ -222,4 +300,32 @@ public class IdentityLoader extends AsyncTaskLoader<List<IdentityInfo>> {
|
||||
return new AutoValue_IdentityLoader_LinkedIdInfo(rank, verified, isPrimary, uriAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
public abstract static class TrustIdInfo implements IdentityInfo {
|
||||
public abstract int getRank();
|
||||
public abstract int getVerified();
|
||||
public abstract boolean isPrimary();
|
||||
|
||||
public abstract String getTrustId();
|
||||
public abstract String getPackageName();
|
||||
@Nullable
|
||||
public abstract Drawable getAppIcon();
|
||||
@Nullable
|
||||
public abstract UserIdInfo getUserIdInfo();
|
||||
@Nullable
|
||||
public abstract Intent getTrustIdIntent();
|
||||
|
||||
static TrustIdInfo create(UserIdInfo userIdInfo, String autocryptPeer, String packageName,
|
||||
Drawable appIcon, Intent autocryptPeerIntent) {
|
||||
return new AutoValue_IdentityLoader_TrustIdInfo(userIdInfo.getRank(), userIdInfo.getVerified(),
|
||||
userIdInfo.isPrimary(), autocryptPeer, packageName, appIcon, userIdInfo, autocryptPeerIntent);
|
||||
}
|
||||
|
||||
static TrustIdInfo create(String autocryptPeer, String packageName, Drawable appIcon, Intent autocryptPeerIntent) {
|
||||
return new AutoValue_IdentityLoader_TrustIdInfo(
|
||||
0, Certs.VERIFIED_SELF, false, autocryptPeer, packageName, appIcon, null, autocryptPeerIntent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,18 +28,22 @@ import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.View;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.ui.EditIdentitiesActivity;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.IdentityClickListener;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.LinkedIdViewFragment;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.LinkedIdInfo;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.TrustIdInfo;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.UserIdInfo;
|
||||
import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -67,17 +71,23 @@ public class IdentitiesPresenter implements LoaderCallbacks<List<IdentityInfo>>
|
||||
this.masterKeyId = masterKeyId;
|
||||
this.isSecret = isSecret;
|
||||
|
||||
identitiesAdapter = new IdentityAdapter(context, isSecret);
|
||||
identitiesAdapter = new IdentityAdapter(context, isSecret, new IdentityClickListener() {
|
||||
@Override
|
||||
public void onClickIdentity(int position) {
|
||||
showIdentityInfo(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClickIdentityMore(int position, View anchor) {
|
||||
showIdentityContextMenu(position, anchor);
|
||||
|
||||
}
|
||||
});
|
||||
view.setIdentitiesAdapter(identitiesAdapter);
|
||||
|
||||
view.setEditIdentitiesButtonVisible(isSecret);
|
||||
|
||||
view.setIdentitiesCardListener(new IdentitiesCardListener() {
|
||||
@Override
|
||||
public void onIdentityItemClick(int position) {
|
||||
showIdentityInfo(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClickEditIdentities() {
|
||||
editIdentities();
|
||||
@@ -117,9 +127,18 @@ public class IdentitiesPresenter implements LoaderCallbacks<List<IdentityInfo>>
|
||||
showLinkedId((LinkedIdInfo) info);
|
||||
} else if (info instanceof UserIdInfo) {
|
||||
showUserIdInfo((UserIdInfo) info);
|
||||
} else if (info instanceof TrustIdInfo) {
|
||||
Intent autocryptPeerIntent = ((TrustIdInfo) info).getTrustIdIntent();
|
||||
if (autocryptPeerIntent != null) {
|
||||
viewKeyMvpView.startActivity(autocryptPeerIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showIdentityContextMenu(int position, View anchor) {
|
||||
viewKeyMvpView.showContextMenu(position, anchor);
|
||||
}
|
||||
|
||||
private void showLinkedId(final LinkedIdInfo info) {
|
||||
final LinkedIdViewFragment frag;
|
||||
try {
|
||||
@@ -154,6 +173,18 @@ public class IdentitiesPresenter implements LoaderCallbacks<List<IdentityInfo>>
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public void onClickForgetIdentity(int position) {
|
||||
TrustIdInfo info = (TrustIdInfo) identitiesAdapter.getInfo(position);
|
||||
if (info == null) {
|
||||
Log.e(Constants.TAG, "got a 'forget' click on a bad trust id");
|
||||
return;
|
||||
}
|
||||
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao =
|
||||
new AutocryptPeerDataAccessObject(context, info.getPackageName());
|
||||
autocryptPeerDao.delete(info.getTrustId());
|
||||
}
|
||||
|
||||
public interface IdentitiesMvpView {
|
||||
void setIdentitiesAdapter(IdentityAdapter userIdsAdapter);
|
||||
void setIdentitiesCardListener(IdentitiesCardListener identitiesCardListener);
|
||||
@@ -161,8 +192,6 @@ public class IdentitiesPresenter implements LoaderCallbacks<List<IdentityInfo>>
|
||||
}
|
||||
|
||||
public interface IdentitiesCardListener {
|
||||
void onIdentityItemClick(int position);
|
||||
|
||||
void onClickEditIdentities();
|
||||
void onClickAddIdentity();
|
||||
}
|
||||
|
||||
@@ -4,12 +4,16 @@ package org.sufficientlysecure.keychain.ui.keyview.presenter;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.View;
|
||||
|
||||
|
||||
public interface ViewKeyMvpView {
|
||||
void switchToFragment(Fragment frag, String backStackName);
|
||||
|
||||
void startActivity(Intent intent);
|
||||
void startActivityAndShowResultSnackbar(Intent intent);
|
||||
void showDialogFragment(DialogFragment dialogFragment, final String tag);
|
||||
void setContentShown(boolean show, boolean animate);
|
||||
|
||||
void showContextMenu(int position, View anchor);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter.IdentitiesCardListener;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter.IdentitiesMvpView;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener;
|
||||
|
||||
|
||||
public class IdentitiesCardView extends CardView implements IdentitiesMvpView {
|
||||
@@ -59,16 +58,6 @@ public class IdentitiesCardView extends CardView implements IdentitiesMvpView {
|
||||
}
|
||||
});
|
||||
|
||||
vIdentities.addOnItemTouchListener(new RecyclerItemClickListener(context,
|
||||
new RecyclerItemClickListener.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(View view, int position) {
|
||||
if (identitiesCardListener != null) {
|
||||
identitiesCardListener.onIdentityItemClick(position);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Button linkedIdsAddButton = (Button) view.findViewById(R.id.view_key_card_linked_ids_add);
|
||||
|
||||
linkedIdsAddButton.setOnClickListener(new View.OnClickListener() {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.sufficientlysecure.keychain.ui.util;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
|
||||
public class PackageIconGetter {
|
||||
private final PackageManager packageManager;
|
||||
private final HashMap<String, Drawable> appIconCache = new HashMap<>();
|
||||
|
||||
|
||||
public static PackageIconGetter getInstance(Context context) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
return new PackageIconGetter(pm);
|
||||
}
|
||||
|
||||
private PackageIconGetter(PackageManager packageManager) {
|
||||
this.packageManager = packageManager;
|
||||
}
|
||||
|
||||
public Drawable getDrawableForPackageName(String packageName) {
|
||||
if (appIconCache.containsKey(packageName)) {
|
||||
return appIconCache.get(packageName);
|
||||
}
|
||||
|
||||
try {
|
||||
ApplicationInfo ai = packageManager.getApplicationInfo(packageName, 0);
|
||||
|
||||
Drawable appIcon = packageManager.getApplicationIcon(ai);
|
||||
appIconCache.put(packageName, appIcon);
|
||||
|
||||
return appIcon;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
@@ -35,7 +36,7 @@ import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH extends RecyclerView.ViewHolder>
|
||||
public abstract class CursorAdapter<C extends SimpleCursor, VH extends RecyclerView.ViewHolder>
|
||||
extends RecyclerView.Adapter<VH> {
|
||||
public static final String TAG = "CursorAdapter";
|
||||
|
||||
@@ -58,7 +59,7 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
|
||||
|
||||
/**
|
||||
* Constructor that allows control over auto-requery. It is recommended
|
||||
* you not use this, but instead {@link #CursorAdapter(Context, AbstractCursor, int)}.
|
||||
* you not use this, but instead {@link #CursorAdapter(Context, SimpleCursor, int)}.
|
||||
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
|
||||
* will always be set.
|
||||
*
|
||||
@@ -223,7 +224,7 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
|
||||
|
||||
/**
|
||||
* Swap in a new Cursor, returning the old Cursor. Unlike
|
||||
* {@link #changeCursor(AbstractCursor)}, the returned old Cursor is <em>not</em>
|
||||
* {@link #changeCursor(SimpleCursor)}, the returned old Cursor is <em>not</em>
|
||||
* closed.
|
||||
*
|
||||
* @param newCursor The new cursor to be used.
|
||||
@@ -312,10 +313,10 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class AbstractCursor extends CursorWrapper {
|
||||
public static class SimpleCursor extends CursorWrapper {
|
||||
public static final String[] PROJECTION = {"_id"};
|
||||
|
||||
public static <T extends AbstractCursor> T wrap(Cursor cursor, Class<T> type) {
|
||||
public static <T extends SimpleCursor> T wrap(Cursor cursor, Class<T> type) {
|
||||
if (cursor != null) {
|
||||
try {
|
||||
Constructor<T> constructor = type.getConstructor(Cursor.class);
|
||||
@@ -335,7 +336,7 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
|
||||
*
|
||||
* @param cursor The underlying cursor to wrap.
|
||||
*/
|
||||
protected AbstractCursor(Cursor cursor) {
|
||||
public SimpleCursor(Cursor cursor) {
|
||||
super(cursor);
|
||||
mColumnIndices = new HashMap<>(cursor.getColumnCount() * 4 / 3, 0.75f);
|
||||
}
|
||||
@@ -376,12 +377,12 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeyCursor extends AbstractCursor {
|
||||
public static class KeyCursor extends SimpleCursor {
|
||||
public static final String[] PROJECTION;
|
||||
|
||||
static {
|
||||
ArrayList<String> arr = new ArrayList<>();
|
||||
arr.addAll(Arrays.asList(AbstractCursor.PROJECTION));
|
||||
arr.addAll(Arrays.asList(SimpleCursor.PROJECTION));
|
||||
arr.addAll(Arrays.asList(
|
||||
KeychainContract.KeyRings.MASTER_KEY_ID,
|
||||
KeychainContract.KeyRings.USER_ID,
|
||||
@@ -392,7 +393,8 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
|
||||
KeychainContract.KeyRings.CREATION,
|
||||
KeychainContract.KeyRings.NAME,
|
||||
KeychainContract.KeyRings.EMAIL,
|
||||
KeychainContract.KeyRings.COMMENT
|
||||
KeychainContract.KeyRings.COMMENT,
|
||||
KeychainContract.KeyRings.API_KNOWN_TO_PACKAGE_NAMES
|
||||
));
|
||||
|
||||
PROJECTION = arr.toArray(new String[arr.size()]);
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.util.SparseArrayCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
@@ -27,18 +26,16 @@ import android.view.ViewGroup;
|
||||
|
||||
import com.tonicartos.superslim.LayoutManager;
|
||||
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @param <T> section type.
|
||||
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
|
||||
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
|
||||
*/
|
||||
public abstract class SectionCursorAdapter<C extends CursorAdapter.AbstractCursor, T, VH extends SectionCursorAdapter.ViewHolder,
|
||||
public abstract class SectionCursorAdapter<C extends SimpleCursor, T, VH extends SectionCursorAdapter.ViewHolder,
|
||||
SH extends SectionCursorAdapter.ViewHolder> extends CursorAdapter<C, RecyclerView.ViewHolder> {
|
||||
|
||||
public static final String TAG = "SectionCursorAdapter";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
|
||||
</vector>
|
||||
138
OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml
Normal file
138
OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml
Normal file
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:layout_marginTop="24dp"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:elevation="4dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="center_horizontal"
|
||||
tools:targetApi="lollipop">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:src="@drawable/link_24dp"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/icon_client_app"
|
||||
tools:src="@drawable/apps_k9"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
tools:ignore="UselessParent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="16dp"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="Multiple candidate keys"
|
||||
android:id="@+id/dialog_title"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/select_key_item_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="valodim@mugenguild.com"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/select_identity_key_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="There are multiple keys that could be used to encrypt to this address. Please choose from the list:"
|
||||
/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/duplicate_key_list"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:padding="8dp"
|
||||
style="?buttonBarStyle">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_cancel"
|
||||
android:id="@+id/button_cancel"
|
||||
style="?buttonBarButtonStyle"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Select"
|
||||
android:id="@+id/button_select"
|
||||
android:enabled="false"
|
||||
style="?buttonBarButtonStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
45
OpenKeychain/src/main/res/layout/duplicate_key_item.xml
Normal file
45
OpenKeychain/src/main/res/layout/duplicate_key_item.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:layout_marginTop="30dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:id="@+id/key_list_item_icon"
|
||||
tools:tint="@color/md_grey_600"
|
||||
tools:src="@drawable/apps_k9"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/key_list_item_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/label_main_user_id"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/key_list_item_creation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:visibility="visible"
|
||||
tools:text="Created on 10/10/2010 10:00" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -2,7 +2,6 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/listPreferredItemHeight"
|
||||
@@ -11,7 +10,15 @@
|
||||
android:orientation="horizontal"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:background="@drawable/list_item_ripple"
|
||||
android:focusable="false">
|
||||
android:focusable="false"
|
||||
tools:layout_marginTop="30dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="4dp"
|
||||
android:id="@+id/key_list_item_tid_icon"
|
||||
tools:src="@drawable/apps_k9" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/key_list_item_data"
|
||||
@@ -24,8 +31,7 @@
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="4dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
tools:visibility="gone">
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/key_list_item_name"
|
||||
@@ -60,7 +66,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
tools:visibility="gone">
|
||||
|
||||
<View
|
||||
android:layout_width="1dip"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:orientation="horizontal"
|
||||
android:singleLine="true"
|
||||
android:background="?selectableItemBackground"
|
||||
tools:showIn="@layout/linked_id_view_fragment">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="LinearLayout"
|
||||
tools:layout_width="match_parent"
|
||||
tools:layout_height="match_parent">
|
||||
tools:layout_height="match_parent"
|
||||
tools:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/subkey_status_card_content" />
|
||||
|
||||
</LinearLayout>
|
||||
8
OpenKeychain/src/main/res/layout/trust_id_icon.xml
Normal file
8
OpenKeychain/src/main/res/layout/trust_id_icon.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="4dp"
|
||||
tools:src="@drawable/apps_k9" />
|
||||
@@ -22,7 +22,8 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<TextView
|
||||
style="@style/CardViewHeader"
|
||||
|
||||
@@ -3,31 +3,61 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
android:orientation="horizontal"
|
||||
android:maxLines="1"
|
||||
android:padding="8dp"
|
||||
android:background="?selectableItemBackground"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_id_item_address"
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/trust_id_app_icon"
|
||||
tools:src="@drawable/apps_k9"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_id_item_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="alice@example.com"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_id_item_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Alice"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_id_item_comment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="comment"
|
||||
tools:visibility="gone"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="alice@example.com"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/user_id_item_more"
|
||||
android:background="?selectableItemBackground"
|
||||
android:src="@drawable/ic_more_vert_black_24dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_id_item_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Alice"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_id_item_comment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="comment"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
</LinearLayout>
|
||||
|
||||
70
OpenKeychain/src/main/res/layout/view_key_trust_id_item.xml
Normal file
70
OpenKeychain/src/main/res/layout/view_key_trust_id_item.xml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="?selectableItemBackground"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:orientation="horizontal"
|
||||
android:maxLines="1"
|
||||
android:padding="8dp"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/trust_id_app_icon"
|
||||
tools:src="@drawable/apps_k9"/>
|
||||
|
||||
<TextView
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/trust_id_name"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:text="alice@example.com"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/user_id_item_more"
|
||||
android:background="?selectableItemBackground"
|
||||
android:src="@drawable/ic_chat_black_24dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/trust_id_button_bar"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
style="?android:buttonBarStyle">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:id="@+id/view_key_card_user_ids_edit"
|
||||
android:text="Forget"
|
||||
android:textColor="@color/card_view_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
10
OpenKeychain/src/main/res/menu/identity_context_menu.xml
Normal file
10
OpenKeychain/src/main/res/menu/identity_context_menu.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/autocrypt_forget"
|
||||
android:title="@string/identity_context_forget"
|
||||
android:icon="@drawable/ic_delete_grey_24dp"
|
||||
/>
|
||||
|
||||
</menu>
|
||||
@@ -1886,4 +1886,6 @@
|
||||
<string name="dialog_insecure_button_view_key">View Key</string>
|
||||
<string name="dialog_insecure_button_ok">Got it</string>
|
||||
|
||||
<string name="identity_context_forget">Forget</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
import java.security.AccessControlException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.pm.PackageInfo;
|
||||
@@ -14,13 +15,17 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.res.builder.RobolectricPackageManager;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
import org.sufficientlysecure.keychain.KeychainTestRunner;
|
||||
import org.sufficientlysecure.keychain.operations.CertifyOperation;
|
||||
import org.sufficientlysecure.keychain.operations.results.CertifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepositorySaveTest;
|
||||
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
|
||||
@@ -43,6 +48,7 @@ public class KeychainExternalProviderTest {
|
||||
static final String USER_ID_SEC_1 = "twi <twi-sec@openkeychain.org>";
|
||||
static final long KEY_ID_SECRET = 0x5D4DA4423C39122FL;
|
||||
static final long KEY_ID_PUBLIC = 0x9A282CE2AB44A382L;
|
||||
public static final String AUTOCRYPT_PEER = "tid";
|
||||
|
||||
|
||||
KeyWritableRepository databaseInteractor =
|
||||
@@ -50,10 +56,13 @@ public class KeychainExternalProviderTest {
|
||||
ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver();
|
||||
ApiPermissionHelper apiPermissionHelper;
|
||||
ApiDataAccessObject apiDao;
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ShadowLog.stream = System.out;
|
||||
|
||||
RobolectricPackageManager rpm = (RobolectricPackageManager) RuntimeEnvironment.getPackageManager();
|
||||
rpm.setPackagesForUid(0, PACKAGE_NAME);
|
||||
PackageInfo packageInfo = new PackageInfo();
|
||||
@@ -63,6 +72,7 @@ public class KeychainExternalProviderTest {
|
||||
|
||||
apiDao = new ApiDataAccessObject(RuntimeEnvironment.application);
|
||||
apiPermissionHelper = new ApiPermissionHelper(RuntimeEnvironment.application, apiDao);
|
||||
autocryptPeerDao = new AutocryptPeerDataAccessObject(RuntimeEnvironment.application, PACKAGE_NAME);
|
||||
|
||||
apiDao.insertApiApp(new AppSettings(PACKAGE_NAME, PACKAGE_SIGNATURE));
|
||||
}
|
||||
@@ -78,15 +88,6 @@ public class KeychainExternalProviderTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = AccessControlException.class)
|
||||
public void testPermission__withExplicitPackage() throws Exception {
|
||||
contentResolver.query(
|
||||
EmailStatus.CONTENT_URI.buildUpon().appendPath("fake_pkg").build(),
|
||||
new String[] { EmailStatus.EMAIL_ADDRESS, EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID },
|
||||
null, new String [] { }, null
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = AccessControlException.class)
|
||||
public void testPermission__withWrongPackageCert() throws Exception {
|
||||
apiDao.deleteApiApp(PACKAGE_NAME);
|
||||
@@ -100,7 +101,7 @@ public class KeychainExternalProviderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuery__withNonExistentAddress() throws Exception {
|
||||
public void testEmailStatus_withNonExistentAddress() throws Exception {
|
||||
Cursor cursor = contentResolver.query(
|
||||
EmailStatus.CONTENT_URI, new String[] {
|
||||
EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID_STATUS, EmailStatus.USER_ID },
|
||||
@@ -110,12 +111,12 @@ public class KeychainExternalProviderTest {
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
|
||||
assertEquals(0, cursor.getInt(1));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertTrue(cursor.isNull(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuery() throws Exception {
|
||||
public void testEmailStatus() throws Exception {
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
@@ -127,13 +128,13 @@ public class KeychainExternalProviderTest {
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
|
||||
assertEquals(1, cursor.getInt(1));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNVERIFIED, cursor.getInt(1));
|
||||
assertEquals("twi <twi@openkeychain.org>", cursor.getString(2));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuery__multiple() throws Exception {
|
||||
public void testEmailStatus_multiple() throws Exception {
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
@@ -145,17 +146,17 @@ public class KeychainExternalProviderTest {
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToNext());
|
||||
assertEquals(MAIL_ADDRESS_2, cursor.getString(0));
|
||||
assertEquals(0, cursor.getInt(1));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertTrue(cursor.isNull(2));
|
||||
assertTrue(cursor.moveToNext());
|
||||
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
|
||||
assertEquals(1, cursor.getInt(1));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNVERIFIED, cursor.getInt(1));
|
||||
assertEquals("twi <twi@openkeychain.org>", cursor.getString(2));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuery__withSecretKey() throws Exception {
|
||||
public void testEmailStatus_withSecretKey() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
@@ -173,7 +174,7 @@ public class KeychainExternalProviderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuery__withConfirmedKey() throws Exception {
|
||||
public void testEmailStatus_withConfirmedKey() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
@@ -181,18 +182,197 @@ public class KeychainExternalProviderTest {
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
EmailStatus.CONTENT_URI, new String[] {
|
||||
EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID_STATUS, EmailStatus.USER_ID },
|
||||
EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID, EmailStatus.USER_ID_STATUS},
|
||||
null, new String [] { MAIL_ADDRESS_1 }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
|
||||
assertEquals(USER_ID_1, cursor.getString(2));
|
||||
assertEquals(2, cursor.getInt(1));
|
||||
assertEquals(USER_ID_1, cursor.getString(1));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(2));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_autocryptPeer_withUnconfirmedKey() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
autocryptPeerDao.updateToAvailableState("tid", new Date(), KEY_ID_PUBLIC);
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID,
|
||||
AutocryptStatus.AUTOCRYPT_PEER_STATE
|
||||
},
|
||||
null, new String [] { AUTOCRYPT_PEER }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals("tid", cursor.getString(0));
|
||||
assertTrue(cursor.isNull(1));
|
||||
assertEquals(null, cursor.getString(2));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNVERIFIED, cursor.getInt(3));
|
||||
assertEquals(KEY_ID_PUBLIC, cursor.getLong(4));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE, cursor.getInt(5));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_available_withConfirmedKey() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
autocryptPeerDao.updateToAvailableState("tid", new Date(), KEY_ID_PUBLIC);
|
||||
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
|
||||
null, new String [] { AUTOCRYPT_PEER }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals("tid", cursor.getString(0));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertEquals(null, cursor.getString(2));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(3));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE, cursor.getInt(4));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_noData() throws Exception {
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
|
||||
null, new String [] { AUTOCRYPT_PEER }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals("tid", cursor.getString(0));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertEquals(null, cursor.getString(2));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(3));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_RESET, cursor.getInt(4));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_afterDelete() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
autocryptPeerDao.updateToGossipState("tid", new Date(), KEY_ID_PUBLIC);
|
||||
autocryptPeerDao.delete("tid");
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
|
||||
null, new String [] { AUTOCRYPT_PEER }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals("tid", cursor.getString(0));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertEquals(null, cursor.getString(2));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(3));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_RESET, cursor.getInt(4));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_stateGossip() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
autocryptPeerDao.updateToGossipState("tid", new Date(), KEY_ID_PUBLIC);
|
||||
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
|
||||
null, new String [] { AUTOCRYPT_PEER }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals("tid", cursor.getString(0));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertEquals(null, cursor.getString(2));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(3));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_GOSSIP, cursor.getInt(4));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_stateSelected() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
autocryptPeerDao.updateToSelectedState("tid", KEY_ID_PUBLIC);
|
||||
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
|
||||
null, new String [] { AUTOCRYPT_PEER }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals("tid", cursor.getString(0));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
|
||||
assertEquals(null, cursor.getString(2));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(3));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_SELECTED, cursor.getInt(4));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocryptStatus_withConfirmedKey() throws Exception {
|
||||
insertSecretKeyringFrom("/test-keys/testring.sec");
|
||||
insertPublicKeyringFrom("/test-keys/testring.pub");
|
||||
|
||||
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
|
||||
|
||||
Cursor cursor = contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI, new String[] {
|
||||
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
|
||||
AutocryptStatus.AUTOCRYPT_PEER_STATE },
|
||||
null, new String [] { MAIL_ADDRESS_1 }, null
|
||||
);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
|
||||
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(1));
|
||||
assertEquals(USER_ID_1, cursor.getString(2));
|
||||
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_RESET, cursor.getInt(3));
|
||||
assertFalse(cursor.moveToNext());
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = AccessControlException.class)
|
||||
public void testPermission__withExplicitPackage() throws Exception {
|
||||
contentResolver.query(
|
||||
AutocryptStatus.CONTENT_URI.buildUpon().appendPath("fake_pkg").build(),
|
||||
new String[] { AutocryptStatus.ADDRESS },
|
||||
null, new String [] { }, null
|
||||
);
|
||||
}
|
||||
|
||||
private void certifyKey(long secretMasterKeyId, long publicMasterKeyId, String userId) {
|
||||
CertifyActionsParcel.Builder certifyActionsParcel = CertifyActionsParcel.builder(secretMasterKeyId);
|
||||
certifyActionsParcel.addAction(
|
||||
|
||||
@@ -74,7 +74,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
assertArrayEqualsSorted(KEY_IDS, keyIdResult.getKeyIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void returnKeyIdsFromIntent__withUserIds__withEmptyQueryResult() throws Exception {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
|
||||
@@ -82,15 +82,10 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
setupContentResolverResult();
|
||||
|
||||
PendingIntent pendingIntent = mock(PendingIntent.class);
|
||||
setupPendingIntentFactoryResult(pendingIntent);
|
||||
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
|
||||
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
|
||||
BuildConfig.APPLICATION_ID);
|
||||
|
||||
|
||||
assertEquals(KeyIdResultStatus.NO_KEYS, keyIdResult.getStatus());
|
||||
assertTrue(keyIdResult.hasKeySelectionPendingIntent());
|
||||
openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false, BuildConfig.APPLICATION_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,7 +94,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[] { });
|
||||
|
||||
PendingIntent pendingIntent = mock(PendingIntent.class);
|
||||
setupPendingIntentFactoryResult(pendingIntent);
|
||||
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
|
||||
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
|
||||
@@ -117,7 +112,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[0]);
|
||||
|
||||
PendingIntent pendingIntent = mock(PendingIntent.class);
|
||||
setupPendingIntentFactoryResult(pendingIntent);
|
||||
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
|
||||
BuildConfig.APPLICATION_ID);
|
||||
@@ -135,7 +130,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
setupContentResolverResult();
|
||||
|
||||
PendingIntent pendingIntent = mock(PendingIntent.class);
|
||||
setupPendingIntentFactoryResult(pendingIntent);
|
||||
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
|
||||
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, true,
|
||||
@@ -152,7 +147,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
|
||||
|
||||
setupContentResolverResult(USER_IDS, new Long[] { 123L, 234L }, new int[] { 0, 0 });
|
||||
setupContentResolverResult(USER_IDS, new Long[] { 123L, 234L }, new int[] { 0, 0 }, new int[] { 1, 1, 1 });
|
||||
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
|
||||
@@ -170,11 +165,11 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
|
||||
|
||||
setupContentResolverResult(new String[] {
|
||||
USER_IDS[0], USER_IDS[0], USER_IDS[1]
|
||||
}, new Long[] { 123L, 345L, 234L }, new int[] { 0, 0, 0 });
|
||||
USER_IDS[0], USER_IDS[1]
|
||||
}, new Long[] { 123L, 234L }, new int[] { 0, 0 }, new int[] { 2, 1 });
|
||||
|
||||
PendingIntent pendingIntent = mock(PendingIntent.class);
|
||||
setupPendingIntentFactoryResult(pendingIntent);
|
||||
setupDeduplicatePendingIntentFactoryResult(pendingIntent);
|
||||
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
|
||||
@@ -190,10 +185,10 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
|
||||
|
||||
setupContentResolverResult(USER_IDS, new Long[] { null, 234L }, new int[] { 0, 0 });
|
||||
setupContentResolverResult(USER_IDS, new Long[] { null, 234L }, new int[] { 0, 0 }, new int[] { 0, 1 });
|
||||
|
||||
PendingIntent pendingIntent = mock(PendingIntent.class);
|
||||
setupPendingIntentFactoryResult(pendingIntent);
|
||||
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
|
||||
|
||||
|
||||
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
|
||||
@@ -205,16 +200,16 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
}
|
||||
|
||||
private void setupContentResolverResult() {
|
||||
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_KEY_SEARCH);
|
||||
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_MAIL_STATUS);
|
||||
when(contentResolver.query(
|
||||
any(Uri.class), any(String[].class), any(String.class), any(String[].class), any(String.class)))
|
||||
.thenReturn(resultCursor);
|
||||
}
|
||||
|
||||
private void setupContentResolverResult(String[] userIds, Long[] resultKeyIds, int[] verified) {
|
||||
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_KEY_SEARCH);
|
||||
private void setupContentResolverResult(String[] userIds, Long[] resultKeyIds, int[] verified, int[] candidates) {
|
||||
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_MAIL_STATUS);
|
||||
for (int i = 0; i < userIds.length; i++) {
|
||||
resultCursor.addRow(new Object[] { userIds[i], resultKeyIds[i], verified[i] });
|
||||
resultCursor.addRow(new Object[] { userIds[i], resultKeyIds[i], verified[i], candidates[i], null, null, null });
|
||||
}
|
||||
|
||||
when(contentResolver.query(
|
||||
@@ -222,12 +217,16 @@ public class OpenPgpServiceKeyIdExtractorTest {
|
||||
.thenReturn(resultCursor);
|
||||
}
|
||||
|
||||
private void setupPendingIntentFactoryResult(PendingIntent pendingIntent) {
|
||||
private void setupSelectPubkeyPendingIntentFactoryResult(PendingIntent pendingIntent) {
|
||||
when(apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
|
||||
any(Intent.class), any(long[].class), any(ArrayList.class), any(ArrayList.class), any(Boolean.class)))
|
||||
.thenReturn(pendingIntent);
|
||||
}
|
||||
|
||||
private void setupDeduplicatePendingIntentFactoryResult(PendingIntent pendingIntent) {
|
||||
when(apiPendingIntentFactory.createDeduplicatePendingIntent(
|
||||
any(String.class), any(Intent.class), any(ArrayList.class))).thenReturn(pendingIntent);
|
||||
}
|
||||
|
||||
private static void assertArrayEqualsSorted(long[] a, long[] b) {
|
||||
long[] tmpA = Arrays.copyOf(a, a.length);
|
||||
|
||||
2
extern/openpgp-api-lib
vendored
2
extern/openpgp-api-lib
vendored
Submodule extern/openpgp-api-lib updated: 395fe837a2...332bc7d2df
Reference in New Issue
Block a user