Merge branch 'linked-identities' (and fix OperationHelper ids)

Merge Linked Identities.

Also includes an important fix for OperationHelper ids, which had an
error in the bit mask logic.

Conflicts:
	Graphics/update-drawables.sh
	OpenKeychain/build.gradle
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportFragment.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
	OpenKeychain/src/main/res/anim/fade_in.xml
	OpenKeychain/src/main/res/anim/fade_out.xml
	OpenKeychain/src/main/res/layout/decrypt_text_fragment.xml
	OpenKeychain/src/main/res/layout/encrypt_decrypt_overview_fragment.xml
	OpenKeychain/src/main/res/layout/view_key_fragment.xml
	OpenKeychain/src/main/res/menu/key_view.xml
	OpenKeychain/src/main/res/values/strings.xml
	OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java
	README.md
This commit is contained in:
Vincent Breitmoser
2015-08-29 13:28:56 +02:00
115 changed files with 6277 additions and 97 deletions

View File

@@ -83,7 +83,9 @@ public class CertifyKeyFragment
};
private static final int INDEX_MASTER_KEY_ID = 1;
private static final int INDEX_USER_ID = 2;
@SuppressWarnings("unused")
private static final int INDEX_IS_PRIMARY = 3;
@SuppressWarnings("unused")
private static final int INDEX_IS_REVOKED = 4;
private MultiUserIdsAdapter mUserIdsAdapter;

View File

@@ -280,7 +280,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
case LOADER_ID_SUBKEYS: {

View File

@@ -63,7 +63,6 @@ import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.ConsolidateInputParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;

View File

@@ -71,6 +71,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
* internally and is NOT meant to be used by signing operations before adding a signature time
*/
public class PassphraseDialogActivity extends FragmentActivity {
public static final String RESULT_CRYPTO_INPUT = "result_data";
public static final String EXTRA_REQUIRED_INPUT = "required_input";
@@ -261,6 +262,9 @@ public class PassphraseDialogActivity extends FragmentActivity {
case DIVERT_TO_CARD:
message = getString(R.string.yubikey_pin_for, userId);
break;
// special case: empty passphrase just returns the empty passphrase
case PASSPHRASE_EMPTY:
finishCaching(new Passphrase(""));
default:
throw new AssertionError("Unhandled SecretKeyType (should not happen)");
}

View File

@@ -68,6 +68,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
@@ -357,6 +358,12 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
return true;
}
case R.id.menu_key_view_add_linked_identity: {
Intent intent = new Intent(this, LinkedIdWizard.class);
intent.setData(mDataUri);
startActivity(intent);
return true;
}
case R.id.menu_key_view_edit: {
editKey(mDataUri);
return true;
@@ -377,8 +384,13 @@ public class ViewKeyActivity extends BaseNfcActivity implements
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem editKey = menu.findItem(R.id.menu_key_view_edit);
editKey.setVisible(mIsSecret);
MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file);
exportKey.setVisible(mIsSecret);
MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity);
addLinked.setVisible(mIsSecret);
MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint);
certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked);
MenuItem certifyFingerprintWord = menu.findItem(R.id.menu_key_view_certify_fingerprint_word);
@@ -459,13 +471,13 @@ public class ViewKeyActivity extends BaseNfcActivity implements
return;
}
if (resultCode != Activity.RESULT_OK) {
return;
}
switch (requestCode) {
case REQUEST_QR_FINGERPRINT: {
if (resultCode != Activity.RESULT_OK) {
return;
}
// If there is an EXTRA_RESULT, that's an error. Just show it.
if (data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
@@ -487,11 +499,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
case REQUEST_BACKUP: {
if (resultCode != Activity.RESULT_OK) {
return;
}
backupToFile();
return;
}
case REQUEST_CERTIFY: {
if (resultCode != Activity.RESULT_OK) {
return;
}
if (data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(this).show();
@@ -500,6 +520,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
case REQUEST_DELETE: {
if (resultCode != Activity.RESULT_OK) {
return;
}
setResult(RESULT_OK, data);
finish();
return;
@@ -552,7 +576,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements
finish();
}
}, R.string.snack_yubikey_view).show();
// and if it's not found, offer import
} catch (PgpKeyNotFoundException e) {
Notify.create(this, R.string.snack_yubi_other, Notify.LENGTH_LONG,
@@ -986,4 +1009,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return true;
}
}

View File

@@ -133,7 +133,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
default:

View File

@@ -18,29 +18,42 @@
package org.sufficientlysecure.keychain.ui;
import java.io.IOException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.widget.CardView;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.*;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
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.util.ContactHelper;
import org.sufficientlysecure.keychain.util.Log;
@@ -64,6 +77,7 @@ public class ViewKeyFragment extends LoaderFragment implements
private static final int LOADER_ID_UNIFIED = 0;
private static final int LOADER_ID_USER_IDS = 1;
private static final int LOADER_ID_LINKED_CONTACT = 2;
private static final int LOADER_ID_LINKED_IDS = 3;
private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID
= "loader_linked_contact_master_key_id";
@@ -71,8 +85,13 @@ public class ViewKeyFragment extends LoaderFragment implements
= "loader_linked_contact_is_secret";
private UserIdsAdapter mUserIdsAdapter;
private LinkedIdsAdapter mLinkedIdsAdapter;
private Uri mDataUri;
private ListView mLinkedIds;
private CardView mLinkedIdsCard;
private byte[] mFingerprint;
private TextView mLinkedIdsExpander;
/**
* Creates new instance of this fragment
@@ -93,6 +112,11 @@ public class ViewKeyFragment extends LoaderFragment implements
View view = inflater.inflate(R.layout.view_key_fragment, getContainer());
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
mLinkedIdsCard = (CardView) view.findViewById(R.id.card_linked_ids);
mLinkedIds = (ListView) view.findViewById(R.id.view_key_linked_ids);
mLinkedIdsExpander = (TextView) view.findViewById(R.id.view_key_linked_ids_expander);
mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@@ -100,6 +124,12 @@ public class ViewKeyFragment extends LoaderFragment implements
showUserIdInfo(position);
}
});
mLinkedIds.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
showLinkedId(position);
}
});
mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card);
mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout);
@@ -109,6 +139,47 @@ public class ViewKeyFragment extends LoaderFragment implements
return root;
}
private void showLinkedId(final int position) {
final LinkedIdViewFragment frag;
try {
frag = mLinkedIdsAdapter.getLinkedIdFragment(mDataUri, position, mFingerprint);
} catch (IOException e) {
e.printStackTrace();
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Transition trans = TransitionInflater.from(getActivity())
.inflateTransition(R.transition.linked_id_card_trans);
// setSharedElementReturnTransition(trans);
setExitTransition(new Fade());
frag.setSharedElementEnterTransition(trans);
}
getFragmentManager().beginTransaction()
.add(R.id.view_key_fragment, frag)
.hide(frag)
.commit();
frag.setOnIdentityLoadedListener(new OnIdentityLoadedListener() {
@Override
public void onIdentityLoaded() {
new Handler().post(new Runnable() {
@Override
public void run() {
getFragmentManager().beginTransaction()
.show(frag)
.addSharedElement(mLinkedIdsCard, "card_linked_ids")
.remove(ViewKeyFragment.this)
.addToBackStack("linked_id")
.commit();
}
});
}
});
}
private void showUserIdInfo(final int position) {
if (!mIsSecret) {
final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position);
@@ -129,8 +200,6 @@ public class ViewKeyFragment extends LoaderFragment implements
* Hides card if no linked system contact exists. Sets name, picture
* and onClickListener for the linked system contact's layout.
* In the case of a secret key, "me" (own profile) contact details are loaded.
*
* @param contactId
*/
private void loadLinkedSystemContact(final long contactId) {
// contact doesn't exist, stop
@@ -188,7 +257,6 @@ public class ViewKeyFragment extends LoaderFragment implements
* ContactsContract.Contact table)
*
* @param contactId _ID for row in ContactsContract.Contacts table
* @param context
*/
private void launchContactActivity(final long contactId, Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -225,12 +293,17 @@ public class ViewKeyFragment extends LoaderFragment implements
};
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[] RAWCONTACT_PROJECTION = {
@@ -246,7 +319,6 @@ public class ViewKeyFragment extends LoaderFragment implements
// Prepare the loaders. Either re-connect with an existing ones,
// or start new ones.
// TODO Is this loader the same as the one in the activity?
getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this);
}
@@ -259,8 +331,14 @@ public class ViewKeyFragment extends LoaderFragment implements
Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri);
return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null);
}
case LOADER_ID_USER_IDS:
case LOADER_ID_USER_IDS: {
return UserIdsAdapter.createLoader(getActivity(), mDataUri);
}
case LOADER_ID_LINKED_IDS: {
return LinkedIdsAdapter.createLoader(getActivity(), mDataUri);
}
//we need a separate loader for linked contact to ensure refreshing on verification
case LOADER_ID_LINKED_CONTACT: {
@@ -310,12 +388,18 @@ public class ViewKeyFragment extends LoaderFragment implements
if (data.moveToFirst()) {
mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
mFingerprint = data.getBlob(INDEX_FINGERPRINT);
// load user ids after we know if it's a secret key
mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null);
mUserIds.setAdapter(mUserIdsAdapter);
getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
mLinkedIdsAdapter =
new LinkedIdsAdapter(getActivity(), null, 0, mIsSecret, mLinkedIdsExpander);
mLinkedIds.setAdapter(mLinkedIdsAdapter);
getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this);
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
// we need to load linked contact here to prevent lag introduced by loader
// for the linked contact
@@ -340,6 +424,12 @@ public class ViewKeyFragment extends LoaderFragment implements
break;
}
case LOADER_ID_LINKED_IDS: {
mLinkedIdsAdapter.swapCursor(data);
mLinkedIdsCard.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.VISIBLE : View.GONE);
break;
}
case LOADER_ID_LINKED_CONTACT: {
if (data.moveToFirst()) {// if we have a linked contact
long contactId = data.getLong(INDEX_CONTACT_ID);
@@ -363,6 +453,11 @@ public class ViewKeyFragment extends LoaderFragment implements
mUserIdsAdapter.swapCursor(null);
break;
}
case LOADER_ID_LINKED_IDS: {
mLinkedIdsCard.setVisibility(View.GONE);
mLinkedIdsAdapter.swapCursor(null);
break;
}
}
}

View File

@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Date;
import android.content.Context;
import android.database.Cursor;
@@ -193,9 +194,9 @@ public class KeyAdapter extends CursorAdapter {
String dateTime = DateUtils.formatDateTime(context,
item.mCreation.getTime(),
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH);
mCreationDate.setText(context.getString(R.string.label_key_created,
dateTime));
mCreationDate.setTextColor(textColor);

View File

@@ -0,0 +1,233 @@
/*
* Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2015 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.adapter;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.content.CursorLoader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.UriAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker;
import org.sufficientlysecure.keychain.util.FilterCursorWrapper;
import java.io.IOException;
import java.util.WeakHashMap;
public class LinkedIdsAdapter extends UserAttributesAdapter {
private final boolean mIsSecret;
protected LayoutInflater mInflater;
WeakHashMap<Integer,UriAttribute> mLinkedIdentityCache = new WeakHashMap<>();
private Cursor mUnfilteredCursor;
private TextView mExpander;
public LinkedIdsAdapter(Context context, Cursor c, int flags,
boolean isSecret, TextView expander) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
mIsSecret = isSecret;
if (expander != null) {
expander.setVisibility(View.GONE);
/* don't show an expander (maybe in some sort of advanced view?)
mExpander = expander;
mExpander.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showUnfiltered();
}
});
*/
}
}
@Override
public Cursor swapCursor(Cursor cursor) {
if (cursor == null) {
mUnfilteredCursor = null;
return super.swapCursor(null);
}
mUnfilteredCursor = cursor;
FilterCursorWrapper filteredCursor = new FilterCursorWrapper(cursor) {
@Override
public boolean isVisible(Cursor cursor) {
UriAttribute id = getItemAtPosition(cursor);
return id instanceof LinkedAttribute;
}
};
if (mExpander != null) {
int hidden = filteredCursor.getHiddenCount();
if (hidden == 0) {
mExpander.setVisibility(View.GONE);
} else {
mExpander.setVisibility(View.VISIBLE);
mExpander.setText(mContext.getResources().getQuantityString(
R.plurals.linked_id_expand, hidden));
}
}
return super.swapCursor(filteredCursor);
}
private void showUnfiltered() {
mExpander.setVisibility(View.GONE);
super.swapCursor(mUnfilteredCursor);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder holder = (ViewHolder) view.getTag();
if (!mIsSecret) {
int isVerified = cursor.getInt(INDEX_VERIFIED);
switch (isVerified) {
case Certs.VERIFIED_SECRET:
KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
case Certs.VERIFIED_SELF:
KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
default:
KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
break;
}
}
UriAttribute id = getItemAtPosition(cursor);
holder.setData(mContext, id);
}
public UriAttribute getItemAtPosition(Cursor cursor) {
int rank = cursor.getInt(INDEX_RANK);
Log.d(Constants.TAG, "requested rank: " + rank);
UriAttribute ret = mLinkedIdentityCache.get(rank);
if (ret != null) {
Log.d(Constants.TAG, "cached!");
return ret;
}
Log.d(Constants.TAG, "not cached!");
try {
byte[] data = cursor.getBlob(INDEX_ATTRIBUTE_DATA);
ret = LinkedAttribute.fromAttributeData(data);
mLinkedIdentityCache.put(rank, ret);
return ret;
} catch (IOException e) {
Log.e(Constants.TAG, "could not read linked identity subpacket data", e);
return null;
}
}
@Override
public UriAttribute getItem(int position) {
Cursor cursor = getCursor();
cursor.moveToPosition(position);
return getItemAtPosition(cursor);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = mInflater.inflate(R.layout.linked_id_item, null);
ViewHolder holder = new ViewHolder(v);
v.setTag(holder);
return v;
}
// don't show revoked user ids, irrelevant for average users
public static final String LINKED_IDS_WHERE = UserPackets.IS_REVOKED + " = 0";
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
UserIdsAdapter.USER_PACKETS_PROJECTION, LINKED_IDS_WHERE, null, null);
}
public LinkedIdViewFragment getLinkedIdFragment(Uri baseUri,
int position, byte[] fingerprint) throws IOException {
Cursor c = getCursor();
c.moveToPosition(position);
int rank = c.getInt(UserIdsAdapter.INDEX_RANK);
Uri dataUri = UserPackets.buildLinkedIdsUri(baseUri);
return LinkedIdViewFragment.newInstance(dataUri, rank, mIsSecret, fingerprint);
}
public static class ViewHolder {
final public ImageView vVerified;
final public ImageView vIcon;
final public TextView vTitle;
final public TextView vComment;
public ViewHolder(View view) {
vVerified = (ImageView) view.findViewById(R.id.linked_id_certified_icon);
vIcon = (ImageView) view.findViewById(R.id.linked_id_type_icon);
vTitle = (TextView) view.findViewById(R.id.linked_id_title);
vComment = (TextView) view.findViewById(R.id.linked_id_comment);
}
public void setData(Context context, UriAttribute id) {
vTitle.setText(id.getDisplayTitle(context));
String comment = id.getDisplayComment(context);
if (comment != null) {
vComment.setVisibility(View.VISIBLE);
vComment.setText(comment);
} else {
vComment.setVisibility(View.GONE);
}
vIcon.setImageResource(id.getDisplayIcon());
}
public void seekAttention() {
ObjectAnimator anim = SubtleAttentionSeeker.tintText(vComment, 1000);
anim.setStartDelay(200);
anim.start();
}
}
}

