add secret key status (wip)

This commit is contained in:
Vincent Breitmoser 2017-04-24 20:49:45 +02:00
parent 720f2dbef1
commit fed0fff9d7
13 changed files with 1236 additions and 128 deletions

View file

@ -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

View file

@ -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

View file

@ -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<Cursor> {
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<Cursor> 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;
}
}

View file

@ -0,0 +1,188 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.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);
}
}

View file

@ -0,0 +1,264 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.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<KeySubkeyStatus> {
static final Comparator<SubKeyItem> SUBKEY_COMPARATOR = new Comparator<SubKeyItem>() {
@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<KeySubkeyStatus> onCreateLoader(int id, Bundle args) {
return new SubkeyStatusLoader(context, context.getContentResolver(), masterKeyId, SUBKEY_COMPARATOR);
}
@Override
public void onLoadFinished(Loader<KeySubkeyStatus> 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();
}
}

View file

@ -0,0 +1,148 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.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);
}
}

View file

@ -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<KeySubkeyStatus> {
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<SubKeyItem> comparator;
private KeySubkeyStatus cachedResult;
SubkeyStatusLoader(Context context, ContentResolver contentResolver, long masterKeyId, Comparator<SubKeyItem> 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<SubKeyItem> keysSign = new ArrayList<>();
ArrayList<SubKeyItem> 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<SubKeyItem> keysSign;
final List<SubKeyItem> keysEncrypt;
KeySubkeyStatus(@NonNull SubKeyItem keyCertify, List<SubKeyItem> keysSign, List<SubKeyItem> 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;
}
}
}

View file

@ -0,0 +1,96 @@
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
style="@style/CardViewHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Key Status" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/key_health_layout"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:background="?selectableItemBackground"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/key_health_icon"
tools:src="@drawable/ic_check_white_24dp"
tools:tint="@color/android_green_light"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/key_health_title"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Healthy" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/key_health_subtitle"
tools:text="No key issues found." />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/key_health_expander"
android:src="@drawable/ic_expand_more_black_24dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:visibility="gone"
tools:visibility="visible"
/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dip"
android:id="@+id/key_health_divider"
android:background="?android:attr/listDivider" />
<org.sufficientlysecure.keychain.ui.widget.KeyStatusList
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/key_health_status_list"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:visibility="gone"
tools:visibility="visible"
/>
</LinearLayout>
</merge>

View file

@ -13,5 +13,4 @@
android:name="org.sufficientlysecure.keychain.ui.LogDisplayFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View file

@ -0,0 +1,172 @@
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="@layout/tools_vertlin">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:id="@+id/cap_certify">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_cert_icon"
android:src="@drawable/ic_action_verified_cutout_24dp"
tools:tint="@color/android_green_light"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/colorHeaderText"
android:text="@string/cap_title_confirm" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_cert_text"
tools:text="@string/cap_cert_ok" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_cert_security_token"
android:src="@drawable/yubi_icon_24dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:visibility="gone"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:id="@+id/cap_sign">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_sign_icon"
android:src="@drawable/ic_mode_edit_white_24dp"
tools:tint="@color/android_green_light"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_sign_caption"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/colorHeaderText"
android:text="@string/cap_title_sign" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_sign_text"
tools:text="@string/cap_sign_divert" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_sign_security_token"
android:src="@drawable/yubi_icon_24dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:visibility="gone"
tools:visibility="visible"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:id="@+id/cap_decrypt">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_decrypt_icon"
android:src="@drawable/ic_lock_white_24dp"
tools:tint="@color/android_red_light"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/colorHeaderText"
android:text="@string/cap_title_decrypt" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_decrypt_text"
tools:textColor="@color/android_red_light"
tools:text="@string/cap_decrypt_unavailable" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/cap_decrypt_security_token"
android:src="@drawable/yubi_icon_24dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:visibility="gone"
/>
</LinearLayout>
</merge>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/subkey_status_card_content" />
</LinearLayout>

View file

