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);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
index 29d874f9a..785ba4ace 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
@@ -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
*
*
@@ -80,7 +89,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
*
*/
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 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();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
index 42d0b1490..7fa062915 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
@@ -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 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;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java
index db0e5e03b..92eba893d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java
@@ -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 keyIds = new HashSet<>();
ArrayList missingEmails = new ArrayList<>();
ArrayList duplicateEmails = new ArrayList<>();
+ AutocryptState combinedAutocryptState = null;
if (hasAddresses) {
- HashMap keyRows = getStatusMapForQueriedAddresses(encryptionAddresses, callingPackageName);
+ HashMap userIdEntries = getStatusMapForQueriedAddresses(
+ encryptionAddresses, callingPackageName);
boolean anyKeyNotVerified = false;
- for (Entry 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 getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
- HashMap 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 getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
+ HashMap 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 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 keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus) {
+
+ private KeyIdResult(HashSet keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus,
+ AutocryptState combinedAutocryptState) {
mKeySelectionPendingIntent = null;
mUserKeyIds = keyIds;
mAllKeysConfirmed = allKeysConfirmed;
mStatus = keyIdResultStatus;
mExplicitKeyIds = null;
+ mCombinedAutocryptState = combinedAutocryptState;
}
private KeyIdResult(KeyIdResult keyIdResult, HashSet 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 encryptKeyIds, boolean allKeysConfirmed) {
- return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK);
+ private KeyIdResult createKeysOkResult(HashSet 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 selectedKeyIds, ArrayList missingEmails, ArrayList duplicateEmails) {
- long[] keyIdsArray = KeyFormattingUtils.getUnboxedLongArray(selectedKeyIds);
- PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
- data, keyIdsArray, missingEmails, duplicateEmails, false);
+ private KeyIdResult createDuplicateKeysResult(Intent data, String packageName, ArrayList duplicateEmails) {
+ PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createDeduplicatePendingIntent(
+ packageName, data, duplicateEmails);
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.DUPLICATE);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java
new file mode 100644
index 000000000..53055b40a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017 Vincent Breitmoser
+ *
+ * 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 .
+ */
+
+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> {
+ // 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 cachedResult;
+
+
+ KeyLoader(Context context, ContentResolver contentResolver, String emailAddress) {
+ super(context);
+
+ this.contentResolver = contentResolver;
+ this.emailAddress = emailAddress;
+ }
+
+ @Override
+ public List loadInBackground() {
+ ArrayList 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 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);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java
new file mode 100644
index 000000000..3872ce352
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2017 Vincent Breitmoser
+ *
+ * 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 .
+ */
+
+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 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 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 {
+ private final LayoutInflater layoutInflater;
+ private final Resources resources;
+ private List 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 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);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java
new file mode 100644
index 000000000..5697dc9b3
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 Vincent Breitmoser
+ *
+ * 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 .
+ */
+
+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> {
+ 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 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> onCreateLoader(int id, Bundle args) {
+ return new KeyLoader(context, context.getContentResolver(), duplicateAddress);
+ }
+
+ @Override
+ public void onLoadFinished(Loader> loader, List 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 data);
+ void setActiveItem(Integer position);
+ void setEnableSelectButton(boolean enabled);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java
index e0a4df9c1..abc730292 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java
@@ -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 {
private CertListListener mListener;
@@ -162,11 +163,11 @@ public class CertSectionedListAdapter extends SectionCursorAdapter 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,
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java
index d7b7fea8d..52ef50b5c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java
@@ -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 {
private final Context context;
private final LayoutInflater layoutInflater;
private final boolean isSecret;
+ private final IdentityClickListener identityClickListener;
private List 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 data) {
@@ -75,7 +79,11 @@ public class IdentityAdapter extends RecyclerView.Adapter {
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 {
@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 {
@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 {
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 {
vName.setTypeface(null, Typeface.NORMAL);
vAddress.setTypeface(null, Typeface.NORMAL);
}
-
}
}
+
+ public interface IdentityClickListener {
+ void onClickIdentity(int position);
+ void onClickIdentityMore(int position, View anchor);
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java
index da7612143..53ad5f416 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java
@@ -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 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 0;
}
+
+ public List 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 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;
+ }
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java
index a5e6af591..fb7335373 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java
@@ -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;
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java
index a2edbaffe..a14287811 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java
@@ -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> {
@@ -68,6 +76,7 @@ public class IdentityLoader extends AsyncTaskLoader> {
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> {
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> {
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 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 resolveInfos = getContext().getPackageManager().queryIntentActivities(intent, 0);
+ if (resolveInfos != null && !resolveInfos.isEmpty()) {
+ return intent;
+ } else {
+ return null;
+ }
+ }
+
+ private static UserIdInfo findUserIdMatchingTrustId(List 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 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> {
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);
+ }
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java
index 0356b42d0..0680e09fe 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java
@@ -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>
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>
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>
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>
}
public interface IdentitiesCardListener {
- void onIdentityItemClick(int position);
-
void onClickEditIdentities();
void onClickAddIdentity();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java
index 98399a52c..3a4fa7f59 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java
@@ -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);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java
index d30f93093..0ddc6b160 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java
@@ -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() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PackageIconGetter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PackageIconGetter.java
new file mode 100644
index 000000000..0ea8c7b7c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PackageIconGetter.java
@@ -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 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;
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java
index 75b1c9ff0..06fa523d0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java
@@ -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
+public abstract class CursorAdapter
extends RecyclerView.Adapter {
public static final String TAG = "CursorAdapter";
@@ -58,7 +59,7 @@ public abstract class CursorAdapternot
+ * {@link #changeCursor(SimpleCursor)}, the returned old Cursor is not
* closed.
*
* @param newCursor The new cursor to be used.
@@ -312,10 +313,10 @@ public abstract class CursorAdapter T wrap(Cursor cursor, Class type) {
+ public static T wrap(Cursor cursor, Class type) {
if (cursor != null) {
try {
Constructor constructor = type.getConstructor(Cursor.class);
@@ -335,7 +336,7 @@ public abstract class CursorAdapter(cursor.getColumnCount() * 4 / 3, 0.75f);
}
@@ -376,12 +377,12 @@ public abstract class CursorAdapter 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 section type.
* @param the view holder extending {@code BaseViewHolder} that is bound to the cursor data.
* @param the view holder extending {@code BaseViewHolder<>} that is bound to the section data.
*/
-public abstract class SectionCursorAdapter extends CursorAdapter {
public static final String TAG = "SectionCursorAdapter";
diff --git a/OpenKeychain/src/main/res/drawable/ic_chat_black_24dp.xml b/OpenKeychain/src/main/res/drawable/ic_chat_black_24dp.xml
new file mode 100644
index 000000000..e3489bdea
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable/ic_chat_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml b/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml
new file mode 100644
index 000000000..b02f0f053
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/duplicate_key_item.xml b/OpenKeychain/src/main/res/layout/duplicate_key_item.xml
new file mode 100644
index 000000000..440e35c43
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/duplicate_key_item.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenKeychain/src/main/res/layout/key_list_item.xml b/OpenKeychain/src/main/res/layout/key_list_item.xml
index 43f12342b..d106a29e5 100644
--- a/OpenKeychain/src/main/res/layout/key_list_item.xml
+++ b/OpenKeychain/src/main/res/layout/key_list_item.xml
@@ -2,7 +2,6 @@
+ android:focusable="false"
+ tools:layout_marginTop="30dp">
+
+
+ android:paddingBottom="4dp">
+ android:orientation="horizontal"
+ tools:visibility="gone">
+ tools:layout_height="match_parent"
+ tools:orientation="vertical">
-
-
-
-
-
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/trust_id_icon.xml b/OpenKeychain/src/main/res/layout/trust_id_icon.xml
new file mode 100644
index 000000000..706d3ea04
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/trust_id_icon.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/view_key_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_fragment.xml
index 16fa99c97..664ffaada 100644
--- a/OpenKeychain/src/main/res/layout/view_key_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/view_key_fragment.xml
@@ -22,7 +22,8 @@
+ android:orientation="vertical"
+ android:animateLayoutChanges="true">
+ android:orientation="horizontal"
+ android:maxLines="1"
+ android:padding="8dp"
+ android:background="?selectableItemBackground"
+ >
-
+
+
+
+
+
+
+
+
+
+
+
+ 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"
+ />
-
-
-
diff --git a/OpenKeychain/src/main/res/layout/view_key_trust_id_item.xml b/OpenKeychain/src/main/res/layout/view_key_trust_id_item.xml
new file mode 100644
index 000000000..101caa48b
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/view_key_trust_id_item.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/menu/identity_context_menu.xml b/OpenKeychain/src/main/res/menu/identity_context_menu.xml
new file mode 100644
index 000000000..1cb44614b
--- /dev/null
+++ b/OpenKeychain/src/main/res/menu/identity_context_menu.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index 680d27bac..23c94a904 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -1886,4 +1886,6 @@
View Key
Got it
+ Forget
+
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java
index 98623da74..9b80e8e16 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java
@@ -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 ";
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 ", 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 ", 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(
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java
index 2cf52a05a..46b98e9fc 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java
@@ -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);
diff --git a/extern/openpgp-api-lib b/extern/openpgp-api-lib
index 395fe837a..332bc7d2d 160000
--- a/extern/openpgp-api-lib
+++ b/extern/openpgp-api-lib
@@ -1 +1 @@
-Subproject commit 395fe837a23fc33c7a4abbb0625cb584c5dbb176
+Subproject commit 332bc7d2df0c962f6a855766b55dafa6f8c89f95