View File

@@ -0,0 +1,57 @@
package org.sufficientlysecure.keychain.ui.adapter;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.content.CursorLoader;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public class LinkedIdsCertAdapter extends CursorAdapter {
public static final String[] USER_CERTS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.TYPE,
UserPackets.USER_ID,
UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK,
UserPackets.VERIFIED,
UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED
};
protected static final int INDEX_ID = 0;
protected static final int INDEX_TYPE = 1;
protected static final int INDEX_USER_ID = 2;
protected static final int INDEX_ATTRIBUTE_DATA = 3;
protected static final int INDEX_RANK = 4;
protected static final int INDEX_VERIFIED = 5;
protected static final int INDEX_IS_PRIMARY = 6;
protected static final int INDEX_IS_REVOKED = 7;
public LinkedIdsCertAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return null;
}
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
}

View File

@@ -171,7 +171,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
CertifyAction action = actions.get(keyId);
if (actions.get(keyId) == null) {
actions.put(keyId, new CertifyAction(keyId, uids));
actions.put(keyId, new CertifyAction(keyId, uids, null));
} else {
action.mUserIds.addAll(uids);
}

View File

@@ -137,9 +137,9 @@ abstract public class SelectKeyCursorAdapter extends CursorAdapter {
String dateTime = DateUtils.formatDateTime(context,
cursor.getLong(mIndexCreation) * 1000,
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH);
h.creation.setText(context.getString(R.string.label_key_created, dateTime));
h.creation.setVisibility(View.VISIBLE);
} else {

View File

@@ -8,22 +8,24 @@ import android.view.View;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public abstract class UserAttributesAdapter extends CursorAdapter {
public static final String[] USER_IDS_PROJECTION = new String[]{
public static final String[] USER_PACKETS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.TYPE,
UserPackets.USER_ID,
UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK,
UserPackets.VERIFIED,
UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED
};
protected static final int INDEX_ID = 0;
protected static final int INDEX_TYPE = 1;
protected static final int INDEX_USER_ID = 2;
protected static final int INDEX_RANK = 3;
protected static final int INDEX_VERIFIED = 4;
protected static final int INDEX_IS_PRIMARY = 5;
protected static final int INDEX_IS_REVOKED = 6;
public static final int INDEX_ID = 0;
public static final int INDEX_TYPE = 1;
public static final int INDEX_USER_ID = 2;
public static final int INDEX_ATTRIBUTE_DATA = 3;
public static final int INDEX_RANK = 4;
public static final int INDEX_VERIFIED = 5;
public static final int INDEX_IS_PRIMARY = 6;
public static final int INDEX_IS_REVOKED = 7;
public UserAttributesAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@@ -46,4 +48,5 @@ public abstract class UserAttributesAdapter extends CursorAdapter {
mCursor.moveToPosition(position);
return mCursor.getInt(INDEX_VERIFIED);
}
}

View File

@@ -188,7 +188,7 @@ public class UserIdsAdapter extends UserAttributesAdapter {
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildUserIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
UserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_WHERE, null, null);
UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null);
}
}

View File

@@ -18,9 +18,9 @@
package org.sufficientlysecure.keychain.ui.base;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
@@ -50,17 +50,21 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
* @see KeychainService
*
*/
abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult>
public abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult>
extends Fragment implements CryptoOperationHelper.Callback<T, S> {
final private CryptoOperationHelper<T, S> mOperationHelper;
public CryptoOperationFragment() {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing);
}
public CryptoOperationFragment(Integer initialProgressMsg) {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, initialProgressMsg);
}
public CryptoOperationFragment() {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing);
public CryptoOperationFragment(int id, Integer initialProgressMsg) {
mOperationHelper = new CryptoOperationHelper<>(id, this, this, initialProgressMsg);
}
@Override

View File

@@ -70,9 +70,11 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
// particular helper. a request code looks as follows:
// (id << 9) + (1<<8) + REQUEST_CODE_X
// that is, starting from LSB, there are 8 bits request code, 1
// fixed bit set, then 7 bit operator-id code. the first two
// summands are stored in the mId for easy operation.
private final int mId;
// fixed bit set, then 7 bit helper-id code. the first two
// summands are stored in the mHelperId for easy operation.
private final int mHelperId;
// bitmask for helperId is everything except the least 8 bits
public static final int HELPER_ID_BITMASK = ~0xff;
public static final int REQUEST_CODE_PASSPHRASE = 1;
public static final int REQUEST_CODE_NFC = 2;
@@ -92,7 +94,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
*/
public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback,
Integer progressMessageString) {
mId = (id << 9) + (1<<8);
mHelperId = (id << 9) + (1<<8);
mActivity = activity;
mUseFragment = false;
mCallback = callback;
@@ -103,7 +105,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
* if OperationHelper is being integrated into a fragment
*/
public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) {
mId = (id << 9) + (1<<8);
mHelperId = (id << 9) + (1<<8);
mFragment = fragment;
mUseFragment = true;
mProgressMessageResource = progressMessageString;
@@ -162,9 +164,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
protected void startActivityForResult(Intent intent, int requestCode) {
if (mUseFragment) {
mFragment.startActivityForResult(intent, mId + requestCode);
mFragment.startActivityForResult(intent, mHelperId + requestCode);
} else {
mActivity.startActivityForResult(intent, mId + requestCode);
mActivity.startActivityForResult(intent, mHelperId + requestCode);
}
}
@@ -176,13 +178,13 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(Constants.TAG, "received activity result in OperationHelper");
if ((requestCode & mId) != mId) {
if ((requestCode & HELPER_ID_BITMASK) != mHelperId) {
// this wasn't meant for us to handle
return false;
}
Log.d(Constants.TAG, "handling activity result in OperationHelper");
// filter out mId from requestCode
requestCode ^= mId;
// filter out mHelperId from requestCode
requestCode ^= mHelperId;
if (resultCode == Activity.RESULT_CANCELED) {
mCallback.onCryptoOperationCancelled();

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.resources.DnsResource;
public class LinkedIdCreateDnsStep1Fragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
EditText mEditDns;
/**
* Creates new instance of this fragment
*/
public static LinkedIdCreateDnsStep1Fragment newInstance() {
LinkedIdCreateDnsStep1Fragment frag = new LinkedIdCreateDnsStep1Fragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.linked_create_dns_fragment_step1, container, false);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String uri = mEditDns.getText().toString();
if (!checkUri(uri)) {
mEditDns.setError("Please enter a valid domain name!");
return;
}
String proofText = DnsResource.generateText(
mLinkedIdWizard.mFingerprint);
LinkedIdCreateDnsStep2Fragment frag =
LinkedIdCreateDnsStep2Fragment.newInstance(uri, proofText);
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mEditDns = (EditText) view.findViewById(R.id.linked_create_dns_domain);
mEditDns.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
String uri = editable.toString();
if (uri.length() > 0) {
if (checkUri(uri)) {
mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_ok, 0);
} else {
mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_bad, 0);
}
} else {
// remove drawable if email is empty
mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
});
return view;
}
private static boolean checkUri(String uri) {
return Patterns.DOMAIN_NAME.matcher(uri).matches();
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.linked.resources.DnsResource;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
public class LinkedIdCreateDnsStep2Fragment extends LinkedIdCreateFinalFragment {
private static final int REQUEST_CODE_OUTPUT = 0x00007007;
public static final String DOMAIN = "domain", TEXT = "text";
TextView mTextView;
String mResourceDomain;
String mResourceString;
public static LinkedIdCreateDnsStep2Fragment newInstance
(String uri, String proofText) {
LinkedIdCreateDnsStep2Fragment frag = new LinkedIdCreateDnsStep2Fragment();
Bundle args = new Bundle();
args.putString(DOMAIN, uri);
args.putString(TEXT, proofText);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mResourceDomain = getArguments().getString(DOMAIN);
mResourceString = getArguments().getString(TEXT);
}
@Override
protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.linked_create_dns_fragment_step2, container, false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSend();
}
});
view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofToClipboard();
}
});
mTextView = (TextView) view.findViewById(R.id.linked_create_dns_text);
mTextView.setText(mResourceString);
}
return view;
}
@Override
LinkedTokenResource getResource(OperationLog log) {
return DnsResource.createNew(mResourceDomain);
}
private void proofSend () {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
sendIntent.setType("text/plain");
startActivity(sendIntent);
}
private void proofToClipboard() {
Activity activity = getActivity();
if (activity == null) {
return;
}
ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipMan == null) {
Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR).show();
return;
}
ClipData clip = ClipData.newPlainText(Constants.CLIPBOARD_LABEL, mResourceString);
clipMan.setPrimaryClip(clip);
Notify.create(getActivity(), R.string.linked_text_clipboard, Notify.Style.OK).show();
}
private void saveFile(Uri uri) {
try {
PrintWriter out =
new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
out.print(mResourceString);
if (out.checkError()) {
Notify.create(getActivity(), "Error writing file!", Style.ERROR).show();
}
} catch (FileNotFoundException e) {
Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show();
e.printStackTrace();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
// For saving a file
case REQUEST_CODE_OUTPUT:
if (data == null) {
return;
}
Uri uri = data.getData();
saveFile(uri);
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@@ -0,0 +1,221 @@
package org.sufficientlysecure.keychain.ui.linked;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.Notify;
public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragment {
protected LinkedIdWizard mLinkedIdWizard;
private ImageView mVerifyImage;
private TextView mVerifyStatus;
private ViewAnimator mVerifyAnimator;
// This is a resource, set AFTER it has been verified
LinkedTokenResource mVerifiedResource = null;
private ViewAnimator mVerifyButtonAnimator;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = newView(inflater, container, savedInstanceState);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
cryptoOperation();
}
});
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mVerifyAnimator = (ViewAnimator) view.findViewById(R.id.verify_progress);
mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
mVerifyButtonAnimator = (ViewAnimator) view.findViewById(R.id.verify_buttons);
view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofVerify();
}
});
view.findViewById(R.id.button_retry).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofVerify();
}
});
setVerifyProgress(false, null);
mVerifyStatus.setText(R.string.linked_verify_pending);
return view;
}
abstract LinkedTokenResource getResource(OperationLog log);
private void setVerifyProgress(boolean on, Boolean success) {
if (success == null) {
mVerifyStatus.setText(R.string.linked_verifying);
displayButton(on ? 2 : 0);
} else if (success) {
mVerifyStatus.setText(R.string.linked_verify_success);
mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
PorterDuff.Mode.SRC_IN);
displayButton(2);
} else {
mVerifyStatus.setText(R.string.linked_verify_error);
mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
PorterDuff.Mode.SRC_IN);
displayButton(1);
}
mVerifyAnimator.setDisplayedChild(on ? 1 : 0);
}
public void displayButton(int button) {
if (mVerifyButtonAnimator.getDisplayedChild() == button) {
return;
}
mVerifyButtonAnimator.setDisplayedChild(button);
}
protected void proofVerify() {
setVerifyProgress(true, null);
new AsyncTask<Void,Void,LinkedVerifyResult>() {
@Override
protected LinkedVerifyResult doInBackground(Void... params) {
long timer = System.currentTimeMillis();
OperationLog log = new OperationLog();
LinkedTokenResource resource = getResource(log);
if (resource == null) {
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
LinkedVerifyResult result = resource.verify(getActivity(), mLinkedIdWizard.mFingerprint);
// ux flow: this operation should take at last a second
timer = System.currentTimeMillis() -timer;
if (timer < 1000) try {
Thread.sleep(1000 -timer);
} catch (InterruptedException e) {
// never mind
}
if (result.success()) {
mVerifiedResource = resource;
}
return result;
}
@Override
protected void onPostExecute(LinkedVerifyResult result) {
super.onPostExecute(result);
if (result.success()) {
setVerifyProgress(false, true);
} else {
setVerifyProgress(false, false);
// on error, show error message
result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this);
}
}
}.execute();
}
@Override
protected void cryptoOperation() {
if (mVerifiedResource == null) {
Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR)
.show(LinkedIdCreateFinalFragment.this);
return;
}
super.cryptoOperation();
}
@Override
protected void cryptoOperation(CryptoInputParcel cryptoInput) {
if (mVerifiedResource == null) {
Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR)
.show(LinkedIdCreateFinalFragment.this);
return;
}
super.cryptoOperation(cryptoInput);
}
@Nullable
@Override
public Parcelable createOperationInput() {
SaveKeyringParcel skp =
new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint);
WrappedUserAttribute ua =
LinkedAttribute.fromResource(mVerifiedResource).toUserAttribute();
skp.mAddUserAttribute.add(ua);
return skp;
}
@Override
public void onCryptoOperationSuccess(OperationResult result) {
// if bad -> display here!
if (!result.success()) {
result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this);
return;
}
getActivity().finish();
}
@Override
public void onCryptoOperationError(OperationResult result) {
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.util.Notify;
public class LinkedIdCreateGithubStep1Fragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
EditText mEditHandle;
public static LinkedIdCreateGithubStep1Fragment newInstance() {
LinkedIdCreateGithubStep1Fragment frag = new LinkedIdCreateGithubStep1Fragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.linked_create_github_fragment_step1, container, false);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final String handle = mEditHandle.getText().toString();
new AsyncTask<Void,Void,Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
return true; // return checkHandle(handle);
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result == null) {
Notify.create(getActivity(),
"Connection error while checking username!", Notify.Style.ERROR).show();
return;
}
if (!result) {
Notify.create(getActivity(),
"This handle does not exist on Github!", Notify.Style.ERROR).show();
return;
}
LinkedIdCreateGithubStep2Fragment frag =
LinkedIdCreateGithubStep2Fragment.newInstance(handle);
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
}.execute();
}
});
view.findViewById(R.id.back_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mEditHandle = (EditText) view.findViewById(R.id.linked_create_github_handle);
return view;
}
/* not used at this point, too much hassle
private static Boolean checkHandle(String handle) {
try {
HttpURLConnection nection =
(HttpURLConnection) new URL("https://api.github.com/" + handle).openConnection();
nection.setRequestMethod("HEAD");
return nection.getResponseCode() == 200;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
*/
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.linked.resources.GithubResource;
public class LinkedIdCreateGithubStep2Fragment extends LinkedIdCreateFinalFragment {
public static final String ARG_HANDLE = "handle";
String mResourceHandle;
String mResourceString;
public static LinkedIdCreateGithubStep2Fragment newInstance
(String handle) {
LinkedIdCreateGithubStep2Fragment frag = new LinkedIdCreateGithubStep2Fragment();
Bundle args = new Bundle();
args.putString(ARG_HANDLE, handle);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mResourceString =
GithubResource.generate(getActivity(), mLinkedIdWizard.mFingerprint);
mResourceHandle = getArguments().getString(ARG_HANDLE);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSend();
}
});
view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofShare();
}
});
}
return view;
}
@Override
LinkedTokenResource getResource(OperationLog log) {
return GithubResource.searchInGithubStream(getActivity(), mResourceHandle, mResourceString, log);
}
@Override
protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.linked_create_github_fragment_step2, container, false);
}
private void proofShare() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
sendIntent.setType("text/plain");
startActivity(sendIntent);
}
private void proofSend() {
Uri.Builder builder = Uri.parse("https://gist.github.com/").buildUpon();
builder.appendQueryParameter("text", mResourceString);
Uri uri = builder.build();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
getActivity().startActivity(intent);
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
public class LinkedIdCreateHttpsStep1Fragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
EditText mEditUri;
public static LinkedIdCreateHttpsStep1Fragment newInstance() {
LinkedIdCreateHttpsStep1Fragment frag = new LinkedIdCreateHttpsStep1Fragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String uri = "https://" + mEditUri.getText();
if (!checkUri(uri)) {
return;
}
String proofText = GenericHttpsResource.generateText(getActivity(),
mLinkedIdWizard.mFingerprint);
LinkedIdCreateHttpsStep2Fragment frag =
LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofText);
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
mEditUri.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
String uri = "https://" + editable;
if (uri.length() > 0) {
if (checkUri(uri)) {
mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_ok, 0);
} else {
mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_bad, 0);
}
} else {
// remove drawable if email is empty
mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
});
// mEditUri.setText("mugenguild.com/pgpkey.txt");
return view;
}
private static boolean checkUri(String uri) {
return Patterns.WEB_URL.matcher(uri).matches();
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FileHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment {
private static final int REQUEST_CODE_OUTPUT = 0x00007007;
public static final String ARG_URI = "uri", ARG_TEXT = "text";
EditText mEditUri;
URI mResourceUri;
String mResourceString;
public static LinkedIdCreateHttpsStep2Fragment newInstance
(String uri, String proofText) {
LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment();
Bundle args = new Bundle();
args.putString(ARG_URI, uri);
args.putString(ARG_TEXT, proofText);
frag.setArguments(args);
return frag;
}
@Override
GenericHttpsResource getResource(OperationLog log) {
return GenericHttpsResource.createNew(mResourceUri);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
mResourceUri = new URI(getArguments().getString(ARG_URI));
} catch (URISyntaxException e) {
e.printStackTrace();
getActivity().finish();
}
mResourceString = getArguments().getString(ARG_TEXT);
}
protected View newView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSend();
}
});
view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSave();
}
});
mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
mEditUri.setText(mResourceUri.toString());
}
return view;
}
private void proofSend () {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
sendIntent.setType("text/plain");
startActivity(sendIntent);
}
private void proofSave () {
String state = Environment.getExternalStorageState();
if (!Environment.MEDIA_MOUNTED.equals(state)) {
Notify.create(getActivity(), "External storage not available!", Style.ERROR);
return;
}
String targetName = "pgpkey.txt";
FileHelper.saveDocument(this,
targetName, Uri.fromFile(new File(Constants.Path.APP_DIR, targetName)),
"text/plain", R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to,
REQUEST_CODE_OUTPUT);
}
private void saveFile(Uri uri) {
try {
PrintWriter out =
new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
out.print(mResourceString);
if (out.checkError()) {
Notify.create(getActivity(), "Error writing file!", Style.ERROR).show();
}
} catch (FileNotFoundException e) {
Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
// For saving a file
case REQUEST_CODE_OUTPUT:
if (data == null) {
return;
}
Uri uri = data.getData();
saveFile(uri);
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.util.Notify;
public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
EditText mEditHandle;
public static LinkedIdCreateTwitterStep1Fragment newInstance() {
LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final String handle = mEditHandle.getText().toString();
if ("".equals(handle)) {
mEditHandle.setError("Please input a Twitter handle!");
return;
}
new AsyncTask<Void,Void,Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
return true;
// return checkHandle(handle);
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result == null) {
Notify.create(getActivity(),
"Connection error while checking username!",
Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this);
return;
}
if (!result) {
Notify.create(getActivity(),
"This handle does not exist on Twitter!",
Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this);
return;
}
LinkedIdCreateTwitterStep2Fragment frag =
LinkedIdCreateTwitterStep2Fragment.newInstance(handle);
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
}.execute();
}
});
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mEditHandle = (EditText) view.findViewById(R.id.linked_create_twitter_handle);
return view;
}
/* not used at this point, too many problems
private static Boolean checkHandle(String handle) {
try {
HttpURLConnection nection =
(HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
nection.setRequestMethod("HEAD");
nection.setRequestProperty("User-Agent", "OpenKeychain");
return nection.getResponseCode() == 200;
} catch (IOException e) {
return null;
}
}
*/
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.linked.resources.TwitterResource;
public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragment {
public static final String ARG_HANDLE = "handle";
String mResourceHandle;
String mResourceString;
public static LinkedIdCreateTwitterStep2Fragment newInstance
(String handle) {
LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment();
Bundle args = new Bundle();
args.putString(ARG_HANDLE, handle);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mResourceString =
TwitterResource.generate(mLinkedIdWizard.mFingerprint);
mResourceHandle = getArguments().getString(ARG_HANDLE);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSend();
}
});
view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofShare();
}
});
((TextView) view.findViewById(R.id.linked_tweet_published)).setText(
Html.fromHtml(getString(R.string.linked_create_twitter_2_3, mResourceHandle))
);
}
return view;
}
@Override
LinkedTokenResource getResource(OperationLog log) {
return TwitterResource.searchInTwitterStream(getActivity(),
mResourceHandle, mResourceString, log);
}
@Override
protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.linked_create_twitter_fragment_step2, container, false);
}
private void proofShare() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
sendIntent.setType("text/plain");
startActivity(sendIntent);
}
private void proofSend() {
Uri.Builder builder = Uri.parse("https://twitter.com/intent/tweet").buildUpon();
builder.appendQueryParameter("text", mResourceString);
Uri uri = builder.build();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
getActivity().startActivity(intent);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.sufficientlysecure.keychain.R;
public class LinkedIdSelectFragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
/**
* Creates new instance of this fragment
*/
public static LinkedIdSelectFragment newInstance() {
LinkedIdSelectFragment frag = new LinkedIdSelectFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.linked_select_fragment, container, false);
view.findViewById(R.id.linked_create_https_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateHttpsStep1Fragment frag =
LinkedIdCreateHttpsStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.linked_create_dns_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateDnsStep1Fragment frag =
LinkedIdCreateDnsStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.linked_create_twitter_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateTwitterStep1Fragment frag =
LinkedIdCreateTwitterStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.linked_create_github_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateGithubStep1Fragment frag =
LinkedIdCreateGithubStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
}

View File

@@ -0,0 +1,558 @@
package org.sufficientlysecure.keychain.ui.linked;
import java.io.IOException;
import java.util.Collections;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.key;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.LinkedResource;
import org.sufficientlysecure.keychain.linked.UriAttribute;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.ViewHolder.VerifyState;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker;
import org.sufficientlysecure.keychain.ui.widget.CertListWidget;
import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner;
import org.sufficientlysecure.keychain.util.Log;
public class LinkedIdViewFragment extends CryptoOperationFragment implements
LoaderManager.LoaderCallbacks<Cursor>, OnBackStackChangedListener {
private static final String ARG_DATA_URI = "data_uri";
private static final String ARG_LID_RANK = "rank";
private static final String ARG_IS_SECRET = "verified";
private static final String ARG_FINGERPRINT = "fingerprint";
private static final int LOADER_ID_LINKED_ID = 1;
private UriAttribute mLinkedId;
private LinkedTokenResource mLinkedResource;
private boolean mIsSecret;
private Context mContext;
private byte[] mFingerprint;
private AsyncTask mInProgress;
private Uri mDataUri;
private ViewHolder mViewHolder;
private int mLidRank;
private OnIdentityLoadedListener mIdLoadedListener;
private long mCertifyKeyId;
public static LinkedIdViewFragment newInstance(Uri dataUri, int rank,
boolean isSecret, byte[] fingerprint) throws IOException {
LinkedIdViewFragment frag = new LinkedIdViewFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri);
args.putInt(ARG_LID_RANK, rank);
args.putBoolean(ARG_IS_SECRET, isSecret);
args.putByteArray(ARG_FINGERPRINT, fingerprint);
frag.setArguments(args);
return frag;
}
public LinkedIdViewFragment() {
// IMPORTANT: the id must be unique in the ViewKeyActivity CryptoOperationHelper id namespace!
// no initial progress message -> we handle progress ourselves!
super(5, null);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
mDataUri = args.getParcelable(ARG_DATA_URI);
mLidRank = args.getInt(ARG_LID_RANK);
mIsSecret = args.getBoolean(ARG_IS_SECRET);
mFingerprint = args.getByteArray(ARG_FINGERPRINT);
mContext = getActivity();
getLoaderManager().initLoader(LOADER_ID_LINKED_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID_LINKED_ID:
return new CursorLoader(getActivity(), mDataUri,
UserIdsAdapter.USER_PACKETS_PROJECTION,
Tables.USER_PACKETS + "." + UserPackets.RANK
+ " = " + Integer.toString(mLidRank), null, null);
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
switch (loader.getId()) {
case LOADER_ID_LINKED_ID:
// Nothing to load means break if we are *expected* to load
if (!cursor.moveToFirst()) {
if (mIdLoadedListener != null) {
Notify.create(getActivity(), "Error loading identity!",
Notify.LENGTH_LONG, Style.ERROR).show();
finishFragment();
}
// Or just ignore, this is probably some intermediate state during certify
break;
}
try {
int certStatus = cursor.getInt(UserIdsAdapter.INDEX_VERIFIED);
byte[] data = cursor.getBlob(UserIdsAdapter.INDEX_ATTRIBUTE_DATA);
UriAttribute linkedId = LinkedAttribute.fromAttributeData(data);
loadIdentity(linkedId, certStatus);
if (mIdLoadedListener != null) {
mIdLoadedListener.onIdentityLoaded();
mIdLoadedListener = null;
}
} catch (IOException e) {
Log.e(Constants.TAG, "error parsing identity", e);
Notify.create(getActivity(), "Error parsing identity!",
Notify.LENGTH_LONG, Style.ERROR).show();
finishFragment();
}
break;
}
}
public void finishFragment() {
new Handler().post(new Runnable() {
@Override
public void run() {
FragmentManager manager = getFragmentManager();
manager.removeOnBackStackChangedListener(LinkedIdViewFragment.this);
manager.popBackStack("linked_id", FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
});
}
public interface OnIdentityLoadedListener {
void onIdentityLoaded();
}
public void setOnIdentityLoadedListener(OnIdentityLoadedListener listener) {
mIdLoadedListener = listener;
}
private void loadIdentity(UriAttribute linkedId, int certStatus) {
mLinkedId = linkedId;
if (mLinkedId instanceof LinkedAttribute) {
LinkedResource res = ((LinkedAttribute) mLinkedId).mResource;
mLinkedResource = (LinkedTokenResource) res;
}
if (!mIsSecret) {
switch (certStatus) {
case Certs.VERIFIED_SECRET:
KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
case Certs.VERIFIED_SELF:
KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
default:
KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
break;
}
}
mViewHolder.mLinkedIdHolder.setData(mContext, mLinkedId);
setShowVerifying(false);
// no resource, nothing further we can do…
if (mLinkedResource == null) {
mViewHolder.vButtonView.setVisibility(View.GONE);
mViewHolder.vButtonVerify.setVisibility(View.GONE);
return;
}
if (mLinkedResource.isViewable()) {
mViewHolder.vButtonView.setVisibility(View.VISIBLE);
mViewHolder.vButtonView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = mLinkedResource.getViewIntent();
if (intent == null) {
return;
}
getActivity().startActivity(intent);
}
});
} else {
mViewHolder.vButtonView.setVisibility(View.GONE);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
static class ViewHolder {
private final View vButtonView;
private final ViewAnimator vVerifyingContainer;
private final ViewAnimator vItemCertified;
private final View vKeySpinnerContainer;
LinkedIdsAdapter.ViewHolder mLinkedIdHolder;
private ViewAnimator vButtonSwitcher;
private CertListWidget vLinkedCerts;
private CertifyKeySpinner vKeySpinner;
private final View vButtonVerify;
private final View vButtonRetry;
private final View vButtonConfirm;
private final ViewAnimator vProgress;
private final TextSwitcher vText;
ViewHolder(View root) {
vLinkedCerts = (CertListWidget) root.findViewById(R.id.linked_id_certs);
vKeySpinner = (CertifyKeySpinner) root.findViewById(R.id.cert_key_spinner);
vKeySpinnerContainer = root.findViewById(R.id.cert_key_spincontainer);
vButtonSwitcher = (ViewAnimator) root.findViewById(R.id.button_animator);
mLinkedIdHolder = new LinkedIdsAdapter.ViewHolder(root);
vButtonVerify = root.findViewById(R.id.button_verify);
vButtonRetry = root.findViewById(R.id.button_retry);
vButtonConfirm = root.findViewById(R.id.button_confirm);
vButtonView = root.findViewById(R.id.button_view);
vVerifyingContainer = (ViewAnimator) root.findViewById(R.id.linked_verify_container);
vItemCertified = (ViewAnimator) root.findViewById(R.id.linked_id_certified);
vProgress = (ViewAnimator) root.findViewById(R.id.linked_cert_progress);
vText = (TextSwitcher) root.findViewById(R.id.linked_cert_text);
}
enum VerifyState {
VERIFYING, VERIFY_OK, VERIFY_ERROR, CERTIFYING
}
void setVerifyingState(Context context, VerifyState state, boolean isSecret) {
switch (state) {
case VERIFYING:
vProgress.setDisplayedChild(0);
vText.setText(context.getString(R.string.linked_text_verifying));
vKeySpinnerContainer.setVisibility(View.GONE);
break;
case VERIFY_OK:
vProgress.setDisplayedChild(1);
if (!isSecret) {
showButton(2);
if (!vKeySpinner.isSingleEntry()) {
vKeySpinnerContainer.setVisibility(View.VISIBLE);
}
} else {
showButton(1);
vKeySpinnerContainer.setVisibility(View.GONE);
}
break;
case VERIFY_ERROR:
showButton(1);
vProgress.setDisplayedChild(2);
vText.setText(context.getString(R.string.linked_text_error));
vKeySpinnerContainer.setVisibility(View.GONE);
break;
case CERTIFYING:
vProgress.setDisplayedChild(0);
vText.setText(context.getString(R.string.linked_text_confirming));
vKeySpinnerContainer.setVisibility(View.GONE);
break;
}
}
void showVerifyingContainer(Context context, boolean show, boolean isSecret) {
if (vVerifyingContainer.getDisplayedChild() == (show ? 1 : 0)) {
return;
}
vVerifyingContainer.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down);
vVerifyingContainer.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down);
vVerifyingContainer.setDisplayedChild(show ? 1 : 0);
vItemCertified.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down);
vItemCertified.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down);
vItemCertified.setDisplayedChild(show || isSecret ? 1 : 0);
}
void showButton(int which) {
if (vButtonSwitcher.getDisplayedChild() == which) {
return;
}
vButtonSwitcher.setDisplayedChild(which);
}
}
private boolean mVerificationState = false;
/** Switches between the 'verifying' ui bit and certificate status. This method
* must behave correctly in all states, showing or hiding the appropriate views
* and cancelling pending operations where necessary.
*
* This method also handles back button functionality in combination with
* onBackStateChanged.
*/
void setShowVerifying(boolean show) {
if (!show) {
if (mInProgress != null) {
mInProgress.cancel(false);
mInProgress = null;
}
getFragmentManager().removeOnBackStackChangedListener(this);
new Handler().post(new Runnable() {
@Override
public void run() {
getFragmentManager().popBackStack("verification",
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
});
if (!mVerificationState) {
return;
}
mVerificationState = false;
mViewHolder.showButton(0);
mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE);
mViewHolder.showVerifyingContainer(mContext, false, mIsSecret);
return;
}
if (mVerificationState) {
return;
}
mVerificationState = true;
FragmentManager manager = getFragmentManager();
manager.beginTransaction().addToBackStack("verification").commit();
manager.executePendingTransactions();
manager.addOnBackStackChangedListener(this);
mViewHolder.showVerifyingContainer(mContext, true, mIsSecret);
}
@Override
public void onBackStackChanged() {
setShowVerifying(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.linked_id_view_fragment, null);
mViewHolder = new ViewHolder(root);
root.setTag(mViewHolder);
((ImageView) root.findViewById(R.id.status_icon_verified))
.setColorFilter(mContext.getResources().getColor(R.color.android_green_light),
PorterDuff.Mode.SRC_IN);
((ImageView) root.findViewById(R.id.status_icon_invalid))
.setColorFilter(mContext.getResources().getColor(R.color.android_red_light),
PorterDuff.Mode.SRC_IN);
mViewHolder.vButtonVerify.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
verifyResource();
}
});
mViewHolder.vButtonRetry.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
verifyResource();
}
});
mViewHolder.vButtonConfirm.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
initiateCertifying();
}
});
{
Bundle args = new Bundle();
args.putParcelable(CertListWidget.ARG_URI, Certs.buildLinkedIdCertsUri(mDataUri, mLidRank));
args.putBoolean(CertListWidget.ARG_IS_SECRET, mIsSecret);
getLoaderManager().initLoader(CertListWidget.LOADER_ID_LINKED_CERTS,
args, mViewHolder.vLinkedCerts);
}
return root;
}
void verifyResource() {
// only one at a time (no sync needed, mInProgress is only touched in ui thread)
if (mInProgress != null) {
return;
}
setShowVerifying(true);
mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE);
mViewHolder.setVerifyingState(mContext, VerifyState.VERIFYING, mIsSecret);
mInProgress = new AsyncTask<Void,Void,LinkedVerifyResult>() {
@Override
protected LinkedVerifyResult doInBackground(Void... params) {
long timer = System.currentTimeMillis();
LinkedVerifyResult result = mLinkedResource.verify(getActivity(), mFingerprint);
// ux flow: this operation should take at last a second
timer = System.currentTimeMillis() -timer;
if (timer < 1000) try {
Thread.sleep(1000 -timer);
} catch (InterruptedException e) {
// never mind
}
return result;
}
@Override
protected void onPostExecute(LinkedVerifyResult result) {
if (isCancelled()) {
return;
}
if (result.success()) {
mViewHolder.vText.setText(getString(mLinkedResource.getVerifiedText(mIsSecret)));
// hack to preserve bold text
((TextView) mViewHolder.vText.getCurrentView()).setText(
mLinkedResource.getVerifiedText(mIsSecret));
mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_OK, mIsSecret);
mViewHolder.mLinkedIdHolder.seekAttention();
} else {
mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_ERROR, mIsSecret);
result.createNotify(getActivity()).show();
}
mInProgress = null;
}
}.execute();
}
private void initiateCertifying() {
if (mIsSecret) {
return;
}
// get the user's passphrase for this key (if required)
mCertifyKeyId = mViewHolder.vKeySpinner.getSelectedKeyId();
if (mCertifyKeyId == key.none || mCertifyKeyId == key.symmetric) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SubtleAttentionSeeker.tintBackground(mViewHolder.vKeySpinnerContainer, 600).start();
} else {
Notify.create(getActivity(), R.string.select_key_to_certify, Style.ERROR).show();
}
return;
}
mViewHolder.setVerifyingState(mContext, VerifyState.CERTIFYING, false);
cryptoOperation();
}
@Override
public void onCryptoOperationCancelled() {
super.onCryptoOperationCancelled();
// go back to 'verified ok'
setShowVerifying(false);
}
@Nullable
@Override
public Parcelable createOperationInput() {
long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint);
CertifyAction action = new CertifyAction(masterKeyId, null,
Collections.singletonList(mLinkedId.toUserAttribute()));
// fill values for this action
CertifyActionsParcel parcel = new CertifyActionsParcel(mCertifyKeyId);
parcel.mCertifyActions.addAll(Collections.singletonList(action));
return parcel;
}
@Override
public void onCryptoOperationSuccess(OperationResult result) {
result.createNotify(getActivity()).show();
// no need to do anything else, we will get a loader refresh!
}
@Override
public void onCryptoOperationError(OperationResult result) {
result.createNotify(getActivity()).show();
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return true;
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.linked;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.util.Log;
public class LinkedIdWizard extends BaseActivity {
public static final int FRAG_ACTION_START = 0;
public static final int FRAG_ACTION_TO_RIGHT = 1;
public static final int FRAG_ACTION_TO_LEFT = 2;
long mMasterKeyId;
byte[] mFingerprint;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.title_linked_id_create));
try {
Uri uri = getIntent().getData();
uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(uri);
CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(uri);
if (!ring.hasAnySecret()) {
Log.e(Constants.TAG, "Linked Identities can only be added to secret keys!");
finish();
return;
}
mMasterKeyId = ring.extractOrGetMasterKeyId();
mFingerprint = ring.getFingerprint();
} catch (PgpKeyNotFoundException e) {
Log.e(Constants.TAG, "Invalid uri given, key does not exist!");
finish();
return;
}
// pass extras into fragment
LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance();
loadFragment(null, frag, FRAG_ACTION_START);
}
@Override
protected void initLayout() {
setContentView(R.layout.create_key_activity);
}
public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
hideKeyboard();
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
switch (action) {
case FRAG_ACTION_START:
transaction.setCustomAnimations(0, 0);
transaction.replace(R.id.create_key_fragment_container, fragment)
.commitAllowingStateLoss();
break;
case FRAG_ACTION_TO_LEFT:
getSupportFragmentManager().popBackStackImmediate();
break;
case FRAG_ACTION_TO_RIGHT:
transaction.setCustomAnimations(R.anim.frag_slide_in_from_right, R.anim.frag_slide_out_to_left,
R.anim.frag_slide_in_from_left, R.anim.frag_slide_out_to_right);
transaction.addToBackStack(null);
transaction.replace(R.id.create_key_fragment_container, fragment)
.commitAllowingStateLoss();
break;
}
// do it immediately!
getSupportFragmentManager().executePendingTransactions();
}
private void hideKeyboard() {
InputMethodManager inputManager = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
// check if no view has focus
View v = getCurrentFocus();
if (v == null)
return;
inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}

View File

@@ -269,6 +269,10 @@ public class KeyFormattingUtils {
return hexString;
}
public static long convertFingerprintToKeyId(byte[] fingerprint) {
return ByteBuffer.wrap(fingerprint, 12, 8).getLong();
}
/**
* Makes a human-readable version of a key ID, which is usually 64 bits: lower-case, no
* leading 0x, space-separated quartets (for keys whose length in hex is divisible by 4)

View File

@@ -17,13 +17,19 @@
package org.sufficientlysecure.keychain.ui.util;
import android.animation.AnimatorInflater;
import android.animation.ArgbEvaluator;
import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build.VERSION_CODES;
import android.view.View;
import org.sufficientlysecure.keychain.R;
@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH)
/** Simple animation helper for subtle attention seeker stuff.
*
@@ -36,6 +42,10 @@ public class SubtleAttentionSeeker {
}
public static ObjectAnimator tada(View view, float shakeFactor) {
return tada(view, shakeFactor, 1400);
}
public static ObjectAnimator tada(View view, float shakeFactor, int duration) {
PropertyValuesHolder pvhScaleX = PropertyValuesHolder.ofKeyframe(View.SCALE_X,
Keyframe.ofFloat(0f, 1f),
@@ -80,7 +90,19 @@ public class SubtleAttentionSeeker {
);
return ObjectAnimator.ofPropertyValuesHolder(view, pvhScaleX, pvhScaleY, pvhRotate).
setDuration(1400);
setDuration(duration);
}
@TargetApi(VERSION_CODES.LOLLIPOP)
public static ObjectAnimator tintBackground(View view, int duration) {
return ObjectAnimator.ofArgb(view, "backgroundColor",
0x00FF0000, 0x33FF0000, 0x00FF0000).setDuration(duration);
}
@TargetApi(VERSION_CODES.LOLLIPOP)
public static ObjectAnimator tintText(View view, int duration) {
return ObjectAnimator.ofArgb(view, "backgroundColor",
0x00FF7F00, 0x33FF7F00, 0x00FF7F00).setDuration(duration);
}
}

View File

@@ -0,0 +1,143 @@
package org.sufficientlysecure.keychain.ui.widget;
import java.util.Date;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.ocpsoft.prettytime.PrettyTime;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
public class CertListWidget extends ViewAnimator
implements LoaderManager.LoaderCallbacks<Cursor> {
public static final int LOADER_ID_LINKED_CERTS = 38572;
public static final String ARG_URI = "uri";
public static final String ARG_IS_SECRET = "is_secret";
// These are the rows that we will retrieve.
static final String[] CERTS_PROJECTION = new String[]{
KeychainContract.Certs._ID,
KeychainContract.Certs.MASTER_KEY_ID,
KeychainContract.Certs.VERIFIED,
KeychainContract.Certs.TYPE,
KeychainContract.Certs.RANK,
KeychainContract.Certs.KEY_ID_CERTIFIER,
KeychainContract.Certs.USER_ID,
KeychainContract.Certs.SIGNER_UID,
KeychainContract.Certs.CREATION
};
public static final int INDEX_MASTER_KEY_ID = 1;
public static final int INDEX_VERIFIED = 2;
public static final int INDEX_TYPE = 3;
public static final int INDEX_RANK = 4;
public static final int INDEX_KEY_ID_CERTIFIER = 5;
public static final int INDEX_USER_ID = 6;
public static final int INDEX_SIGNER_UID = 7;
public static final int INDEX_CREATION = 8;
private TextView vCollapsed;
private ListView vExpanded;
private View vExpandButton;
private boolean mIsSecret;
public CertListWidget(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View root = getRootView();
vCollapsed = (TextView) root.findViewById(R.id.cert_collapsed_list);
vExpanded = (ListView) root.findViewById(R.id.cert_expanded_list);
vExpandButton = root.findViewById(R.id.cert_expand_button);
// for now
vExpandButton.setVisibility(View.GONE);
vExpandButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
toggleExpanded();
}
});
// vExpanded.setAdapter(null);
}
void toggleExpanded() {
setDisplayedChild(getDisplayedChild() == 1 ? 0 : 1);
}
void setExpanded(boolean expanded) {
setDisplayedChild(expanded ? 1 : 0);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = args.getParcelable(ARG_URI);
mIsSecret = args.getBoolean(ARG_IS_SECRET, false);
return new CursorLoader(getContext(), uri,
CERTS_PROJECTION, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (data == null || !data.moveToFirst()) {
return;
}
// TODO support external certificates
Date userCert = null;
while (!data.isAfterLast()) {
int verified = data.getInt(INDEX_VERIFIED);
Date creation = new Date(data.getLong(INDEX_CREATION) * 1000);
if (verified == Certs.VERIFIED_SECRET) {
if (userCert == null || userCert.after(creation)) {
userCert = creation;
}
}
data.moveToNext();
}
if (userCert != null) {
PrettyTime format = new PrettyTime();
if (mIsSecret) {
vCollapsed.setText("You created this identity "
+ format.format(userCert) + ".");
} else {
vCollapsed.setText("You verified and confirmed this identity "
+ format.format(userCert) + ".");
}
} else {
vCollapsed.setText("This identity is not yet verified or confirmed.");
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
setVisibility(View.GONE);
}
}

View File

@@ -17,25 +17,24 @@
package org.sufficientlysecure.keychain.ui.widget;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.AttributeSet;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
public class CertifyKeySpinner extends KeySpinner {
private long mHiddenMasterKeyId = Constants.key.none;
private boolean mIsSingle;
public CertifyKeySpinner(Context context) {
super(context);
@@ -94,9 +93,11 @@ public class CertifyKeySpinner extends KeySpinner {
if (!data.isNull(mIndexHasCertify)) {
if (selection == -1) {
selection = data.getPosition() + 1;
mIsSingle = true;
} else {
// if selection is already set, we have more than one certify key!
// get back to "none"!
mIsSingle = false;
selection = 0;
}
}
@@ -106,6 +107,9 @@ public class CertifyKeySpinner extends KeySpinner {
}
}
public boolean isSingleEntry() {
return mIsSingle && getSelectedItemPosition() != 0;
}
@Override
boolean isItemEnabled(Cursor cursor) {
@@ -128,4 +132,10 @@ public class CertifyKeySpinner extends KeySpinner {
return true;
}
@Override
public @StringRes int getNoneString() {
return R.string.choice_select_cert;
}
}

View File

@@ -23,6 +23,7 @@ import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
@@ -34,6 +35,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
@@ -226,9 +228,11 @@ public abstract class KeySpinner extends AppCompatSpinner implements
return inner.getView(position -1, convertView, parent);
}
return convertView != null ? convertView :
View view = convertView != null ? convertView :
LayoutInflater.from(getContext()).inflate(
R.layout.keyspinner_item_none, parent, false);
((TextView) view.findViewById(R.id.keyspinner_key_name)).setText(getNoneString());
return view;
}
}
@@ -259,4 +263,9 @@ public abstract class KeySpinner extends AppCompatSpinner implements
bundle.putLong(ARG_KEY_ID, getSelectedKeyId());
return bundle;
}
public @StringRes int getNoneString() {
return R.string.cert_none;
}
}

View File

@@ -0,0 +1,45 @@
package org.sufficientlysecure.keychain.ui.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.*;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
public class PrefixedEditText extends EditText {
private String mPrefix;
private Rect mPrefixRect = new Rect();
public PrefixedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray style = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.PrefixedEditText, 0, 0);
mPrefix = style.getString(R.styleable.PrefixedEditText_prefix);
if (mPrefix == null) {
mPrefix = "";
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint());
}
@Override
public int getCompoundPaddingLeft() {
return super.getCompoundPaddingLeft() + mPrefixRect.width();
}
}