@ -9,6 +9,19 @@
android:paddingRight="16dp"
android:paddingTop="16dp">
<org.sufficientlysecure.keychain.ui.widget.KeyHealthCardView
android:id="@+id/subkey_status_card"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
card_view:cardBackgroundColor="?attr/colorCardViewBackground"
card_view:cardElevation="2dp"
card_view:cardUseCompatPadding="true"
card_view:cardCornerRadius="4dp"
/>
<android.support.v7.widget.CardView
android:id="@+id/card_linked_ids"
android:layout_width="match_parent"

View file

@ -121,6 +121,7 @@
<string name="menu_help">"Help"</string>
<string name="menu_export_key">"Backup key"</string>
<string name="menu_delete_key">"Delete key"</string>
<string name="menu_status">"View key status"</string>
<string name="menu_manage_keys">"Manage my keys"</string>
<string name="menu_search">"Search"</string>
<string name="menu_open">"Open"</string>
@ -1807,4 +1808,51 @@
<string name="requested_key_label">Requested key:</string>
<string name="error_preselect_sign_key">Error selecting key %s for signing!</string>
<string name="error_preselect_encrypt_key">Error selecting key %s for encryption!</string>
<string name="title_key_status">"Key Status"</string>
<string name="caption_secret_status">This key is yours. You can use it to:</string>
<string name="cap_title_confirm">Confirm other keys</string>
<string name="cap_cert_ok">"This key can confirm other keys."</string>
<string name="cap_cert_divert">"This key can confirm other keys, using a Security Token."</string>
<string name="cap_cert_stripped">"This key is stripped, it can NOT confirm other keys."</string>
<string name="cap_cert_unavailable">"This key is not configured to confirm keys!"</string>
<string name="cap_title_sign">Sign messages</string>
<string name="cap_sign_ok">"This key can sign/send messages."</string>
<string name="cap_sign_divert">"This key can sign/send messages, using a Security Token."</string>
<string name="cap_sign_expired">"This key can't sign/send messages, because it is expired."</string>
<string name="cap_sign_revoked">"This key can't sign/send messages, because it is revoked."</string>
<string name="cap_sign_stripped">"This key can\'t sign/send messages on this device!"</string>
<string name="cap_sign_unavailable">"This key is not configured to sign/send messages!"</string>
<string name="cap_sign_insecure">"This key can sign/send messages, but not securely!"</string>
<string name="cap_title_decrypt">Decrypt messages</string>
<string name="cap_decrypt_ok">"This key can decrypt/receive messages."</string>
<string name="cap_decrypt_divert">"This key can decrypt/receive messages, using a Security Token."</string>
<string name="cap_decrypt_expired">"This key can decrypt/receive messages, but is expired."</string>
<string name="cap_decrypt_revoked">"This key can decrypt/receive messages, but is revoked."</string>
<string name="cap_decrypt_stripped">"This key can\'t decrypt/receive messages on this device."</string>
<string name="cap_decrypt_unavailable">"This key is not configured to decrypt/receive messages!"</string>
<string name="cap_decrypt_insecure">"This key can decrypt/receive messages, but not securely!"</string>
<string name="key_health_ok_title">"Healthy"</string>
<string name="key_health_ok_subtitle">"No key issues found."</string>
<string name="key_health_divert_title">"Healthy (Security Token)"</string>
<string name="key_health_divert_subtitle">"No key issues found."</string>
<string name="key_health_expired_title">"Expired"</string>
<string name="key_health_expired_subtitle">"This key should not be used anymore."</string>
<string name="key_health_revoked_title">"Revoked"</string>
<string name="key_health_revoked_subtitle">"This key can\'t be used anymore."</string>
<string name="key_health_insecure_title">"Insecure"</string>
<string name="key_health_insecure_subtitle">"This key is not secure!"</string>
<string name="key_health_special_title">"Special"</string>
<string name="key_health_special_subtitle">"Click for details"</string>
<string name="key_health_sign_only_title">"Healthy (Signing Key)"</string>
<string name="key_health_sign_only_subtitle">"Click for details"</string>
<string name="key_health_stripped_title">"Healthy (Stripped)"</string>
<string name="key_health_stripped_subtitle">"Click for details"</string>
<string name="key_health_partial_stripped_title">"Healthy (Partially Stripped)"</string>
<string name="key_health_partial_stripped_subtitle">"Click for details"</string>
</resources>