diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 1d527c131..84047f775 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -94,6 +94,9 @@ dependencies { annotationProcessor "com.google.auto.value:auto-value:1.5" annotationProcessor "com.ryanharter.auto.value:auto-value-parcel:0.2.5" compile 'com.ryanharter.auto.value:auto-value-parcel-adapter:0.2.5' + + compile "android.arch.lifecycle:extensions:1.0.0" + annotationProcessor "android.arch.lifecycle:compiler:1.0.0" } // Output of ./gradlew -q calculateChecksums @@ -144,6 +147,13 @@ dependencyVerification { 'com.squareup.okio:okio:734269c3ebc5090e3b23566db558f421f0b4027277c79ad5d176b8ec168bb850', 'com.fidesmo:nordpol-core:296e71b12884a9cd28cf00ab908973bbf776a90be1f23ac897380d91604e614d', 'com.jakewharton.timber:timber:d553d3d3e883ce7d061f1b21b95d6ee0840f3bfbf6d3bd51c5671f0b0f0b0091', + 'android.arch.lifecycle:runtime:d0b36278878c82b838acc4308595bec61a3b5f6e7f2acc34172d7e071b2cf26d', + 'android.arch.lifecycle:common:ff0215b54e7cbaaa898f8fd00e265ed6ea198859e10604bc1c5e78477df48b5c', + 'android.arch.core:common:5192934cd73df32e2c15722ed7fc488dde90baaec9ae030010dd1a80fb4e74e1', + 'android.arch.lifecycle:runtime:d0b36278878c82b838acc4308595bec61a3b5f6e7f2acc34172d7e071b2cf26d', + 'android.arch.core:runtime:9e08fc5c4d6e48f58c6865b55ba0e72a88f907009407767274187a873e524734', + 'android.arch.core:common:5192934cd73df32e2c15722ed7fc488dde90baaec9ae030010dd1a80fb4e74e1', + 'android.arch.lifecycle:common:ff0215b54e7cbaaa898f8fd00e265ed6ea198859e10604bc1c5e78477df48b5c', ] } diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index fa0286a90..6b7677a14 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -881,6 +881,11 @@ android:name=".remote.ui.RemoteSelectPubKeyActivity" android:exported="false" android:label="@string/app_name" /> + . */ -package org.sufficientlysecure.keychain.remote.ui.dialog; +package org.sufficientlysecure.keychain.livedata; import java.util.ArrayList; @@ -23,19 +23,17 @@ import java.util.Collections; import java.util.List; import android.content.ContentResolver; -import android.content.Context; import android.database.Cursor; import android.net.Uri; 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; +import org.sufficientlysecure.keychain.util.DatabaseUtil; -public class KeyLoader extends AsyncTaskLoader> { +public class KeyInfoInteractor { // These are the rows that we will retrieve. private String[] QUERY_PROJECTION = new String[]{ KeyRings._ID, @@ -61,29 +59,27 @@ public class KeyLoader extends AsyncTaskLoader> { private static final int INDEX_EMAIL = 8; private static final int INDEX_COMMENT = 9; - private static final String QUERY_WHERE = Tables.KEYS + "." + KeyRings.IS_REVOKED + + private static final String QUERY_WHERE_ALL = Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0 AND " + KeyRings.IS_EXPIRED + " = 0"; + private static final String QUERY_WHERE_SECRET = Tables.KEYS + "." + KeyRings.IS_REVOKED + + " = 0 AND " + KeyRings.IS_EXPIRED + " = 0" + " AND " + KeyRings.HAS_ANY_SECRET + " != 0"; private static final String QUERY_ORDER = Tables.KEYS + "." + KeyRings.CREATION + " DESC"; private final ContentResolver contentResolver; - private final KeySelector keySelector; - private List cachedResult; - - KeyLoader(Context context, ContentResolver contentResolver, KeySelector keySelector) { - super(context); + public KeyInfoInteractor(ContentResolver contentResolver) { this.contentResolver = contentResolver; - this.keySelector = keySelector; } - @Override - public List loadInBackground() { + public List loadKeyInfo(KeySelector keySelector) { ArrayList keyInfos = new ArrayList<>(); Cursor cursor; + String selection = keySelector.isOnlySecret() ? QUERY_WHERE_SECRET : QUERY_WHERE_ALL; String additionalSelection = keySelector.getSelection(); - String selection = QUERY_WHERE + (additionalSelection != null ? " AND " + additionalSelection : ""); + + selection = DatabaseUtil.concatenateWhere(selection, additionalSelection); cursor = contentResolver.query(keySelector.getKeyRingUri(), QUERY_PROJECTION, selection, null, QUERY_ORDER); if (cursor == null) { @@ -98,33 +94,6 @@ public class KeyLoader extends AsyncTaskLoader> { 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(); @@ -153,7 +122,7 @@ public class KeyLoader extends AsyncTaskLoader> { String email = cursor.getString(INDEX_EMAIL); String comment = cursor.getString(INDEX_COMMENT); - return new AutoValue_KeyLoader_KeyInfo( + return new AutoValue_KeyInfoInteractor_KeyInfo( masterKeyId, creationDate, hasEncrypt, hasAuthenticate, hasAnySecret, isVerified, name, email, comment); } } @@ -163,9 +132,14 @@ public class KeyLoader extends AsyncTaskLoader> { public abstract Uri getKeyRingUri(); @Nullable public abstract String getSelection(); + public abstract boolean isOnlySecret(); - static KeySelector create(Uri keyRingUri, String selection) { - return new AutoValue_KeyLoader_KeySelector(keyRingUri, selection); + public static KeySelector create(Uri keyRingUri, String selection) { + return new AutoValue_KeyInfoInteractor_KeySelector(keyRingUri, selection, false); + } + + public static KeySelector createOnlySecret(Uri keyRingUri, String selection) { + return new AutoValue_KeyInfoInteractor_KeySelector(keyRingUri, selection, true); } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java new file mode 100644 index 000000000..502ab2f20 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java @@ -0,0 +1,38 @@ +package org.sufficientlysecure.keychain.livedata; + + +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; + +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; +import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; + + +public class KeyInfoLiveData extends AsyncTaskLiveData> { + private final KeyInfoInteractor keyInfoInteractor; + + private KeySelector keySelector; + + public KeyInfoLiveData(Context context, ContentResolver contentResolver) { + super(context, null); + + this.keyInfoInteractor = new KeyInfoInteractor(contentResolver); + } + + public void setKeySelector(KeySelector keySelector) { + this.keySelector = keySelector; + + updateDataInBackground(); + } + + @Override + protected List asyncLoadData() { + if (keySelector == null) { + return null; + } + return keyInfoInteractor.loadKeyInfo(keySelector); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java new file mode 100644 index 000000000..d9515600d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java @@ -0,0 +1,38 @@ +package org.sufficientlysecure.keychain.livedata; + + +import android.content.Context; + +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; +import org.sufficientlysecure.keychain.util.ProgressScaler; + + +public class PgpKeyGenerationLiveData extends AsyncTaskLiveData { + private SaveKeyringParcel saveKeyringParcel; + + public PgpKeyGenerationLiveData(Context context) { + super(context, null); + } + + public void setSaveKeyringParcel(SaveKeyringParcel saveKeyringParcel) { + if (this.saveKeyringParcel == saveKeyringParcel) { + return; + } + this.saveKeyringParcel = saveKeyringParcel; + + updateDataInBackground(); + } + + @Override + protected PgpEditKeyResult asyncLoadData() { + if (saveKeyringParcel == null) { + return null; + } + + PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler()); + return keyOperations.createSecretKeyRing(saveKeyringParcel); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java index c2a1ee642..ed33c1863 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java @@ -99,8 +99,7 @@ public class ApiDataAccessObject { } public void insertApiApp(AppSettings appSettings) { - mQueryInterface.insert(ApiApps.CONTENT_URI, - contentValueForApiApps(appSettings)); + mQueryInterface.insert(ApiApps.CONTENT_URI, contentValueForApiApps(appSettings)); } public void deleteApiApp(String packageName) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java index b0de16a63..077c72285 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java @@ -92,6 +92,16 @@ public class CachedPublicKeyRing extends KeyRing { } } + public long getCreationTime() throws PgpKeyNotFoundException { + try { + Object data = mKeyRepository.getGenericData(mUri, + KeychainContract.KeyRings.CREATION, KeyRepository.FIELD_TYPE_INTEGER); + return (long) data; + } catch (KeyWritableRepository.NotFoundException e) { + throw new PgpKeyNotFoundException(e); + } + } + @Override public String getPrimaryUserId() throws PgpKeyNotFoundException { try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index bd5881eda..4dce0d44c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -944,7 +944,7 @@ public class KeychainProvider extends ContentProvider { break; } case API_APPS: { - db.insertOrThrow(Tables.API_APPS, null, values); + db.insert(Tables.API_APPS, null, values); break; } case API_ALLOWED_KEYS: { @@ -953,7 +953,7 @@ public class KeychainProvider extends ContentProvider { String packageName = uri.getPathSegments().get(1); values.put(ApiAllowedKeys.PACKAGE_NAME, packageName); - db.insertOrThrow(Tables.API_ALLOWED_KEYS, null, values); + db.insert(Tables.API_ALLOWED_KEYS, null, values); break; } default: { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java index 9634bd689..eb988e045 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java @@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.remote.ui.RequestKeyPermissionActivity; import org.sufficientlysecure.keychain.remote.ui.SelectSignKeyIdActivity; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyActivity; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectIdKeyActivity; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; @@ -133,7 +134,7 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } - PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName, String preferredUserId) { + PendingIntent createSelectSignKeyIdLegacyPendingIntent(Intent data, String packageName, String preferredUserId) { Intent intent = new Intent(mContext, SelectSignKeyIdActivity.class); intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); @@ -141,6 +142,18 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } + PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName, + byte[] packageSignature, String preferredUserId, boolean showAutocryptHint) { + Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class); + intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, preferredUserId); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_SHOW_AUTOCRYPT_HINT, showAutocryptHint); + + return createInternal(data, intent); + } + PendingIntent createSelectAuthenticationKeyIdPendingIntent(Intent data, String packageName) { Intent intent = new Intent(mContext, RemoteSelectAuthenticationKeyActivity.class); intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); @@ -182,7 +195,9 @@ public class ApiPendingIntentFactory { private PendingIntent createInternal(Intent data, Intent intent) { // re-attach "data" for pass through. It will be used later to repeat pgp operation - intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_DATA, data); + if (data != null) { + intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_DATA, data); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //noinspection ResourceType, looks like lint is missing FLAG_IMMUTABLE diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java index 97aa7035e..fa6d04bbd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java @@ -65,7 +65,7 @@ public class ApiPermissionHelper { /** Returns true iff the caller is allowed, or false on any type of problem. * This method should only be used in cases where error handling is dealt with separately. */ - protected boolean isAllowedIgnoreErrors() { + public boolean isAllowedIgnoreErrors() { try { return isCallerAllowed(); } catch (WrongPackageCertificateException e) { @@ -124,6 +124,14 @@ public class ApiPermissionHelper { } } + byte[] getPackageCertificateOrError(String packageName) { + try { + return getPackageCertificate(packageName); + } catch (NameNotFoundException e) { + throw new AssertionError("Package signature must be retrievable"); + } + } + private byte[] getPackageCertificate(String packageName) throws NameNotFoundException { @SuppressLint("PackageManagerGetSignatures") // we do check the byte array of *all* signatures PackageInfo pkgInfo = mContext.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES); 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 b4cda1d77..c2bcb0c30 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -39,6 +39,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.openintents.openpgp.AutocryptPeerUpdate; @@ -67,6 +68,7 @@ import org.sufficientlysecure.keychain.pgp.SecurityProblem; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; @@ -196,8 +198,21 @@ public class OpenPgpService extends Service { } } + private Intent autocryptQueryImpl(Intent data) { + try { + KeyIdResult keyIdResult = mKeyIdExtractor.returnKeyIdsFromIntent(data, false, + mApiPermissionHelper.getCurrentCallingPackage()); + Intent resultIntent = getAutocryptStatusResult(keyIdResult); + + return resultIntent; + } catch (Exception e) { + Timber.d(e, "encryptAndSignImpl"); + return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage()); + } + } + private Intent encryptAndSignImpl(Intent data, InputStream inputStream, - OutputStream outputStream, boolean sign, boolean isQueryAutocryptStatus) { + OutputStream outputStream, boolean sign) { try { PgpSignEncryptData.Builder pgpData = PgpSignEncryptData.builder() .setVersionHeader(null); @@ -225,9 +240,6 @@ public class OpenPgpService extends Service { mApiPermissionHelper.getCurrentCallingPackage()); KeyIdResultStatus keyIdResultStatus = keyIdResult.getStatus(); - if (isQueryAutocryptStatus) { - return getAutocryptStatusResult(keyIdResult); - } boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); pgpData.setEnableAsciiArmorOutput(asciiArmor); @@ -672,7 +684,8 @@ public class OpenPgpService extends Service { return result; } - private Intent getSignKeyIdImpl(Intent data) { + /* Signing key choose dialog for older API versions. We keep it around to make sure those don't break */ + private Intent getSignKeyIdImplLegacy(Intent data) { // if data already contains EXTRA_SIGN_KEY_ID, it has been executed again // after user interaction. Then, we just need to return the long again! if (data.hasExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { @@ -686,7 +699,8 @@ public class OpenPgpService extends Service { String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); - PendingIntent pi = mApiPendingIntentFactory.createSelectSignKeyIdPendingIntent(data, currentPkg, preferredUserId); + PendingIntent pi = mApiPendingIntentFactory.createSelectSignKeyIdLegacyPendingIntent( + data, currentPkg, preferredUserId); // return PendingIntent to be executed by client Intent result = new Intent(); @@ -697,6 +711,53 @@ public class OpenPgpService extends Service { } } + private Intent getSignKeyIdImpl(Intent data) { + Intent result = new Intent(); + data.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); + + { // return PendingIntent to be executed by client + String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); + String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); + PendingIntent pi; + // the new dialog doesn't really work if we don't have a user id to work with. just show the old... + if (TextUtils.isEmpty(preferredUserId)) { + pi = mApiPendingIntentFactory.createSelectSignKeyIdLegacyPendingIntent(data, currentPkg, null); + } else { + byte[] packageSignature = mApiPermissionHelper.getPackageCertificateOrError(currentPkg); + boolean showAutocryptHint = data.getBooleanExtra(OpenPgpApi.EXTRA_SHOW_AUTOCRYPT_HINT, false); + pi = mApiPendingIntentFactory.createSelectSignKeyIdPendingIntent( + data, currentPkg, packageSignature, preferredUserId, showAutocryptHint); + } + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + } + + long signKeyId; + if (data.hasExtra(OpenPgpApi.RESULT_SIGN_KEY_ID)) { + signKeyId = data.getLongExtra(OpenPgpApi.RESULT_SIGN_KEY_ID, Constants.key.none); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + } else { + signKeyId = data.getLongExtra(OpenPgpApi.EXTRA_PRESELECT_KEY_ID, Constants.key.none); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + } + result.putExtra(OpenPgpApi.RESULT_SIGN_KEY_ID, signKeyId); + + if (signKeyId != Constants.key.none) { + try { + CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(signKeyId); + String userId = cachedPublicKeyRing.getPrimaryUserId(); + long creationTime = cachedPublicKeyRing.getCreationTime() * 1000; + + result.putExtra(OpenPgpApi.RESULT_PRIMARY_USER_ID, userId); + result.putExtra(OpenPgpApi.RESULT_KEY_CREATION_TIME, creationTime); + } catch (PgpKeyNotFoundException e) { + Timber.e(e, "Error loading key info"); + return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage()); + } + } + + return result; + } + private Intent getKeyIdsImpl(Intent data) { KeyIdResult keyIdResult = mKeyIdExtractor.returnKeyIdsFromIntent(data, true, mApiPermissionHelper.getCurrentCallingPackage()); @@ -842,6 +903,11 @@ public class OpenPgpService extends Service { return result; } + // special exception: getting a sign key id will also register the app + if (OpenPgpApi.ACTION_GET_SIGN_KEY_ID.equals(data.getAction())) { + return null; + } + // check if caller is allowed to access OpenKeychain Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data); if (result != null) { @@ -936,12 +1002,12 @@ public class OpenPgpService extends Service { return signImpl(data, inputStream, outputStream, false); } case OpenPgpApi.ACTION_QUERY_AUTOCRYPT_STATUS: { - return encryptAndSignImpl(data, inputStream, outputStream, false, true); + return autocryptQueryImpl(data); } case OpenPgpApi.ACTION_ENCRYPT: case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: { boolean enableSign = action.equals(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - return encryptAndSignImpl(data, inputStream, outputStream, enableSign, false); + return encryptAndSignImpl(data, inputStream, outputStream, enableSign); } case OpenPgpApi.ACTION_DECRYPT_VERIFY: { return decryptAndVerifyImpl(data, inputStream, outputStream, false, progressable); @@ -952,6 +1018,9 @@ public class OpenPgpService extends Service { case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: { return getSignKeyIdImpl(data); } + case OpenPgpApi.ACTION_GET_SIGN_KEY_ID_LEGACY: { + return getSignKeyIdImplLegacy(data); + } case OpenPgpApi.ACTION_GET_KEY_IDS: { return getKeyIdsImpl(data); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java new file mode 100644 index 000000000..3b6d033c2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * 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; + + +import android.app.Activity; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.widget.LinearLayoutManager; + +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.remote.ui.adapter.SelectIdentityKeyAdapter; +import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; +import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; + + +public class SelectIdentityKeyListFragment extends RecyclerFragment + implements SelectIdentityKeyAdapter.SelectSignKeyListener, LoaderManager.LoaderCallbacks { + private static final String ARG_API_IDENTITY = "api_identity"; + private String apiIdentity; + private boolean listAllKeys; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.getString(ARG_API_IDENTITY, apiIdentity); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null && apiIdentity == null) { + apiIdentity = getArguments().getString(ARG_API_IDENTITY); + } + + SelectIdentityKeyAdapter adapter = new SelectIdentityKeyAdapter(getContext(), null); + adapter.setListener(this); + + setAdapter(adapter); + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration(getContext(), layoutManager.getOrientation(), true); + setLayoutManager(layoutManager); + getRecyclerView().addItemDecoration(dividerItemDecoration); + + // Start out with a progress indicator. + hideList(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + // These are the rows that we will retrieve. + String[] projection = new String[]{ + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + KeyRings.USER_ID, + KeyRings.IS_EXPIRED, + KeyRings.IS_REVOKED, + KeyRings.HAS_ENCRYPT, + KeyRings.VERIFIED, + KeyRings.HAS_ANY_SECRET, + KeyRings.HAS_DUPLICATE_USER_ID, + KeyRings.CREATION, + KeyRings.NAME, + KeyRings.EMAIL, + KeyRings.COMMENT, + }; + + String selection = KeyRings.HAS_ANY_SECRET + " != 0"; + Uri baseUri = listAllKeys ? KeyRings.buildUnifiedKeyRingsUri() : + KeyRings.buildUnifiedKeyRingsFindByEmailUri(apiIdentity); + + String orderBy = KeyRings.USER_ID + " ASC"; + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), baseUri, projection, selection, null, orderBy); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + getAdapter().swapCursor(CursorAdapter.KeyCursor.wrap(data)); + + // The list should now be shown. + if (isResumed()) { + showList(true); + } else { + showList(false); + } + + boolean isEmpty = data.getCount() == 0; + getKeySelectFragmentListener().onChangeListEmptyStatus(isEmpty); + } + + @Override + public void onLoaderReset(Loader loader) { + // This is called when the last Cursor provided to onLoadFinished() + // above is about to be closed. We need to make sure we are no + // longer using it. + getAdapter().swapCursor(null); + } + + @Override + public void onDestroy() { + getAdapter().setListener(null); + super.onDestroy(); + } + + @Override + public void onSelectKeyItemClicked(long masterKeyId) { + getKeySelectFragmentListener().onKeySelected(masterKeyId); + } + + SelectIdentityKeyFragmentListener getKeySelectFragmentListener() { + Activity activity = getActivity(); + if (activity == null) { + return null; + } + + if (!(activity instanceof SelectIdentityKeyFragmentListener)) { + throw new IllegalStateException("SelectIdentityKeyListFragment must be attached to KeySelectFragmentListener!"); + } + + return (SelectIdentityKeyFragmentListener) activity; + } + + public void setApiIdentity(String apiIdentity) { + this.apiIdentity = apiIdentity; + } + + public void setListAllKeys(boolean listAllKeys) { + this.listAllKeys = listAllKeys; + getLoaderManager().restartLoader(0, null, this); + } + + public interface SelectIdentityKeyFragmentListener { + void onKeySelected(Long masterKeyId); + void onChangeListEmptyStatus(boolean isEmpty); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java new file mode 100644 index 000000000..66e5baee8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Tobias Erthal + * Copyright (C) 2014-2016 Dominik Schürmann + * + * 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.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.KeyCursorAdapter; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; + + +public class SelectIdentityKeyAdapter extends KeyCursorAdapter { + private SelectSignKeyListener mListener; + + public SelectIdentityKeyAdapter(Context context, Cursor cursor) { + super(context, KeyCursor.wrap(cursor)); + } + + public void setListener(SelectSignKeyListener listener) { + mListener = listener; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new SignKeyItemHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.select_identity_key_item, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, KeyCursor cursor, String query) { + ((SignKeyItemHolder) holder).bind(cursor, query); + } + + private class SignKeyItemHolder extends RecyclerView.ViewHolder + implements View.OnClickListener { + + private TextView userIdText; + private TextView creationText; + private ImageView statusIcon; + + SignKeyItemHolder(View itemView) { + super(itemView); + itemView.setClickable(true); + itemView.setOnClickListener(this); + + userIdText = (TextView) itemView.findViewById(R.id.select_key_item_name); + creationText = (TextView) itemView.findViewById(R.id.select_key_item_creation); + statusIcon = (ImageView) itemView.findViewById(R.id.select_key_item_status_icon); + } + + public void bind(KeyCursor cursor, String query) { + Context context = itemView.getContext(); + + { // set name and stuff, common to both key types + String name = cursor.getName(); + if (name != null) { + userIdText.setText(context.getString(R.string.use_key, name)); + } else { + String email = cursor.getEmail(); + userIdText.setText(context.getString(R.string.use_key, email)); + } + } + + { // set edit button and status, specific by key type. Note: order is important! + int textColor; + if (cursor.isRevoked()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.REVOKED, + R.color.key_flag_gray + ); + + itemView.setEnabled(false); + statusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (cursor.isExpired()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.EXPIRED, + R.color.key_flag_gray + ); + + itemView.setEnabled(false); + statusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else { + itemView.setEnabled(true); + statusIcon.setImageResource(R.drawable.ic_vpn_key_grey_24dp); + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } + + userIdText.setTextColor(textColor); + + String dateTime = DateUtils.formatDateTime(context, + cursor.getCreationTime(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_ABBREV_MONTH); + creationText.setText(context.getString(R.string.label_key_created, + dateTime)); + creationText.setTextColor(textColor); + creationText.setVisibility(View.VISIBLE); + } + + } + + @Override + public void onClick(View v) { + if (mListener != null) { + mListener.onSelectKeyItemClicked(getItemId()); + } + } + } + + public interface SelectSignKeyListener { + void onSelectKeyItemClicked(long masterKeyId); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java new file mode 100644 index 000000000..31c87ac58 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * 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.ContentResolver; +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; + +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; + + +public class KeyInfoLoader extends AsyncTaskLoader> { + private final KeySelector keySelector; + + private List cachedResult; + private KeyInfoInteractor keyInfoInteractor; + + KeyInfoLoader(Context context, ContentResolver contentResolver, KeySelector keySelector) { + super(context); + + this.keySelector = keySelector; + this.keyInfoInteractor = new KeyInfoInteractor(contentResolver); + } + + @Override + public List loadInBackground() { + return keyInfoInteractor.loadKeyInfo(keySelector); + } + + @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; + } +} 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 index e1fd3edd4..adb079821 100644 --- 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 @@ -51,7 +51,7 @@ import android.widget.TextView; import com.mikepenz.materialdrawer.util.KeyboardUtil; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.remote.ui.RemoteSecurityTokenOperationActivity; -import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicatePresenter.RemoteDeduplicateView; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; 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 index 4717f7da5..ffda9a81d 100644 --- 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 @@ -33,8 +33,8 @@ import android.support.v4.content.Loader; import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; -import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeySelector; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; import timber.log.Timber; @@ -95,7 +95,7 @@ class RemoteDeduplicatePresenter implements LoaderCallbacks> { KeySelector keySelector = KeySelector.create( KeyRings.buildUnifiedKeyRingsFindByEmailUri(duplicateAddress), null); - return new KeyLoader(context, context.getContentResolver(), keySelector); + return new KeyInfoLoader(context, context.getContentResolver(), keySelector); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java index 73ef66dee..367fc496b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java @@ -54,7 +54,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.remote.ui.RemoteSecurityTokenOperationActivity; -import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyPresenter.RemoteSelectAuthenticationKeyView; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java index 2bbe57e92..c9e0f70b0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java @@ -18,6 +18,8 @@ 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; @@ -29,12 +31,10 @@ import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; -import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeySelector; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; import timber.log.Timber; -import java.util.List; - class RemoteSelectAuthenticationKeyPresenter implements LoaderCallbacks> { private final PackageManager packageManager; @@ -84,7 +84,7 @@ class RemoteSelectAuthenticationKeyPresenter implements LoaderCallbacks. + */ + +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.arch.lifecycle.ViewModelProviders; +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.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.transition.Fade; +import android.support.transition.Transition; +import android.support.transition.TransitionListenerAdapter; +import android.support.transition.TransitionManager; +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.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import com.mikepenz.materialdrawer.util.KeyboardUtil; +import org.openintents.openpgp.util.OpenPgpApi; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectIdentityKeyPresenter.RemoteSelectIdentityKeyView; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.MainActivity; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.AbstractCallback; +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; +import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator; +import timber.log.Timber; + + +public class RemoteSelectIdKeyActivity extends FragmentActivity { + public static final String EXTRA_PACKAGE_NAME = "package_name"; + public static final String EXTRA_PACKAGE_SIGNATURE = "package_signature"; + public static final String EXTRA_USER_ID = "user_id"; + public static final String EXTRA_SHOW_AUTOCRYPT_HINT = "show_autocrypt_hint"; + public static final String EXTRA_CURRENT_MASTER_KEY_ID = "current_master_key_id"; + + + private RemoteSelectIdentityKeyPresenter presenter; + private Parcelable currentlyImportingParcel; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + RemoteSelectIdViewModel viewModel = + ViewModelProviders.of(this).get(RemoteSelectIdViewModel.class); + + presenter = new RemoteSelectIdentityKeyPresenter(getBaseContext(), viewModel, this); + + KeyboardUtil.hideKeyboard(this); + + if (savedInstanceState == null) { + RemoteSelectIdentityKeyDialogFragment frag = new RemoteSelectIdentityKeyDialogFragment(); + frag.show(getSupportFragmentManager(), "requestKeyDialog"); + } + } + + @Override + protected void onStart() { + super.onStart(); + + Intent intent = getIntent(); + String userId = intent.getStringExtra(EXTRA_USER_ID); + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + byte[] packageSignature = intent.getByteArrayExtra(EXTRA_PACKAGE_SIGNATURE); + boolean showAutocryptHint = intent.getBooleanExtra(EXTRA_SHOW_AUTOCRYPT_HINT, false); + + presenter.setupFromIntentData(packageName, packageSignature, userId, showAutocryptHint); + } + + public static class RemoteSelectIdentityKeyDialogFragment extends DialogFragment { + private RemoteSelectIdentityKeyPresenter presenter; + private RemoteSelectIdentityKeyView mvpView; + + private RecyclerView keyChoiceList; + private View buttonKeyListCancel; + private View buttonNoKeysNew; + private View buttonExplBack; + private View buttonExplGotIt; + private View buttonGenOkBack; + private View buttonGenOkFinish; + private View buttonNoKeysCancel; + private View buttonNoKeysExisting; + private View buttonKeyListOther; + private View buttonOverflow; + private View buttonGotoOpenKeychain; + + @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") + ViewGroup view = (ViewGroup) layoutInflater.inflate(R.layout.api_select_identity_key, null, false); + alert.setView(view); + + buttonOverflow = view.findViewById(R.id.overflow_menu); + + buttonKeyListCancel = view.findViewById(R.id.button_key_list_cancel); + buttonKeyListOther = view.findViewById(R.id.button_key_list_other); + + buttonNoKeysNew = view.findViewById(R.id.button_no_keys_new); + buttonNoKeysExisting = view.findViewById(R.id.button_no_keys_existing); + buttonNoKeysCancel = view.findViewById(R.id.button_no_keys_cancel); + + buttonExplBack = view.findViewById(R.id.button_expl_back); + buttonExplGotIt = view.findViewById(R.id.button_expl_got_it); + + buttonGenOkBack = view.findViewById(R.id.button_genok_back); + buttonGenOkFinish = view.findViewById(R.id.button_genok_finish); + + buttonGotoOpenKeychain = view.findViewById(R.id.button_goto_openkeychain); + + keyChoiceList = view.findViewById(R.id.identity_key_list); + keyChoiceList.setLayoutManager(new LinearLayoutManager(activity)); + keyChoiceList.addItemDecoration( + new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true)); + + setupListenersForPresenter(); + mvpView = createMvpView(view, layoutInflater); + + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + presenter = ((RemoteSelectIdKeyActivity) getActivity()).presenter; + presenter.setView(mvpView); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + if (presenter != null) { + presenter.onDialogCancel(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + + if (presenter != null) { + presenter.setView(null); + presenter = null; + } + } + + @NonNull + private RemoteSelectIdentityKeyView createMvpView(final ViewGroup rootView, LayoutInflater layoutInflater) { + // final ImageView iconClientApp = rootView.findViewById(R.id.icon_client_app); + final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + final TextView titleText = rootView.findViewById(R.id.text_title_select_key); + final TextView addressText = rootView.findViewById(R.id.text_user_id); + final TextView autocryptHint = rootView.findViewById(R.id.key_import_autocrypt_hint); + final ToolableViewAnimator layoutAnimator = rootView.findViewById(R.id.layout_animator); + keyChoiceList.setAdapter(keyChoiceAdapter); + + return new RemoteSelectIdentityKeyView() { + @Override + public void finishAndReturn(long masterKeyId) { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + Intent resultData = new Intent(); + resultData.putExtra(OpenPgpApi.RESULT_SIGN_KEY_ID, masterKeyId); + activity.setResult(RESULT_OK, resultData); + activity.finish(); + } + + @Override + public void finishAsCancelled() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + activity.setResult(RESULT_CANCELED); + activity.finish(); + } + + @Override + public void setTitleClientIconAndName(Drawable drawable, CharSequence name) { + titleText.setText(getString(R.string.title_select_key, name)); + autocryptHint.setText(getString(R.string.key_import_text_autocrypt_setup_msg, name)); + // iconClientApp.setImageDrawable(drawable); + setSelectionIcons(drawable); + } + + @Override + public void setShowAutocryptHint(boolean showAutocryptHint) { + autocryptHint.setVisibility(showAutocryptHint ? View.VISIBLE : View.GONE); + } + + private void setSelectionIcons(Drawable drawable) { + ConstantState constantState = drawable.getConstantState(); + if (constantState == null) { + return; + } + + Resources resources = getResources(); + Drawable iconSelected = constantState.newDrawable(resources); + Drawable iconUnselected = constantState.newDrawable(resources); + DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_600, null)); + + keyChoiceAdapter.setSelectionDrawables(iconSelected, iconUnselected); + } + + @Override + public void setAddressText(String text) { + addressText.setText(text); + } + + @Override + public void showLayoutEmpty() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_empty); + } + + @Override + public void showLayoutSelectNoKeys() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_no_keys); + buttonOverflow.setVisibility(View.GONE); + } + + @Override + public void showLayoutSelectKeyList() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_key_list); + buttonOverflow.setVisibility(View.VISIBLE); + } + + @Override + public void showLayoutImportExplanation() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_import_expl); + buttonOverflow.setVisibility(View.VISIBLE); + } + + @Override + public void showLayoutGenerateProgress() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_progress); + buttonOverflow.setVisibility(View.GONE); + } + + @Override + public void showLayoutGenerateOk() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_ok); + buttonOverflow.setVisibility(View.GONE); + } + + @Override + public void showLayoutGenerateSave() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_save); + buttonOverflow.setVisibility(View.GONE); + } + + @Override + public void setKeyListData(List data) { + keyChoiceAdapter.setData(data); + } + + @Override + public void highlightKey(int position) { + Transition transition = new Fade().setDuration(450) + .addTarget(LinearLayout.class) + .addTarget(ImageView.class) + .addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(@NonNull Transition transition) { + presenter.onHighlightFinished(); + } + }); + TransitionManager.beginDelayedTransition(rootView, transition); + + buttonKeyListOther.setVisibility(View.INVISIBLE); + buttonKeyListCancel.setVisibility(View.INVISIBLE); + keyChoiceAdapter.setActiveItem(position); + } + + @Override + public void showImportInternalError() { + Toast.makeText(getContext(), R.string.error_save_key_internal, Toast.LENGTH_LONG).show(); + } + + @Override + public void launchImportOperation(ImportKeyringParcel importKeyringParcel) { + RemoteSelectIdKeyActivity activity = (RemoteSelectIdKeyActivity) getActivity(); + if (activity == null) { + return; + } + activity.launchImportOperation(importKeyringParcel); + } + + @Override + public void displayOverflowMenu() { + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(getActivity()); + PopupMenu menu = new PopupMenu(theme, buttonOverflow); + menu.inflate(R.menu.select_identity_menu); + menu.setOnMenuItemClickListener(item -> { + presenter.onClickMenuListAllKeys(); + return false; + }); + menu.show(); + } + + @Override + public void showOpenKeychainIntent() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + Intent intent = new Intent(activity.getApplicationContext(), MainActivity.class); + startActivity(intent); + } + }; + } + + private void setupListenersForPresenter() { + buttonOverflow.setOnClickListener(view -> presenter.onClickOverflowMenu()); + + buttonKeyListOther.setOnClickListener(view -> presenter.onClickKeyListOther()); + buttonKeyListCancel.setOnClickListener(view -> presenter.onClickKeyListCancel()); + + buttonNoKeysNew.setOnClickListener(view -> presenter.onClickNoKeysGenerate()); + buttonNoKeysExisting.setOnClickListener(view -> presenter.onClickNoKeysExisting()); + buttonNoKeysCancel.setOnClickListener(view -> presenter.onClickNoKeysCancel()); + + buttonExplBack.setOnClickListener(view -> presenter.onClickExplanationBack()); + buttonExplGotIt.setOnClickListener(view -> presenter.onClickExplanationGotIt()); + + buttonGenOkBack.setOnClickListener(view -> presenter.onClickGenerateOkBack()); + buttonGenOkFinish.setOnClickListener(view -> presenter.onClickGenerateOkFinish()); + + buttonGotoOpenKeychain.setOnClickListener(view -> presenter.onClickGoToOpenKeychain()); + + keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), + (view, position) -> presenter.onKeyItemClick(position))); + } + } + + private void launchImportOperation(ImportKeyringParcel importKeyringParcel) { + if (currentlyImportingParcel != null) { + Timber.e("Starting import while already running? Inconsistent state!"); + return; + } + + currentlyImportingParcel = importKeyringParcel; + importOpHelper.cryptoOperation(); + } + + 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.api_select_identity_item, parent, false); + return new KeyChoiceViewHolder(keyChoiceItemView); + } + + void setActiveItem(Integer activeItem) { + this.activeItem = activeItem; + notifyDataSetChanged(); + } + + @Override + public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { + KeyInfo keyInfo = data.get(position); + boolean hasActiveItem = activeItem != null; + boolean isActiveItem = hasActiveItem && position == activeItem; + + Drawable icon = isActiveItem ? iconSelected : iconUnselected; + holder.bind(keyInfo, icon); + + holder.itemView.setVisibility(!hasActiveItem || isActiveItem ? View.VISIBLE : View.INVISIBLE); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + void setSelectionDrawables(Drawable iconSelected, Drawable iconUnselected) { + this.iconSelected = iconSelected; + this.iconUnselected = iconUnselected; + + notifyDataSetChanged(); + } + } + + private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + private final ImageView vIcon; + + KeyChoiceViewHolder(View itemView) { + super(itemView); + + vName = itemView.findViewById(R.id.key_list_item_name); + vIcon = itemView.findViewById(R.id.key_list_item_icon); + } + + void bind(KeyInfo keyInfo, Drawable selectionIcon) { + Context context = vCreation.getContext(); + + String email = keyInfo.getEmail(); + String name = keyInfo.getName(); + if (email != null) { + vName.setText(context.getString(R.string.use_key, email)); + } else if (name != null) { + vName.setText(context.getString(R.string.use_key, name)); + } else { + vName.setText(context.getString(R.string.use_key_no_name)); + } + + 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); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (importOpHelper.handleActivityResult(requestCode, resultCode, data)) { + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + + private CryptoOperationHelper importOpHelper = + new CryptoOperationHelper<>(0, this, + new AbstractCallback() { + @Override + public Parcelable createOperationInput() { + return currentlyImportingParcel; + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + currentlyImportingParcel = null; + presenter.onImportOpError(); + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + currentlyImportingParcel = null; + presenter.onImportOpSuccess(result); + } + }, null); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java new file mode 100644 index 000000000..d287129fb --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java @@ -0,0 +1,39 @@ +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import android.arch.lifecycle.ViewModel; +import android.content.Context; + +import org.sufficientlysecure.keychain.livedata.KeyInfoLiveData; +import org.sufficientlysecure.keychain.livedata.PgpKeyGenerationLiveData; + + +public class RemoteSelectIdViewModel extends ViewModel { + + private KeyInfoLiveData keyInfo; + private PgpKeyGenerationLiveData keyGenerationData; + private boolean listAllKeys; + + public KeyInfoLiveData getKeyInfo(Context context) { + if (keyInfo == null) { + keyInfo = new KeyInfoLiveData(context, context.getContentResolver()); + } + return keyInfo; + } + + public PgpKeyGenerationLiveData getKeyGenerationLiveData(Context context) { + if (keyGenerationData == null) { + keyGenerationData = new PgpKeyGenerationLiveData(context); + } + return keyGenerationData; + } + + public boolean isListAllKeys() { + return listAllKeys; + } + + public void setListAllKeys(boolean listAllKeys) { + this.listAllKeys = listAllKeys; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java new file mode 100644 index 000000000..6cd4bdef6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * 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.io.IOException; +import java.util.List; + +import android.arch.lifecycle.LifecycleOwner; +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.net.Uri; + +import org.openintents.openpgp.util.OpenPgpUtils; +import org.openintents.openpgp.util.OpenPgpUtils.UserId; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import timber.log.Timber; + + +class RemoteSelectIdentityKeyPresenter { + private final PackageManager packageManager; + private final Context context; + private final RemoteSelectIdViewModel viewModel; + + + private RemoteSelectIdentityKeyView view; + private List keyInfoData; + + private UserId userId; + private long selectedMasterKeyId; + private byte[] generatedKeyData; + private ApiDataAccessObject apiDao; + private AppSettings appSettings; + + + RemoteSelectIdentityKeyPresenter(Context context, RemoteSelectIdViewModel viewModel, LifecycleOwner lifecycleOwner) { + this.context = context; + this.viewModel = viewModel; + this.apiDao = new ApiDataAccessObject(context); + + packageManager = context.getPackageManager(); + + viewModel.getKeyGenerationLiveData(context).observe(lifecycleOwner, this::onChangeKeyGeneration); + viewModel.getKeyInfo(context).observe(lifecycleOwner, this::onChangeKeyInfoData); + } + + public void setView(RemoteSelectIdentityKeyView view) { + this.view = view; + } + + void setupFromIntentData(String packageName, byte[] packageSignature, String rawUserId, boolean clientHasAutocryptSetupMsg) { + try { + setPackageInfo(packageName, packageSignature); + } catch (NameNotFoundException e) { + Timber.e(e, "Unable to find info of calling app!"); + view.finishAsCancelled(); + return; + } + + this.userId = OpenPgpUtils.splitUserId(rawUserId); + view.setAddressText(userId.email); + view.setShowAutocryptHint(clientHasAutocryptSetupMsg); + + loadKeyInfo(); + } + + private void loadKeyInfo() { + Uri listedKeyRingUri = viewModel.isListAllKeys() ? + KeyRings.buildUnifiedKeyRingsUri() : KeyRings.buildUnifiedKeyRingsFindByUserIdUri(userId.email); + viewModel.getKeyInfo(context).setKeySelector(KeySelector.createOnlySecret(listedKeyRingUri, null)); + } + + private void setPackageInfo(String packageName, byte[] packageSignature) throws NameNotFoundException { + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); + Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); + CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo); + + appSettings = new AppSettings(packageName, packageSignature); + + view.setTitleClientIconAndName(appIcon, appLabel); + } + + private void onChangeKeyInfoData(List data) { + keyInfoData = data; + goToSelectLayout(); + } + + private void goToSelectLayout() { + if (keyInfoData == null) { + view.showLayoutEmpty(); + } else if (keyInfoData.isEmpty()) { + view.showLayoutSelectNoKeys(); + } else { + view.setKeyListData(keyInfoData); + view.showLayoutSelectKeyList(); + } + } + + private void onChangeKeyGeneration(PgpEditKeyResult pgpEditKeyResult) { + if (pgpEditKeyResult == null) { + return; + } + + try { + UncachedKeyRing generatedRing = pgpEditKeyResult.getRing(); + this.generatedKeyData = generatedRing.getEncoded(); + } catch (IOException e) { + throw new AssertionError("Newly generated key ring must be encodable!"); + } + + viewModel.getKeyGenerationLiveData(context).setSaveKeyringParcel(null); + view.showLayoutGenerateOk(); + } + + void onDialogCancel() { + view.finishAsCancelled(); + } + + void onClickKeyListOther() { + view.showLayoutImportExplanation(); + } + + void onClickKeyListCancel() { + view.finishAndReturn(Constants.key.none); + } + + void onClickNoKeysGenerate() { + view.showLayoutGenerateProgress(); + + SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel(); + Constants.addDefaultSubkeys(builder); + builder.addUserId(userId.email); + + viewModel.getKeyGenerationLiveData(context).setSaveKeyringParcel(builder.build()); + } + + void onClickNoKeysExisting() { + view.showLayoutImportExplanation(); + } + + void onClickNoKeysCancel() { + view.finishAndReturn(Constants.key.none); + } + + void onKeyItemClick(int position) { + selectedMasterKeyId = keyInfoData.get(position).getMasterKeyId(); + view.highlightKey(position); + } + + void onClickExplanationBack() { + goToSelectLayout(); + } + + void onClickExplanationGotIt() { + view.finishAsCancelled(); + } + + void onClickGenerateOkBack() { + view.showLayoutSelectNoKeys(); + } + + void onClickGenerateOkFinish() { + if (generatedKeyData == null) { + return; + } + + ImportKeyringParcel importKeyringParcel = ImportKeyringParcel.createFromBytes(generatedKeyData); + generatedKeyData = null; + + view.launchImportOperation(importKeyringParcel); + view.showLayoutGenerateSave(); + } + + void onHighlightFinished() { + apiDao.insertApiApp(appSettings); + apiDao.addAllowedKeyIdForApp(appSettings.getPackageName(), selectedMasterKeyId); + view.finishAndReturn(selectedMasterKeyId); + } + + void onImportOpSuccess(ImportKeyResult result) { + long importedMasterKeyId = result.getImportedMasterKeyIds()[0]; + apiDao.insertApiApp(appSettings); + apiDao.addAllowedKeyIdForApp(appSettings.getPackageName(), selectedMasterKeyId); + view.finishAndReturn(importedMasterKeyId); + } + + void onImportOpError() { + view.showImportInternalError(); + } + + public void onClickOverflowMenu() { + view.displayOverflowMenu(); + } + + public void onClickMenuListAllKeys() { + viewModel.setListAllKeys(!viewModel.isListAllKeys()); + loadKeyInfo(); + view.showLayoutSelectKeyList(); + } + + public void onClickGoToOpenKeychain() { + view.showOpenKeychainIntent(); + } + + interface RemoteSelectIdentityKeyView { + void finishAndReturn(long masterKeyId); + void finishAsCancelled(); + + void setAddressText(String text); + void setTitleClientIconAndName(Drawable drawable, CharSequence name); + void setShowAutocryptHint(boolean showAutocryptHint); + + void showLayoutEmpty(); + void showLayoutSelectNoKeys(); + void showLayoutSelectKeyList(); + void showLayoutImportExplanation(); + void showLayoutGenerateProgress(); + void showLayoutGenerateOk(); + void showLayoutGenerateSave(); + + void setKeyListData(List data); + + void highlightKey(int position); + + void launchImportOperation(ImportKeyringParcel importKeyringParcel); + + void showImportInternalError(); + + void displayOverflowMenu(); + + void showOpenKeychainIntent(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java index 13f31474c..582d19bb7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java @@ -50,6 +50,11 @@ public abstract class ImportKeyringParcel implements Parcelable { return new AutoValue_ImportKeyringParcel(Collections.singletonList(key), null, false); } + public static ImportKeyringParcel createFromBytes(byte[] keyData) { + ParcelableKeyRing keyRing = ParcelableKeyRing.createFromEncodedBytes(keyData); + return new AutoValue_ImportKeyringParcel(Collections.singletonList(keyRing), null, false); + } + public static ImportKeyringParcel createFromFileCacheWithSkipSave() { return new AutoValue_ImportKeyringParcel(null, null, true); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java index 9cec9d778..171db0849 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui.base; import android.content.Context; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.LayoutRes; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.Gravity; @@ -136,8 +137,6 @@ public class RecyclerFragment extends Fragment { RecyclerView listView = new RecyclerView(context); listView.setId(INTERNAL_LIST_VIEW_ID); - int padding = FormattingUtils.dpToPx(context, 8); - listView.setPadding(padding, 0, padding, 0); listView.setClipToPadding(false); listContainer.addView(listView, new FrameLayout.LayoutParams( diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java new file mode 100644 index 000000000..13631e7ff --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java @@ -0,0 +1,112 @@ +package org.sufficientlysecure.keychain.ui.keyview.loader; + + +import android.arch.lifecycle.LiveData; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.os.CancellationSignal; +import android.support.v4.os.OperationCanceledException; + +public abstract class AsyncTaskLiveData extends LiveData { + @NonNull + private final Context context; + private Uri observedUri; + + @NonNull + private final ForceLoadContentObserver observer; + + @Nullable + private CancellationSignal cancellationSignal; + + protected AsyncTaskLiveData(@NonNull Context context, @Nullable Uri observedUri) { + super(); + this.context = context; + this.observedUri = observedUri; + this.observer = new ForceLoadContentObserver(); + } + + protected abstract T asyncLoadData(); + + protected void updateDataInBackground() { + new AsyncTask() { + @Override + protected T doInBackground(Void... params) { + try { + synchronized (AsyncTaskLiveData.this) { + cancellationSignal = new CancellationSignal(); + } + try { + return asyncLoadData(); + } finally { + synchronized (AsyncTaskLiveData.this) { + cancellationSignal = null; + } + } + } catch (OperationCanceledException e) { + if (hasActiveObservers()) { + throw e; + } + return null; + } + } + + @Override + protected void onPostExecute(T value) { + setValue(value); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + protected void onActive() { + T value = getValue(); + if (value == null) { + updateDataInBackground(); + } + + if (observedUri != null) { + getContext().getContentResolver().registerContentObserver(observedUri, true, observer); + } + } + + @Override + protected void onInactive() { + synchronized (AsyncTaskLiveData.this) { + if (cancellationSignal != null) { + cancellationSignal.cancel(); + } + } + + if (observedUri != null) { + getContext().getContentResolver().registerContentObserver(observedUri, true, observer); + } + } + + @NonNull + public Context getContext() { + return context; + } + + public final class ForceLoadContentObserver extends ContentObserver { + + ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + updateDataInBackground(); + } + } + +} \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/api_select_identity_item.xml b/OpenKeychain/src/main/res/layout/api_select_identity_item.xml new file mode 100644 index 000000000..78001af3c --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_select_identity_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/api_select_identity_key.xml b/OpenKeychain/src/main/res/layout/api_select_identity_key.xml new file mode 100644 index 000000000..489a7d5f4 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_select_identity_key.xml @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +