From fed0fff9d7d805fc000f8ba46d6beb0f5039ed69 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 24 Apr 2017 20:49:45 +0200 Subject: [PATCH] add secret key status (wip) --- .../keychain/pgp/CanonicalizedSecretKey.java | 23 ++ .../keychain/ui/ViewKeyActivity.java | 58 ++-- .../keychain/ui/ViewKeyFragment.java | 171 ++++-------- .../keychain/ui/widget/KeyHealthCardView.java | 188 +++++++++++++ .../ui/widget/KeyHealthPresenter.java | 264 ++++++++++++++++++ .../keychain/ui/widget/KeyStatusList.java | 148 ++++++++++ .../ui/widget/SubkeyStatusLoader.java | 173 ++++++++++++ .../res/layout/key_health_card_content.xml | 96 +++++++ .../main/res/layout/log_display_activity.xml | 1 - .../res/layout/subkey_status_card_content.xml | 172 ++++++++++++ .../src/main/res/layout/tools_vertlin.xml | 9 + .../src/main/res/layout/view_key_fragment.xml | 13 + OpenKeychain/src/main/res/values/strings.xml | 48 ++++ 13 files changed, 1236 insertions(+), 128 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java create mode 100644 OpenKeychain/src/main/res/layout/key_health_card_content.xml create mode 100644 OpenKeychain/src/main/res/layout/subkey_status_card_content.xml create mode 100644 OpenKeychain/src/main/res/layout/tools_vertlin.xml diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java index 921aed262..75b612066 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java @@ -121,6 +121,29 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { return this != UNAVAILABLE && this != GNU_DUMMY; } + /** Compares by "usability", which basically compares how independently usable + * two SecretKeyTypes are. The order is roughly this: + * + * empty passphrase < passphrase/others < divert < stripped + * + */ + public int compareUsability(SecretKeyType other) { + // if one is usable but the other isn't, the usable one comes first + if (isUsable() ^ other.isUsable()) { + return isUsable() ? -1 : 1; + } + // if one is a divert-to-card but the other isn't, the non-divert one comes first + if ((this == DIVERT_TO_CARD) ^ (other == DIVERT_TO_CARD)) { + return this != DIVERT_TO_CARD ? -1 : 1; + } + // if one requires a passphrase but another doesn't, the one without a passphrase comes first + if ((this == PASSPHRASE_EMPTY) ^ (other == PASSPHRASE_EMPTY)) { + return this == PASSPHRASE_EMPTY ? -1 : 1; + } + // all other (current) cases are equal + return 0; + } + } /** This method returns the SecretKeyType for this secret key, testing for an empty diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 536900ba1..361c2dd24 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -111,6 +111,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements public static final String EXTRA_SECURITY_TOKEN_AID = "security_token_aid"; public static final String EXTRA_SECURITY_TOKEN_VERSION = "security_token_version"; public static final String EXTRA_SECURITY_TOKEN_FINGERPRINTS = "security_token_fingerprints"; + private boolean mLinkedTransition; @Retention(RetentionPolicy.SOURCE) @IntDef({REQUEST_QR_FINGERPRINT, REQUEST_BACKUP, REQUEST_CERTIFY, REQUEST_DELETE}) @@ -331,20 +332,13 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements return; } - boolean linkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false); - if (linkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mLinkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false); + if (mLinkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { postponeEnterTransition(); } - FragmentManager manager = getSupportFragmentManager(); - // Create an instance of the fragment - final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri, - linkedTransition ? PostponeType.LINKED : PostponeType.NONE); - manager.beginTransaction() - .replace(R.id.view_key_fragment, frag) - .commit(); - if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) { + FragmentManager manager = getSupportFragmentManager(); final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(mDataUri); manager.beginTransaction() .replace(R.id.view_key_keybase_fragment, keybaseFrag) @@ -512,7 +506,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements private void certifyImmediate() { Intent intent = new Intent(this, CertifyKeyActivity.class); - intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId}); + intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { mMasterKeyId }); startActivityForResult(intent, REQUEST_CERTIFY); } @@ -734,6 +728,32 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements } + public void showMainFragment() { + new Handler().post(new Runnable() { + @Override + public void run() { + FragmentManager manager = getSupportFragmentManager(); + + // unless we must refresh + ViewKeyFragment frag = (ViewKeyFragment) manager.findFragmentByTag("view_key_fragment"); + // if everything is valid, just drop it + if (frag != null && frag.isValidForData(mIsSecret)) { + return; + } + + // if the main fragment doesn't exist, or is not of the correct type, (re)create it + frag = ViewKeyFragment.newInstance(mMasterKeyId, mIsSecret, + mLinkedTransition ? PostponeType.LINKED : PostponeType.NONE); + // get rid of possible backstack, this fragment is always at the bottom + manager.popBackStack("security_token", FragmentManager.POP_BACK_STACK_INCLUSIVE); + manager.beginTransaction() + .replace(R.id.view_key_fragment, frag, "view_key_fragment") + // if this gets lost, it doesn't really matter since the loader will reinstate it onResume + .commitAllowingStateLoss(); + } + }); + } + private void encrypt(Uri dataUri, boolean text) { // If there is no encryption key, don't bother. if (!mHasEncrypt) { @@ -900,6 +920,15 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); mFingerprint = data.getBlob(INDEX_FINGERPRINT); mFingerprintString = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); + mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; + mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0; + mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0; + mIsSecure = data.getInt(INDEX_IS_SECURE) == 1; + mIsVerified = data.getInt(INDEX_VERIFIED) > 0; + + // queue showing of the main fragment + showMainFragment(); // if it wasn't shown yet, display token fragment if (mShowSecurityTokenAfterCreation && getIntent().hasExtra(EXTRA_SECURITY_TOKEN_AID)) { @@ -912,13 +941,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements showSecurityTokenFragment(tokenFingerprints, tokenUserId, tokenAid, tokenVersion); } - mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; - mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0; - mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0; - mIsSecure = data.getInt(INDEX_IS_SECURE) == 1; - mIsVerified = data.getInt(INDEX_VERIFIED) > 0; - // if the refresh animation isn't playing if (!mRotate.hasStarted() && !mRotateSpin.hasStarted()) { // re-create options menu based on mIsSecret, mIsVerified diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java index 81a3a71e5..f3662c1e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui; + import java.io.IOException; import java.util.List; @@ -59,6 +60,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.base.LoaderFragment; @@ -66,14 +68,16 @@ import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment; import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.OnIdentityLoadedListener; import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthCardView; import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.Preferences; public class ViewKeyFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks { - public static final String ARG_DATA_URI = "uri"; + public static final String ARG_MASTER_KEY_ID = "master_key_id"; + public static final String ARG_IS_SECRET = "is_secret"; public static final String ARG_POSTPONE_TYPE = "postpone_type"; private ListView mUserIds; @@ -84,20 +88,16 @@ public class ViewKeyFragment extends LoaderFragment implements boolean mIsSecret = false; - private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_USER_IDS = 1; private static final int LOADER_ID_LINKED_IDS = 2; private static final int LOADER_ID_LINKED_CONTACT = 3; - - private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID - = "loader_linked_contact_master_key_id"; - private static final String LOADER_EXTRA_LINKED_CONTACT_IS_SECRET - = "loader_linked_contact_is_secret"; + private static final int LOADER_ID_SUBKEY_STATUS = 4; private UserIdsAdapter mUserIdsAdapter; private LinkedIdsAdapter mLinkedIdsAdapter; private Uri mDataUri; + private PostponeType mPostponeType; private CardView mSystemContactCard; @@ -111,13 +111,19 @@ public class ViewKeyFragment extends LoaderFragment implements private byte[] mFingerprint; private TextView mLinkedIdsExpander; + KeyHealthCardView mKeyHealthCard; + KeyHealthPresenter mKeyHealthPresenter; + + private long mMasterKeyId; + /** * Creates new instance of this fragment */ - public static ViewKeyFragment newInstance(Uri dataUri, PostponeType postponeType) { + public static ViewKeyFragment newInstance(long masterKeyId, boolean isSecret, PostponeType postponeType) { ViewKeyFragment frag = new ViewKeyFragment(); Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); + args.putLong(ARG_MASTER_KEY_ID, masterKeyId); + args.putBoolean(ARG_IS_SECRET, isSecret); args.putString(ARG_POSTPONE_TYPE, postponeType.toString()); frag.setArguments(args); @@ -169,6 +175,8 @@ public class ViewKeyFragment extends LoaderFragment implements } }); + mKeyHealthCard = (KeyHealthCardView) view.findViewById(R.id.subkey_status_card); + return root; } @@ -223,7 +231,29 @@ public class ViewKeyFragment extends LoaderFragment implements }); } }); + } + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mMasterKeyId = getArguments().getLong(ARG_MASTER_KEY_ID); + mDataUri = KeyRings.buildGenericKeyRingUri(mMasterKeyId); + mIsSecret = getArguments().getBoolean(ARG_IS_SECRET); + mPostponeType = PostponeType.valueOf(getArguments().getString(ARG_POSTPONE_TYPE)); + + // load user ids after we know if it's a secret key + mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null); + mUserIds.setAdapter(mUserIdsAdapter); + + // initialize loaders, which will take care of auto-refresh on change + getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + initLinkedContactLoader(); + initCardButtonsVisibility(mIsSecret); + + mKeyHealthPresenter = new KeyHealthPresenter( + getContext(), mKeyHealthCard, LOADER_ID_SUBKEY_STATUS, mMasterKeyId, mIsSecret); + mKeyHealthPresenter.startLoader(getLoaderManager()); } private void showUserIdInfo(final int position) { @@ -249,7 +279,9 @@ public class ViewKeyFragment extends LoaderFragment implements */ private void loadLinkedSystemContact(final long contactId) { // contact doesn't exist, stop - if (contactId == -1) return; + if (contactId == -1) { + return; + } final Context context = mSystemContactName.getContext(); ContactHelper contactHelper = new ContactHelper(context); @@ -265,7 +297,7 @@ public class ViewKeyFragment extends LoaderFragment implements contactName = contactHelper.getContactName(contactId); } - if (contactName != null) {//contact name exists for given master key + if (contactName != null) { //contact name exists for given master key showLinkedSystemContact(); mSystemContactName.setText(contactName); @@ -311,21 +343,6 @@ public class ViewKeyFragment extends LoaderFragment implements context.startActivity(intent); } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - mPostponeType = PostponeType.valueOf(getArguments().getString(ARG_POSTPONE_TYPE)); - if (dataUri == null) { - Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); - getActivity().finish(); - return; - } - - loadData(dataUri); - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { // if a result has been returned, display a notify @@ -337,58 +354,16 @@ public class ViewKeyFragment extends LoaderFragment implements } } - static final String[] UNIFIED_PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - KeychainContract.KeyRings.HAS_ENCRYPT - }; - - static final int INDEX_MASTER_KEY_ID = 1; - @SuppressWarnings("unused") - static final int INDEX_USER_ID = 2; - @SuppressWarnings("unused") - static final int INDEX_IS_REVOKED = 3; - @SuppressWarnings("unused") - static final int INDEX_IS_EXPIRED = 4; - @SuppressWarnings("unused") - static final int INDEX_VERIFIED = 5; - static final int INDEX_HAS_ANY_SECRET = 6; - static final int INDEX_FINGERPRINT = 7; - @SuppressWarnings("unused") - static final int INDEX_HAS_ENCRYPT = 8; - private static final String[] RAW_CONTACT_PROJECTION = { ContactsContract.RawContacts.CONTACT_ID }; private static final int INDEX_CONTACT_ID = 0; - private void loadData(Uri dataUri) { - mDataUri = dataUri; - - Log.i(Constants.TAG, "mDataUri: " + mDataUri); - - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - } - @Override public Loader onCreateLoader(int id, Bundle args) { switch (id) { - case LOADER_ID_UNIFIED: { - setContentShown(false, false); - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); - } - case LOADER_ID_USER_IDS: { return UserIdsAdapter.createLoader(getActivity(), mDataUri); } @@ -401,11 +376,7 @@ public class ViewKeyFragment extends LoaderFragment implements // we need a separate loader for linked contact // to ensure refreshing on verification - // passed in args to explicitly specify their need - long masterKeyId = args.getLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID); - boolean isSecret = args.getBoolean(LOADER_EXTRA_LINKED_CONTACT_IS_SECRET); - - Uri baseUri = isSecret ? ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI : + Uri baseUri = mIsSecret ? ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI : ContactsContract.RawContacts.CONTENT_URI; return new CursorLoader( @@ -417,12 +388,16 @@ public class ViewKeyFragment extends LoaderFragment implements ContactsContract.RawContacts.DELETED + "=?", new String[]{ Constants.ACCOUNT_TYPE, - Long.toString(masterKeyId), + Long.toString(mMasterKeyId), "0" // "0" for "not deleted" }, null); } + case LOADER_ID_SUBKEY_STATUS: { + throw new IllegalStateException("This callback should never end up here!"); + } + default: return null; } @@ -439,22 +414,6 @@ public class ViewKeyFragment extends LoaderFragment implements // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - if (data.getCount() == 1 && data.moveToFirst()) { - - mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mFingerprint = data.getBlob(INDEX_FINGERPRINT); - long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - - // init other things after we know if it's a secret key - initUserIds(mIsSecret); - initLinkedIds(mIsSecret); - initLinkedContactLoader(masterKeyId, mIsSecret); - initCardButtonsVisibility(mIsSecret); - } - break; - } - case LOADER_ID_USER_IDS: { setContentShown(true, false); mUserIdsAdapter.swapCursor(data); @@ -494,25 +453,14 @@ public class ViewKeyFragment extends LoaderFragment implements } break; } + + case LOADER_ID_SUBKEY_STATUS: { + throw new IllegalStateException("This callback should never end up here!"); + } } } - private void initUserIds(boolean isSecret) { - mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !isSecret, null); - mUserIds.setAdapter(mUserIdsAdapter); - getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); - } - - private void initLinkedIds(boolean isSecret) { - if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) { - mLinkedIdsAdapter = - new LinkedIdsAdapter(getActivity(), null, 0, isSecret, mLinkedIdsExpander); - mLinkedIds.setAdapter(mLinkedIdsAdapter); - getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this); - } - } - - private void initLinkedContactLoader(long masterKeyId, boolean isSecret) { + private void initLinkedContactLoader() { if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED) { Log.w(Constants.TAG, "loading linked system contact not possible READ_CONTACTS permission denied!"); @@ -521,8 +469,6 @@ public class ViewKeyFragment extends LoaderFragment implements } Bundle linkedContactData = new Bundle(); - linkedContactData.putLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID, masterKeyId); - linkedContactData.putBoolean(LOADER_EXTRA_LINKED_CONTACT_IS_SECRET, isSecret); // initialises loader for contact query so we can listen to any updates getLoaderManager().initLoader(LOADER_ID_LINKED_CONTACT, linkedContactData, this); @@ -557,7 +503,14 @@ public class ViewKeyFragment extends LoaderFragment implements mLinkedIdsAdapter.swapCursor(null); break; } + case LOADER_ID_SUBKEY_STATUS: + mKeyHealthPresenter.onLoaderReset(loader); + break; } } + public boolean isValidForData(boolean isSecret) { + return isSecret == mIsSecret; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java new file mode 100644 index 000000000..a2f904974 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import android.content.Context; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.CardView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyHealthClickListener; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyHealthMvpView; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyHealthStatus; +import org.sufficientlysecure.keychain.ui.widget.KeyStatusList.KeyDisplayStatus; + + +public class KeyHealthCardView extends CardView implements KeyHealthMvpView, OnClickListener { + private final View vLayout; + private final TextView vTitle, vSubtitle; + private final ImageView vIcon; + private final ImageView vExpander; + private final KeyStatusList vKeyStatusList; + private final View vKeyStatusDivider; + + private KeyHealthClickListener keyHealthClickListener; + + public KeyHealthCardView(Context context, AttributeSet attrs) { + super(context, attrs); + + View view = LayoutInflater.from(context).inflate(R.layout.key_health_card_content, this, true); + + vLayout = view.findViewById(R.id.key_health_layout); + vTitle = (TextView) view.findViewById(R.id.key_health_title); + vSubtitle = (TextView) view.findViewById(R.id.key_health_subtitle); + vIcon = (ImageView) view.findViewById(R.id.key_health_icon); + vExpander = (ImageView) view.findViewById(R.id.key_health_expander); + + vLayout.setOnClickListener(this); + + vKeyStatusDivider = view.findViewById(R.id.key_health_divider); + vKeyStatusList = (KeyStatusList) view.findViewById(R.id.key_health_status_list); + } + + enum KeyHealthDisplayStatus { + OK (R.string.key_health_ok_title, R.string.key_health_ok_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light), + DIVERT (R.string.key_health_divert_title, R.string.key_health_divert_subtitle, + R.drawable.yubi_icon_24dp, R.color.md_black_1000), + REVOKED (R.string.key_health_revoked_title, R.string.key_health_revoked_subtitle, + R.drawable.ic_close_black_24dp, R.color.android_red_light), + EXPIRED (R.string.key_health_expired_title, R.string.key_health_expired_subtitle, + R.drawable.status_signature_expired_cutout_24dp, R.color.android_red_light), + INSECURE (R.string.key_health_insecure_title, R.string.key_health_insecure_subtitle, + R.drawable.ic_close_black_24dp, R.color.android_red_light), + SPECIAL (R.string.key_health_special_title, R.string.key_health_special_subtitle, + R.drawable.status_signature_unverified_cutout_24dp, R.color.android_orange_light), + SIGN_ONLY (R.string.key_health_sign_only_title, R.string.key_health_sign_only_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light), + STRIPPED (R.string.key_health_stripped_title, R.string.key_health_stripped_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light), + PARTIAL_STRIPPED (R.string.key_health_partial_stripped_title, R.string.key_health_partial_stripped_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light); + + @StringRes + private final int title, subtitle; + @DrawableRes + private final int icon; + @ColorRes + private final int iconColor; + + KeyHealthDisplayStatus(@StringRes int title, @StringRes int subtitle, + @DrawableRes int icon, @ColorRes int iconColor) { + this.title = title; + this.subtitle = subtitle; + this.icon = icon; + this.iconColor = iconColor; + } + } + + @Override + public void setKeyStatus(KeyHealthStatus keyHealthStatus) { + switch (keyHealthStatus) { + case OK: + setKeyStatus(KeyHealthDisplayStatus.OK); + break; + case DIVERT: + setKeyStatus(KeyHealthDisplayStatus.DIVERT); + break; + case REVOKED: + setKeyStatus(KeyHealthDisplayStatus.REVOKED); + break; + case EXPIRED: + setKeyStatus(KeyHealthDisplayStatus.EXPIRED); + break; + case INSECURE: + setKeyStatus(KeyHealthDisplayStatus.INSECURE); + break; + case SPECIAL: + setKeyStatus(KeyHealthDisplayStatus.SPECIAL); + break; + case STRIPPED: + setKeyStatus(KeyHealthDisplayStatus.STRIPPED); + break; + case SIGN_ONLY: + setKeyStatus(KeyHealthDisplayStatus.SIGN_ONLY); + break; + case PARTIAL_STRIPPED: + setKeyStatus(KeyHealthDisplayStatus.PARTIAL_STRIPPED); + break; + } + } + + @Override + public void onClick(View view) { + if (keyHealthClickListener != null) { + keyHealthClickListener.onKeyHealthClick(); + } + } + + @Override + public void setOnHealthClickListener(KeyHealthClickListener keyHealthClickListener) { + this.keyHealthClickListener = keyHealthClickListener; + vLayout.setClickable(keyHealthClickListener != null); + } + + @Override + public void setShowExpander(boolean showExpander) { + vLayout.setClickable(showExpander); + vExpander.setVisibility(showExpander ? View.VISIBLE : View.GONE); + } + + @Override + public void showExpandedState(KeyDisplayStatus certifyStatus, KeyDisplayStatus signStatus, + KeyDisplayStatus encryptStatus) { + if (certifyStatus == null && signStatus == null && encryptStatus == null) { + vKeyStatusList.setVisibility(View.GONE); + vKeyStatusDivider.setVisibility(View.GONE); + vExpander.setImageResource(R.drawable.ic_expand_more_black_24dp); + } else { + vKeyStatusList.setVisibility(View.VISIBLE); + vKeyStatusDivider.setVisibility(View.VISIBLE); + vExpander.setImageResource(R.drawable.ic_expand_less_black_24dp); + + vKeyStatusList.setCertifyStatus(certifyStatus); + vKeyStatusList.setSignStatus(signStatus); + vKeyStatusList.setDecryptStatus(encryptStatus); + } + + } + + @Override + public void hideExpandedInfo() { + showExpandedState(null, null, null); + } + + private void setKeyStatus(KeyHealthDisplayStatus keyHealthDisplayStatus) { + vTitle.setText(keyHealthDisplayStatus.title); + vSubtitle.setText(keyHealthDisplayStatus.subtitle); + vIcon.setImageResource(keyHealthDisplayStatus.icon); + vIcon.setColorFilter(ContextCompat.getColor(getContext(), keyHealthDisplayStatus.iconColor)); + + setVisibility(View.VISIBLE); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java new file mode 100644 index 000000000..d1f500781 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import java.util.Comparator; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.ui.widget.KeyStatusList.KeyDisplayStatus; +import org.sufficientlysecure.keychain.ui.widget.SubkeyStatusLoader.KeySubkeyStatus; +import org.sufficientlysecure.keychain.ui.widget.SubkeyStatusLoader.SubKeyItem; + + +public class KeyHealthPresenter implements LoaderCallbacks { + static final Comparator SUBKEY_COMPARATOR = new Comparator() { + @Override + public int compare(SubKeyItem one, SubKeyItem two) { + // if one is valid and the other isn't, the valid one always comes first + if (one.isValid() ^ two.isValid()) { + return one.isValid() ? -1 : 1; + } + // compare usability, if one is "more usable" than the other, that one comes first + int usability = one.mSecretKeyType.compareUsability(two.mSecretKeyType); + if (usability != 0) { + return usability; + } + if (one.mIsSecure ^ two.mIsSecure) { + return one.mIsSecure ? -1 : 1; + } + // otherwise, the newer one comes first + return one.newerThan(two) ? -1 : 1; + } + }; + + private final Context context; + private final KeyHealthMvpView view; + private final int loaderId; + + private final long masterKeyId; + private final boolean isSecret; + + private KeySubkeyStatus subkeyStatus; + private boolean showingExpandedInfo; + + + public KeyHealthPresenter(Context context, KeyHealthMvpView view, int loaderId, long masterKeyId, boolean isSecret) { + this.context = context; + this.view = view; + this.loaderId = loaderId; + + this.masterKeyId = masterKeyId; + this.isSecret = isSecret; + + view.setOnHealthClickListener(new KeyHealthClickListener() { + @Override + public void onKeyHealthClick() { + KeyHealthPresenter.this.onKeyHealthClick(); + } + }); + } + + public void startLoader(LoaderManager loaderManager) { + loaderManager.restartLoader(loaderId, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new SubkeyStatusLoader(context, context.getContentResolver(), masterKeyId, SUBKEY_COMPARATOR); + } + + @Override + public void onLoadFinished(Loader loader, KeySubkeyStatus subkeyStatus) { + this.subkeyStatus = subkeyStatus; + + KeyHealthStatus keyHealthStatus = determineKeyHealthStatus(subkeyStatus); + boolean forceExpanded = keyHealthStatus == KeyHealthStatus.INSECURE; + if (forceExpanded) { + view.setKeyStatus(keyHealthStatus); + view.setShowExpander(false); + displayExpandedInfo(false); + } else { + view.setKeyStatus(keyHealthStatus); + view.setShowExpander( + keyHealthStatus != KeyHealthStatus.EXPIRED && keyHealthStatus != KeyHealthStatus.REVOKED); + } + } + + private KeyHealthStatus determineKeyHealthStatus(KeySubkeyStatus subkeyStatus) { + SubKeyItem keyCertify = subkeyStatus.keyCertify; + if (keyCertify.mIsRevoked) { + return KeyHealthStatus.REVOKED; + } + + if (keyCertify.mIsExpired) { + return KeyHealthStatus.EXPIRED; + } + + if (!keyCertify.mIsSecure) { + return KeyHealthStatus.INSECURE; + } + + if (!subkeyStatus.keysSign.isEmpty() && subkeyStatus.keysEncrypt.isEmpty()) { + SubKeyItem keySign = subkeyStatus.keysSign.get(0); + if (!keySign.isValid()) { + return KeyHealthStatus.SPECIAL; + } + + if (!keySign.mIsSecure) { + return KeyHealthStatus.INSECURE; + } + + return KeyHealthStatus.SIGN_ONLY; + } + + if (subkeyStatus.keysSign.isEmpty() || subkeyStatus.keysEncrypt.isEmpty()) { + return KeyHealthStatus.SPECIAL; + } + + SubKeyItem keySign = subkeyStatus.keysSign.get(0); + SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.get(0); + + if (!keySign.mIsSecure && keySign.isValid() + || !keyEncrypt.mIsSecure && keyEncrypt.isValid()) { + return KeyHealthStatus.INSECURE; + } + + if (!keySign.isValid() || !keyEncrypt.isValid()) { + return KeyHealthStatus.SPECIAL; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY + && keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY + && keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyHealthStatus.STRIPPED; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY + || keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY + || keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyHealthStatus.PARTIAL_STRIPPED; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD + && keySign.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD + && keyEncrypt.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { + return KeyHealthStatus.DIVERT; + } + + return KeyHealthStatus.OK; + } + + @Override + public void onLoaderReset(Loader loader) { + + } + + private void onKeyHealthClick() { + if (showingExpandedInfo) { + showingExpandedInfo = false; + view.hideExpandedInfo(); + } else { + showingExpandedInfo = true; + displayExpandedInfo(true); + } + } + + private void displayExpandedInfo(boolean displayAll) { + SubKeyItem keyCertify = subkeyStatus.keyCertify; + SubKeyItem keySign = subkeyStatus.keysSign.isEmpty() ? null : subkeyStatus.keysSign.get(0); + SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.isEmpty() ? null : subkeyStatus.keysEncrypt.get(0); + + KeyDisplayStatus certDisplayStatus = getKeyDisplayStatus(keyCertify); + KeyDisplayStatus signDisplayStatus = getKeyDisplayStatus(keySign); + KeyDisplayStatus encryptDisplayStatus = getKeyDisplayStatus(keyEncrypt); + + if (!displayAll) { + if (certDisplayStatus == KeyDisplayStatus.OK) { + certDisplayStatus = null; + } + if (certDisplayStatus == KeyDisplayStatus.INSECURE) { + signDisplayStatus = null; + encryptDisplayStatus = null; + } + if (signDisplayStatus == KeyDisplayStatus.OK) { + signDisplayStatus = null; + } + if (encryptDisplayStatus == KeyDisplayStatus.OK) { + encryptDisplayStatus = null; + } + } + + view.showExpandedState(certDisplayStatus, signDisplayStatus, encryptDisplayStatus); + } + + private KeyDisplayStatus getKeyDisplayStatus(SubKeyItem subKeyItem) { + if (subKeyItem == null) { + return KeyDisplayStatus.UNAVAILABLE; + } + + if (subKeyItem.mIsRevoked) { + return KeyDisplayStatus.REVOKED; + } + if (subKeyItem.mIsExpired) { + return KeyDisplayStatus.EXPIRED; + } + if (!subKeyItem.mIsSecure) { + return KeyDisplayStatus.INSECURE; + } + if (subKeyItem.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyDisplayStatus.STRIPPED; + } + if (subKeyItem.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { + return KeyDisplayStatus.DIVERT; + } + + return KeyDisplayStatus.OK; + } + + enum KeyHealthStatus { + OK, DIVERT, REVOKED, EXPIRED, INSECURE, SIGN_ONLY, STRIPPED, PARTIAL_STRIPPED, SPECIAL + } + + interface KeyHealthMvpView { + void setKeyStatus(KeyHealthStatus keyHealthStatus); + void setShowExpander(boolean showExpander); + void setOnHealthClickListener(KeyHealthClickListener keyHealthClickListener); + + void showExpandedState(KeyDisplayStatus certifyStatus, KeyDisplayStatus signStatus, + KeyDisplayStatus encryptStatus); + + void hideExpandedInfo(); + } + + interface KeyStatusMvpView { + void setCertifyStatus(KeyDisplayStatus unavailable); + void setSignStatus(KeyDisplayStatus signStatus); + void setDecryptStatus(KeyDisplayStatus encryptStatus); + } + + interface KeyHealthClickListener { + void onKeyHealthClick(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java new file mode 100644 index 000000000..171891f39 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import android.content.Context; +import android.support.annotation.ColorRes; +import android.support.annotation.StringRes; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyStatusMvpView; + + +public class KeyStatusList extends LinearLayout implements KeyStatusMvpView { + private final TextView vCertText, vSignText, vDecryptText; + private final ImageView vCertIcon, vSignIcon, vDecryptIcon; + private final View vCertToken, vSignToken, vDecryptToken; + private final View vCertifyLayout, vSignLayout, vDecryptLayout; + + public KeyStatusList(Context context, AttributeSet attrs) { + super(context, attrs); + + setOrientation(VERTICAL); + + View view = LayoutInflater.from(context).inflate(R.layout.subkey_status_card_content, this, true); + + vCertifyLayout = view.findViewById(R.id.cap_certify); + vSignLayout = view.findViewById(R.id.cap_sign); + vDecryptLayout = view.findViewById(R.id.cap_decrypt); + + vCertText = (TextView) view.findViewById(R.id.cap_cert_text); + vSignText = (TextView) view.findViewById(R.id.cap_sign_text); + vDecryptText = (TextView) view.findViewById(R.id.cap_decrypt_text); + + vCertIcon = (ImageView) view.findViewById(R.id.cap_cert_icon); + vSignIcon = (ImageView) view.findViewById(R.id.cap_sign_icon); + vDecryptIcon = (ImageView) view.findViewById(R.id.cap_decrypt_icon); + + vCertToken = view.findViewById(R.id.cap_cert_security_token); + vSignToken = view.findViewById(R.id.cap_sign_security_token); + vDecryptToken = view.findViewById(R.id.cap_decrypt_security_token); + + } + + // this is just a list of statuses a key can be in, which we can also display + enum KeyDisplayStatus { + OK (R.color.android_green_light, R.color.primary, + R.string.cap_cert_ok, R.string.cap_sign_ok, R.string.cap_decrypt_ok, false), + DIVERT (R.color.android_green_light, R.color.primary, + R.string.cap_cert_divert, R.string.cap_sign_divert, R.string.cap_decrypt_divert, true), + REVOKED (R.color.android_red_light, R.color.android_red_light, + R.string.cap_sign_revoked, R.string.cap_decrypt_revoked, false), + EXPIRED (R.color.android_red_light, R.color.android_red_light, + R.string.cap_sign_expired, R.string.cap_decrypt_expired, false), + STRIPPED (R.color.android_red_light, R.color.android_red_light, + R.string.cap_cert_stripped, R.string.cap_sign_stripped, R.string.cap_decrypt_stripped, false), + INSECURE (R.color.android_red_light, R.color.android_red_light, + R.string.cap_sign_insecure, R.string.cap_sign_insecure, false), + UNAVAILABLE (R.color.android_red_light, R.color.android_red_light, + R.string.cap_cert_unavailable, R.string.cap_sign_unavailable, R.string.cap_decrypt_unavailable, false); + + @ColorRes final int mColor, mTextColor; + @StringRes final Integer mCertifyStr, mSignStr, mDecryptStr; + final boolean mIsDivert; + + KeyDisplayStatus(@ColorRes int color, @ColorRes int textColor, + @StringRes int signStr, @StringRes int encryptStr, boolean isDivert) { + mColor = color; + mTextColor = textColor; + mCertifyStr = null; + mSignStr = signStr; + mDecryptStr = encryptStr; + mIsDivert = isDivert; + } + + KeyDisplayStatus(@ColorRes int color, @ColorRes int textColor, + @StringRes int certifyStr, @StringRes int signStr, @StringRes int encryptStr, boolean isDivert) { + mColor = color; + mTextColor = textColor; + mCertifyStr = certifyStr; + mSignStr = signStr; + mDecryptStr = encryptStr; + mIsDivert = isDivert; + } + + } + + @Override + public void setCertifyStatus(KeyDisplayStatus keyDisplayStatus) { + if (keyDisplayStatus == null) { + vCertifyLayout.setVisibility(View.GONE); + return; + } + + vCertIcon.setColorFilter(getResources().getColor(keyDisplayStatus.mColor)); + vCertText.setText(keyDisplayStatus.mCertifyStr); + vCertText.setTextColor(getResources().getColor(keyDisplayStatus.mTextColor)); + vCertToken.setVisibility(keyDisplayStatus.mIsDivert ? View.VISIBLE : View.GONE); + vCertifyLayout.setVisibility(View.VISIBLE); + } + + @Override + public void setSignStatus(KeyDisplayStatus keyDisplayStatus) { + if (keyDisplayStatus == null) { + vSignLayout.setVisibility(View.GONE); + return; + } + vSignIcon.setColorFilter(getResources().getColor(keyDisplayStatus.mColor)); + vSignText.setText(keyDisplayStatus.mSignStr); + vSignText.setTextColor(getResources().getColor(keyDisplayStatus.mTextColor)); + vSignToken.setVisibility(keyDisplayStatus.mIsDivert ? View.VISIBLE : View.GONE); + vSignLayout.setVisibility(View.VISIBLE); + } + + @Override + public void setDecryptStatus(KeyDisplayStatus keyDisplayStatus) { + if (keyDisplayStatus == null) { + vDecryptLayout.setVisibility(View.GONE); + return; + } + vDecryptIcon.setColorFilter(getResources().getColor(keyDisplayStatus.mColor)); + vDecryptText.setText(keyDisplayStatus.mDecryptStr); + vDecryptText.setTextColor(getResources().getColor(keyDisplayStatus.mTextColor)); + vDecryptToken.setVisibility(keyDisplayStatus.mIsDivert ? View.VISIBLE : View.GONE); + vDecryptLayout.setVisibility(View.VISIBLE); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java new file mode 100644 index 000000000..02c7782bd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java @@ -0,0 +1,173 @@ +package org.sufficientlysecure.keychain.ui.widget; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.ui.widget.SubkeyStatusLoader.KeySubkeyStatus; + + +class SubkeyStatusLoader extends AsyncTaskLoader { + public static final String[] PROJECTION = new String[] { + Keys.KEY_ID, + Keys.CREATION, + Keys.CAN_CERTIFY, + Keys.CAN_SIGN, + Keys.CAN_ENCRYPT, + Keys.HAS_SECRET, + Keys.EXPIRY, + Keys.IS_REVOKED, + Keys.IS_SECURE + }; + private static final int INDEX_KEY_ID = 0; + private static final int INDEX_CREATION = 1; + private static final int INDEX_CAN_CERTIFY = 2; + private static final int INDEX_CAN_SIGN = 3; + private static final int INDEX_CAN_ENCRYPT = 4; + private static final int INDEX_HAS_SECRET = 5; + private static final int INDEX_EXPIRY = 6; + private static final int INDEX_IS_REVOKED = 7; + private static final int INDEX_IS_SECURE = 8; + + + private final ContentResolver contentResolver; + private final long masterKeyId; + private final Comparator comparator; + + private KeySubkeyStatus cachedResult; + + + SubkeyStatusLoader(Context context, ContentResolver contentResolver, long masterKeyId, Comparator comparator) { + super(context); + + this.contentResolver = contentResolver; + this.masterKeyId = masterKeyId; + this.comparator = comparator; + } + + @Override + public KeySubkeyStatus loadInBackground() { + Cursor cursor = contentResolver.query(Keys.buildKeysUri(masterKeyId), PROJECTION, null, null, null); + if (cursor == null) { + Log.e(Constants.TAG, "Error loading key items!"); + return null; + } + + try { + SubKeyItem keyCertify = null; + ArrayList keysSign = new ArrayList<>(); + ArrayList keysEncrypt = new ArrayList<>(); + while (cursor.moveToNext()) { + SubKeyItem ski = new SubKeyItem(cursor); + + if (ski.mKeyId == masterKeyId) { + keyCertify = ski; + } + + if (ski.mCanSign) { + keysSign.add(ski); + } + if (ski.mCanEncrypt) { + keysEncrypt.add(ski); + } + } + + if (keyCertify == null) { + throw new IllegalStateException("Certification key must be set at this point, it's a bug otherwise!"); + } + + Collections.sort(keysSign, comparator); + Collections.sort(keysEncrypt, comparator); + + return new KeySubkeyStatus(keyCertify, keysSign, keysEncrypt); + } finally { + cursor.close(); + } + } + + @Override + public void deliverResult(KeySubkeyStatus keySubkeyStatus) { + cachedResult = keySubkeyStatus; + + if (isStarted()) { + super.deliverResult(keySubkeyStatus); + } + } + + @Override + protected void onStartLoading() { + if (cachedResult != null) { + deliverResult(cachedResult); + } + + if (takeContentChanged() || cachedResult == null) { + forceLoad(); + } + } + + static class KeySubkeyStatus { + @NonNull + final SubKeyItem keyCertify; + final List keysSign; + final List keysEncrypt; + + KeySubkeyStatus(@NonNull SubKeyItem keyCertify, List keysSign, List keysEncrypt) { + this.keyCertify = keyCertify; + this.keysSign = keysSign; + this.keysEncrypt = keysEncrypt; + } + } + + static class SubKeyItem { + final int mPosition; + final long mKeyId; + final Date mCreation; + final SecretKeyType mSecretKeyType; + final boolean mIsRevoked, mIsExpired; + final boolean mCanCertify, mCanSign, mCanEncrypt; + final boolean mIsSecure; + + SubKeyItem(Cursor cursor) { + mPosition = cursor.getPosition(); + + mKeyId = cursor.getLong(INDEX_KEY_ID); + mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); + + mSecretKeyType = SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET)); + + mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; + Date expiryDate = null; + if (!cursor.isNull(INDEX_EXPIRY)) { + expiryDate = new Date(cursor.getLong(INDEX_EXPIRY) * 1000); + } + mIsExpired = expiryDate != null && expiryDate.before(new Date()); + + mCanCertify = cursor.getInt(INDEX_CAN_CERTIFY) > 0; + mCanSign = cursor.getInt(INDEX_CAN_SIGN) > 0; + mCanEncrypt = cursor.getInt(INDEX_CAN_ENCRYPT) > 0; + + mIsSecure = cursor.getInt(INDEX_IS_SECURE) > 0; + } + + boolean newerThan(SubKeyItem other) { + return mCreation.after(other.mCreation); + } + + boolean isValid() { + return !mIsRevoked && !mIsExpired; + } + } +} diff --git a/OpenKeychain/src/main/res/layout/key_health_card_content.xml b/OpenKeychain/src/main/res/layout/key_health_card_content.xml new file mode 100644 index 000000000..11c7eff10 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_health_card_content.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/log_display_activity.xml b/OpenKeychain/src/main/res/layout/log_display_activity.xml index be08ef6fd..16888f81a 100644 --- a/OpenKeychain/src/main/res/layout/log_display_activity.xml +++ b/OpenKeychain/src/main/res/layout/log_display_activity.xml @@ -13,5 +13,4 @@ android:name="org.sufficientlysecure.keychain.ui.LogDisplayFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> - \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml b/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml new file mode 100644 index 000000000..51f980ad5 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/tools_vertlin.xml b/OpenKeychain/src/main/res/layout/tools_vertlin.xml new file mode 100644 index 000000000..d4f26b18f --- /dev/null +++ b/OpenKeychain/src/main/res/layout/tools_vertlin.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/view_key_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_fragment.xml index aa3a8e8da..fbf910543 100644 --- a/OpenKeychain/src/main/res/layout/view_key_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_fragment.xml @@ -9,6 +9,19 @@ android:paddingRight="16dp" android:paddingTop="16dp"> + + "Help" "Backup key" "Delete key" + "View key status" "Manage my keys" "Search" "Open" @@ -1807,4 +1808,51 @@ Requested key: Error selecting key %s for signing! Error selecting key %s for encryption! + + "Key Status" + This key is yours. You can use it to: + + Confirm other keys + "This key can confirm other keys." + "This key can confirm other keys, using a Security Token." + "This key is stripped, it can NOT confirm other keys." + "This key is not configured to confirm keys!" + + Sign messages + "This key can sign/send messages." + "This key can sign/send messages, using a Security Token." + "This key can't sign/send messages, because it is expired." + "This key can't sign/send messages, because it is revoked." + "This key can\'t sign/send messages on this device!" + "This key is not configured to sign/send messages!" + "This key can sign/send messages, but not securely!" + + Decrypt messages + "This key can decrypt/receive messages." + "This key can decrypt/receive messages, using a Security Token." + "This key can decrypt/receive messages, but is expired." + "This key can decrypt/receive messages, but is revoked." + "This key can\'t decrypt/receive messages on this device." + "This key is not configured to decrypt/receive messages!" + "This key can decrypt/receive messages, but not securely!" + + "Healthy" + "No key issues found." + "Healthy (Security Token)" + "No key issues found." + "Expired" + "This key should not be used anymore." + "Revoked" + "This key can\'t be used anymore." + "Insecure" + "This key is not secure!" + "Special" + "Click for details" + "Healthy (Signing Key)" + "Click for details" + "Healthy (Stripped)" + "Click for details" + "Healthy (Partially Stripped)" + "Click for details" +