From 6ee47dea31f92e1c6219b988eb166ef60b75434b Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 22 Jan 2018 02:04:03 +0100 Subject: [PATCH 01/17] add architecture lifecycle component --- OpenKeychain/build.gradle | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 65147a0a9..3fda8ab03 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', ] } From c9b1690b76754264a9663c99bfc864444a1a7603 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sun, 25 Mar 2018 15:09:36 +0200 Subject: [PATCH 02/17] add AsyncTaskLiveData class --- .../ui/keyview/loader/AsyncTaskLiveData.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java 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..9cfda21fb --- /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; + + AsyncTaskLiveData(@NonNull Context context, @Nullable Uri observedUri) { + super(); + this.context = context; + this.observedUri = observedUri; + this.observer = new ForceLoadContentObserver(); + } + + abstract T asyncLoadData(); + + private void loadDataInBackground() { + 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) { + loadDataInBackground(); + } + + 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) { + loadDataInBackground(); + } + } + +} \ No newline at end of file From 4c92426fe5ecfd97170e8c27aaf07063a94ab152 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Wed, 28 Mar 2018 21:11:33 +0200 Subject: [PATCH 03/17] Use more subtle animation for translucent theme --- OpenKeychain/src/main/res/values-v23/themes.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/OpenKeychain/src/main/res/values-v23/themes.xml b/OpenKeychain/src/main/res/values-v23/themes.xml index 29bd762c0..995837e3f 100644 --- a/OpenKeychain/src/main/res/values-v23/themes.xml +++ b/OpenKeychain/src/main/res/values-v23/themes.xml @@ -2,6 +2,13 @@ - From 69121bfa98967927b6a63bf0391f8629e6ad7840 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Wed, 28 Mar 2018 21:16:03 +0200 Subject: [PATCH 04/17] extract KeyInfoInteractor from KeyLoader --- .../KeyInfoInteractor.java} | 64 +++++----------- .../remote/ui/dialog/KeyInfoLoader.java | 76 +++++++++++++++++++ .../ui/dialog/RemoteDeduplicateActivity.java | 2 +- .../ui/dialog/RemoteDeduplicatePresenter.java | 6 +- ...RemoteSelectAuthenticationKeyActivity.java | 2 +- ...emoteSelectAuthenticationKeyPresenter.java | 10 +-- 6 files changed, 105 insertions(+), 55 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/{remote/ui/dialog/KeyLoader.java => livedata/KeyInfoInteractor.java} (74%) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoInteractor.java similarity index 74% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoInteractor.java index ac30275a7..173e58ac5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoInteractor.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -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/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 Date: Wed, 28 Mar 2018 21:17:32 +0200 Subject: [PATCH 05/17] Use only two subkeys in default configuration --- .../java/org/sufficientlysecure/keychain/Constants.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 000ef4495..fa3ac244d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -187,13 +187,11 @@ public final class Constants { } /** - * Default key configuration: 3072 bit RSA (certify, sign, encrypt) + * Default key configuration: 3072 bit RSA (certify + sign, encrypt) */ public static void addDefaultSubkeys(SaveKeyringParcel.Builder builder) { builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(SaveKeyringParcel.Algorithm.RSA, - 3072, null, KeyFlags.CERTIFY_OTHER, 0L)); - builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(SaveKeyringParcel.Algorithm.RSA, - 3072, null, KeyFlags.SIGN_DATA, 0L)); + 3072, null, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, 0L)); builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(SaveKeyringParcel.Algorithm.RSA, 3072, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L)); } From b92ff869885e4e223156e54765190f26eb61b1c4 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 26 Jan 2017 20:23:58 +0100 Subject: [PATCH 06/17] Redesign "select signing key" api dialog --- OpenKeychain/src/main/AndroidManifest.xml | 5 + .../keychain/livedata/KeyInfoLiveData.java | 38 ++ .../livedata/PgpKeyGenerationLiveData.java | 38 ++ .../remote/ApiPendingIntentFactory.java | 20 +- .../keychain/remote/ApiPermissionHelper.java | 2 +- .../keychain/remote/OpenPgpService.java | 22 +- .../ui/SelectIdentityKeyListFragment.java | 167 +++++++ .../ui/adapter/SelectIdentityKeyAdapter.java | 152 ++++++ .../ui/dialog/RemoteSelectIdKeyActivity.java | 403 ++++++++++++++++ .../ui/dialog/RemoteSelectIdViewModel.java | 31 ++ .../RemoteSelectIdentityKeyPresenter.java | 191 ++++++++ .../keychain/ui/base/RecyclerFragment.java | 3 +- .../ui/keyview/loader/AsyncTaskLiveData.java | 10 +- .../res/layout/api_select_identity_item.xml | 47 ++ .../res/layout/api_select_identity_key.xml | 431 ++++++++++++++++++ .../res/layout/select_encrypt_key_item.xml | 6 +- .../res/layout/select_identity_key_item.xml | 44 ++ .../main/res/menu/select_identity_menu.xml | 8 + OpenKeychain/src/main/res/values/strings.xml | 7 + OpenKeychain/src/main/res/values/themes.xml | 7 +- 20 files changed, 1609 insertions(+), 23 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java create mode 100644 OpenKeychain/src/main/res/layout/api_select_identity_item.xml create mode 100644 OpenKeychain/src/main/res/layout/api_select_identity_key.xml create mode 100644 OpenKeychain/src/main/res/layout/select_identity_key_item.xml create mode 100644 OpenKeychain/src/main/res/menu/select_identity_menu.xml diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 8d76101ae..17f364572 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -873,6 +873,11 @@ android:name=".remote.ui.RemoteSelectPubKeyActivity" android:exported="false" android:label="@string/app_name" /> + > { + 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/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java index 9634bd689..f2b6e6a94 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; @@ -89,6 +90,16 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } + PendingIntent createSelectIdentityKeyPendingIntent( + String packageName, String apiIdentity, Long currentMasterKeyId) { + Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, apiIdentity); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_CURRENT_MASTER_KEY_ID, currentMasterKeyId); + + return createInternal(null, intent); + } + PendingIntent createSelectPublicKeyPendingIntent(Intent data, long[] keyIdsArray, ArrayList missingEmails, ArrayList duplicateEmails, boolean noUserIdsCheck) { Intent intent = new Intent(mContext, RemoteSelectPubKeyActivity.class); @@ -134,9 +145,10 @@ public class ApiPendingIntentFactory { } PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName, String preferredUserId) { - Intent intent = new Intent(mContext, SelectSignKeyIdActivity.class); + Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class); intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); - intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, preferredUserId); return createInternal(data, intent); } @@ -182,7 +194,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..445886892 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) { 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 1fcba550f..3a09a54cf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -196,8 +196,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 +238,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); @@ -936,12 +946,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); 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/RemoteSelectIdKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java new file mode 100644 index 000000000..71ba94cec --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java @@ -0,0 +1,403 @@ +/* + * 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.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.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.TextView; + +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.remote.ui.dialog.RemoteSelectIdentityKeyPresenter.RemoteSelectIdentityKeyView; +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; + + +public class RemoteSelectIdKeyActivity extends FragmentActivity { + public static final String EXTRA_PACKAGE_NAME = "package_name"; + public static final String EXTRA_USER_ID = "user_id"; + public static final String EXTRA_CURRENT_MASTER_KEY_ID = "current_master_key_id"; + + + private RemoteSelectIdentityKeyPresenter presenter; + + + @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); + + presenter.setupFromIntentData(packageName, userId); + } + + 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; + + @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); + + 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); + + 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 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.EXTRA_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)); + iconClientApp.setImageDrawable(drawable); + setSelectionIcons(drawable); + } + + 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); + } + + @Override + public void showLayoutSelectKeyList() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_key_list); + } + + @Override + public void showLayoutImportExplanation() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_import_expl); + } + + @Override + public void showLayoutGenerateProgress() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_progress); + } + + @Override + public void showLayoutGenerateOk() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_ok); + } + + @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); + } + }; + } + + private void setupListenersForPresenter() { + 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()); + + keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), + (view, 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.api_select_identity_item, parent, false); + return new KeyChoiceViewHolder(keyChoiceItemView); + } + + public void setActiveItem(Integer activeItem) { + if (this.activeItem != null) { + notifyItemChanged(activeItem); + } + this.activeItem = activeItem; + if (this.activeItem != null) { + notifyItemChanged(activeItem); + } + } + + @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 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 name = keyInfo.getName(); + if (name != null) { + vName.setText(context.getString(R.string.use_key, name)); + } else { + String email = keyInfo.getEmail(); + vName.setText(context.getString(R.string.use_key, email)); + } + + 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/RemoteSelectIdViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java new file mode 100644 index 000000000..e2325d4ae --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java @@ -0,0 +1,31 @@ +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; + public long selectedMasterKeyId; + + 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; + } + +} 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..716d3e354 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java @@ -0,0 +1,191 @@ +/* + * 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.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 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.PgpEditKeyResult; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +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 UserId userId; + + private RemoteSelectIdentityKeyView view; + private List keyInfoData; + private long masterKeyId; + + + RemoteSelectIdentityKeyPresenter(Context context, RemoteSelectIdViewModel viewModel, LifecycleOwner lifecycleOwner) { + this.context = context; + this.viewModel = viewModel; + + 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, String rawUserId) { + try { + setPackageInfo(packageName); + } 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); + + viewModel.getKeyInfo(context).setKeySelector(KeySelector.createOnlySecret( + KeyRings.buildUnifiedKeyRingsFindByUserIdUri(userId.email), null)); + } + + private void setPackageInfo(String packageName) throws NameNotFoundException { + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); + Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); + CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo); + + 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) { + viewModel.getKeyGenerationLiveData(context).setSaveKeyringParcel(null); + if (pgpEditKeyResult == null) { + return; + } + + 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) { + viewModel.selectedMasterKeyId = keyInfoData.get(position).getMasterKeyId(); + view.highlightKey(position); + } + + void onClickExplanationBack() { + goToSelectLayout(); + } + + void onClickExplanationGotIt() { + view.finishAsCancelled(); + } + + void onClickGenerateOkBack() { + view.showLayoutSelectNoKeys(); + } + + void onClickGenerateOkFinish() { + // saveKey + // view.finishAndReturn + } + + void onHighlightFinished() { + view.finishAndReturn(viewModel.selectedMasterKeyId); + } + + interface RemoteSelectIdentityKeyView { + void finishAndReturn(long masterKeyId); + void finishAsCancelled(); + + void setAddressText(String text); + void setTitleClientIconAndName(Drawable drawable, CharSequence name); + + void showLayoutEmpty(); + void showLayoutSelectNoKeys(); + void showLayoutSelectKeyList(); + void showLayoutImportExplanation(); + void showLayoutGenerateProgress(); + void showLayoutGenerateOk(); + + void setKeyListData(List data); + + void highlightKey(int position); + } +} 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 index 9cfda21fb..13631e7ff 100644 --- 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 @@ -23,16 +23,16 @@ public abstract class AsyncTaskLiveData extends LiveData { @Nullable private CancellationSignal cancellationSignal; - AsyncTaskLiveData(@NonNull Context context, @Nullable Uri observedUri) { + protected AsyncTaskLiveData(@NonNull Context context, @Nullable Uri observedUri) { super(); this.context = context; this.observedUri = observedUri; this.observer = new ForceLoadContentObserver(); } - abstract T asyncLoadData(); + protected abstract T asyncLoadData(); - private void loadDataInBackground() { + protected void updateDataInBackground() { new AsyncTask() { @Override protected T doInBackground(Void... params) { @@ -66,7 +66,7 @@ public abstract class AsyncTaskLiveData extends LiveData { protected void onActive() { T value = getValue(); if (value == null) { - loadDataInBackground(); + updateDataInBackground(); } if (observedUri != null) { @@ -105,7 +105,7 @@ public abstract class AsyncTaskLiveData extends LiveData { @Override public void onChange(boolean selfChange) { - loadDataInBackground(); + updateDataInBackground(); } } 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..7dfcac1b6 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_select_identity_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + 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..73d6e2fc7 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_select_identity_key.xml @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +