diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain')
16 files changed, 1623 insertions, 214 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditIdentitiesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditIdentitiesActivity.java new file mode 100644 index 000000000..6f65e9cef --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditIdentitiesActivity.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014-2015 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; + +import android.net.Uri; +import android.os.Bundle; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.util.Log; + +public class EditIdentitiesActivity extends BaseActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Uri dataUri = getIntent().getData(); + if (dataUri == null) { + Log.e(Constants.TAG, "Either a key Uri or EXTRA_SAVE_KEYRING_PARCEL is required!"); + finish(); + return; + } + + loadFragment(savedInstanceState, dataUri); + } + + @Override + protected void initLayout() { + setContentView(R.layout.edit_identities_activity); + } + + private void loadFragment(Bundle savedInstanceState, Uri dataUri) { + // 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; + } + + // Create an instance of the fragment + EditIdentitiesFragment mEditIdentitiesFragment; + mEditIdentitiesFragment = EditIdentitiesFragment.newInstance(dataUri); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.edit_key_fragment_container, mEditIdentitiesFragment) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditIdentitiesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditIdentitiesFragment.java new file mode 100644 index 000000000..cfd3f3cea --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditIdentitiesFragment.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2014-2015 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; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +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.AdapterView; +import android.widget.CheckBox; +import android.widget.ListView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.SingletonResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; +import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; +import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; +import org.sufficientlysecure.keychain.ui.adapter.UserIdsAddedAdapter; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.dialog.AddUserIdDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditUserIdDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Preferences; + +public class EditIdentitiesFragment extends Fragment + implements LoaderManager.LoaderCallbacks<Cursor> { + + public static final String ARG_DATA_URI = "uri"; + + private CheckBox mUploadKeyCheckbox; + private ListView mUserIdsList; + private ListView mUserIdsAddedList; + private View mAddUserId; + + private static final int LOADER_ID_USER_IDS = 0; + + private UserIdsAdapter mUserIdsAdapter; + private UserIdsAddedAdapter mUserIdsAddedAdapter; + + private Uri mDataUri; + + private SaveKeyringParcel mSaveKeyringParcel; + + private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditOpHelper; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; + + private String mPrimaryUserId; + + /** + * Creates new instance of this fragment + */ + public static EditIdentitiesFragment newInstance(Uri dataUri) { + EditIdentitiesFragment frag = new EditIdentitiesFragment(); + + Bundle args = new Bundle(); + args.putParcelable(ARG_DATA_URI, dataUri); + + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.edit_identities_fragment, null); + + mUploadKeyCheckbox = (CheckBox) view.findViewById(R.id.edit_identities_upload_checkbox); + mUserIdsList = (ListView) view.findViewById(R.id.edit_identities_user_ids); + mUserIdsAddedList = (ListView) view.findViewById(R.id.edit_identities_user_ids_added); + mAddUserId = view.findViewById(R.id.edit_identities_add_user_id); + + // If this is a debug build, don't upload by default + if (Constants.DEBUG) { + mUploadKeyCheckbox.setChecked(false); + } + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ((EditIdentitiesActivity) getActivity()).setFullScreenDialogDoneClose( + R.string.btn_save, + new OnClickListener() { + @Override + public void onClick(View v) { + editKey(); + } + }, new OnClickListener() { + @Override + public void onClick(View v) { + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); + } + }); + + Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); + if (dataUri == null) { + Log.e(Constants.TAG, "Either a key Uri is required!"); + getActivity().finish(); + return; + } + + initView(); + loadData(dataUri); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mEditOpHelper != null) { + mEditOpHelper.handleActivityResult(requestCode, resultCode, data); + } + if (mUploadOpHelper != null) { + mUploadOpHelper.handleActivityResult(requestCode, resultCode, data); + } + + super.onActivityResult(requestCode, resultCode, data); + } + + private void loadData(Uri dataUri) { + mDataUri = dataUri; + + Log.i(Constants.TAG, "mDataUri: " + mDataUri); + + // load the secret key ring. we do verify here that the passphrase is correct, so cached won't do + try { + Uri secretUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); + CachedPublicKeyRing keyRing = + new ProviderHelper(getActivity()).getCachedPublicKeyRing(secretUri); + long masterKeyId = keyRing.getMasterKeyId(); + + // check if this is a master secret key we can work with + switch (keyRing.getSecretKeyType(masterKeyId)) { + case GNU_DUMMY: + finishWithError(LogType.MSG_EK_ERROR_DUMMY); + return; + } + + mSaveKeyringParcel = new SaveKeyringParcel(masterKeyId, keyRing.getFingerprint()); + mPrimaryUserId = keyRing.getPrimaryUserIdWithFallback(); + + } catch (PgpKeyNotFoundException | NotFoundException e) { + finishWithError(LogType.MSG_EK_ERROR_NOT_FOUND); + return; + } + + // Prepare the loaders. Either re-connect with an existing ones, + // or start new ones. + getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, EditIdentitiesFragment.this); + + mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0); + mUserIdsAdapter.setEditMode(mSaveKeyringParcel); + mUserIdsList.setAdapter(mUserIdsAdapter); + + // TODO: SaveParcel from savedInstance?! + mUserIdsAddedAdapter = new UserIdsAddedAdapter(getActivity(), mSaveKeyringParcel.mAddUserIds, false); + mUserIdsAddedList.setAdapter(mUserIdsAddedAdapter); + } + + private void initView() { + mAddUserId.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + addUserId(); + } + }); + + mUserIdsList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + editUserId(position); + } + }); + } + + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + + switch (id) { + case LOADER_ID_USER_IDS: { + Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, + UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); + } + + default: + return null; + } + } + + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + switch (loader.getId()) { + case LOADER_ID_USER_IDS: { + mUserIdsAdapter.swapCursor(data); + break; + } + } + } + + /** + * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. + * We need to make sure we are no longer using it. + */ + public void onLoaderReset(Loader<Cursor> loader) { + switch (loader.getId()) { + case LOADER_ID_USER_IDS: { + mUserIdsAdapter.swapCursor(null); + break; + } + } + } + + private void editUserId(final int position) { + final String userId = mUserIdsAdapter.getUserId(position); + final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position); + final boolean isRevokedPending = mUserIdsAdapter.getIsRevokedPending(position); + + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case EditUserIdDialogFragment.MESSAGE_CHANGE_PRIMARY_USER_ID: + // toggle + if (mSaveKeyringParcel.mChangePrimaryUserId != null + && mSaveKeyringParcel.mChangePrimaryUserId.equals(userId)) { + mSaveKeyringParcel.mChangePrimaryUserId = null; + } else { + mSaveKeyringParcel.mChangePrimaryUserId = userId; + } + break; + case EditUserIdDialogFragment.MESSAGE_REVOKE: + // toggle + if (mSaveKeyringParcel.mRevokeUserIds.contains(userId)) { + mSaveKeyringParcel.mRevokeUserIds.remove(userId); + } else { + mSaveKeyringParcel.mRevokeUserIds.add(userId); + // not possible to revoke and change to primary user id + if (mSaveKeyringParcel.mChangePrimaryUserId != null + && mSaveKeyringParcel.mChangePrimaryUserId.equals(userId)) { + mSaveKeyringParcel.mChangePrimaryUserId = null; + } + } + break; + } + getLoaderManager().getLoader(LOADER_ID_USER_IDS).forceLoad(); + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + EditUserIdDialogFragment dialogFragment = + EditUserIdDialogFragment.newInstance(messenger, isRevoked, isRevokedPending); + dialogFragment.show(getActivity().getSupportFragmentManager(), "editUserIdDialog"); + } + }); + } + + private void addUserId() { + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + + // add new user id + mUserIdsAddedAdapter.add(data + .getString(AddUserIdDialogFragment.MESSAGE_DATA_USER_ID)); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + // pre-fill out primary name + String predefinedName = KeyRing.splitUserId(mPrimaryUserId).name; + AddUserIdDialogFragment addUserIdDialog = AddUserIdDialogFragment.newInstance(messenger, + predefinedName, false); + + addUserIdDialog.show(getActivity().getSupportFragmentManager(), "addUserIdDialog"); + } + + + private void editKey() { + EditIdentitiesActivity activity = (EditIdentitiesActivity) getActivity(); + if (activity == null) { + // this is a ui-triggered action, nvm if it fails while detached! + return; + } + + CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback + = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() { + @Override + public SaveKeyringParcel createOperationInput() { + return mSaveKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + + if (result.mMasterKeyId != null && mUploadKeyCheckbox.isChecked()) { + // result will be displayed after upload + uploadKey(result); + return; + } + + finishWithResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + displayResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + + mEditOpHelper = new CryptoOperationHelper<>(1, this, editKeyCallback, R.string.progress_building_key); + mEditOpHelper.cryptoOperation(); + } + + + private void uploadKey(final EditKeyResult editKeyResult) { + Activity activity = getActivity(); + // if the activity is gone at this point, there is nothing we can do! + if (activity == null) { + return; + } + + // set data uri as path to keyring + final long masterKeyId = editKeyResult.mMasterKeyId; + // upload to favorite keyserver + final String keyserver = Preferences.getPreferences(activity).getPreferredKeyserver(); + + CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> callback + = new CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult>() { + + @Override + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(keyserver, masterKeyId); + } + + @Override + public void onCryptoOperationSuccess(UploadResult result) { + handleResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(UploadResult result) { + displayResult(result); + } + + public void handleResult(UploadResult result) { + editKeyResult.getLog().add(result, 0); + finishWithResult(editKeyResult); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + + mUploadOpHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_uploading); + mUploadOpHelper.cryptoOperation(); + } + + /** + * Closes this activity, returning a result parcel with a single error log entry. + */ + void finishWithError(LogType reason) { + // Prepare an intent with an EXTRA_RESULT + Intent intent = new Intent(); + intent.putExtra(OperationResult.EXTRA_RESULT, + new SingletonResult(SingletonResult.RESULT_ERROR, reason)); + + // Finish with result + getActivity().setResult(Activity.RESULT_OK, intent); + getActivity().finish(); + } + + private void displayResult(OperationResult result) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + result.createNotify(activity).show(); + } + + public void finishWithResult(OperationResult result) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + Intent data = new Intent(); + data.putExtra(OperationResult.EXTRA_RESULT, result); + activity.setResult(Activity.RESULT_OK, data); + activity.finish(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java index 07b0a12d3..1c72cdf41 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java @@ -223,14 +223,16 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, EditKeyFragment.this); getLoaderManager().initLoader(LOADER_ID_SUBKEYS, null, EditKeyFragment.this); - mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, mSaveKeyringParcel); + mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0); + mUserIdsAdapter.setEditMode(mSaveKeyringParcel); mUserIdsList.setAdapter(mUserIdsAdapter); // TODO: SaveParcel from savedInstance?! mUserIdsAddedAdapter = new UserIdsAddedAdapter(getActivity(), mSaveKeyringParcel.mAddUserIds, false); mUserIdsAddedList.setAdapter(mUserIdsAddedAdapter); - mSubkeysAdapter = new SubkeysAdapter(getActivity(), null, 0, mSaveKeyringParcel); + mSubkeysAdapter = new SubkeysAdapter(getActivity(), null, 0); + mSubkeysAdapter.setEditMode(mSaveKeyringParcel); mSubkeysList.setAdapter(mSubkeysAdapter); mSubkeysAddedAdapter = new SubkeysAddedAdapter(getActivity(), mSaveKeyringParcel.mAddSubKeys, false); @@ -554,7 +556,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring // pre-fill out primary name String predefinedName = KeyRing.splitUserId(mPrimaryUserId).name; AddUserIdDialogFragment addUserIdDialog = AddUserIdDialogFragment.newInstance(messenger, - predefinedName); + predefinedName, true); addUserIdDialog.show(getActivity().getSupportFragmentManager(), "addUserIdDialog"); } @@ -610,7 +612,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring new SingletonResult(SingletonResult.RESULT_ERROR, reason)); // Finish with result - getActivity().setResult(EditKeyActivity.RESULT_OK, intent); + getActivity().setResult(Activity.RESULT_OK, intent); getActivity().finish(); } @@ -628,7 +630,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring // if good -> finish, return result to showkey and display there! Intent intent = new Intent(); intent.putExtra(OperationResult.EXTRA_RESULT, result); - activity.setResult(EditKeyActivity.RESULT_OK, intent); + activity.setResult(Activity.RESULT_OK, intent); activity.finish(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 35e00ff21..1db273e48 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -36,6 +36,8 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Message; +import android.os.Messenger; import android.provider.ContactsContract; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CollapsingToolbarLayout; @@ -64,6 +66,7 @@ import android.widget.Toast; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; @@ -75,10 +78,11 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; +import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; @@ -90,6 +94,7 @@ import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.NfcHelper; +import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; @@ -116,7 +121,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements // For CryptoOperationHelper.Callback private String mKeyserver; private ArrayList<ParcelableKeyRing> mKeyList; - private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper; + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper; + private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditOpHelper; + private SaveKeyringParcel mSaveKeyringParcel; private TextView mStatusText; private ImageView mStatusImage; @@ -151,8 +158,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements private boolean mIsRefreshing; private Animation mRotate, mRotateSpin; private View mRefresh; - private String mFingerprint; + private long mMasterKeyId; + private byte[] mFingerprint; + private String mFingerprintString; private byte[] mNfcFingerprints; private String mNfcUserId; @@ -164,7 +173,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements super.onCreate(savedInstanceState); mProviderHelper = new ProviderHelper(this); - mOperationHelper = new CryptoOperationHelper<>(1, this, this, null); + mImportOpHelper = new CryptoOperationHelper<>(1, this, this, null); setTitle(null); @@ -357,6 +366,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivity(homeIntent); return true; } + case R.id.menu_key_change_password: { + changePassword(); + return true; + } case R.id.menu_key_view_backup: { startPassphraseActivity(REQUEST_BACKUP); return true; @@ -379,23 +392,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); - finish(); - return true; - } - case R.id.menu_key_view_edit: { - editKey(mDataUri); - return true; - } case R.id.menu_key_view_certify_fingerprint: { - certifyFingeprint(mDataUri, false); + certifyFingerprint(mDataUri, false); return true; } case R.id.menu_key_view_certify_fingerprint_word: { - certifyFingeprint(mDataUri, true); + certifyFingerprint(mDataUri, true); return true; } } @@ -404,15 +406,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements @Override public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem editKey = menu.findItem(R.id.menu_key_view_edit); - editKey.setVisible(mIsSecret); - MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup); backupKey.setVisible(mIsSecret); - - MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity); - addLinked.setVisible(mIsSecret - && Preferences.getPreferences(this).getExperimentalEnableLinkedIdentities()); + MenuItem changePassword = menu.findItem(R.id.menu_key_change_password); + changePassword.setVisible(mIsSecret); MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint); certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked); @@ -423,6 +420,69 @@ public class ViewKeyActivity extends BaseNfcActivity implements return true; } + private void changePassword() { + mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint); + + CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback + = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() { + @Override + public SaveKeyringParcel createOperationInput() { + return mSaveKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + displayResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + displayResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + + mEditOpHelper = new CryptoOperationHelper<>(2, this, editKeyCallback, R.string.progress_building_key); + + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + + // use new passphrase! + mSaveKeyringParcel.mNewUnlock = new SaveKeyringParcel.ChangeUnlockParcel( + (Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE), + null + ); + + mEditOpHelper.cryptoOperation(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + SetPassphraseDialogFragment setPassphraseDialog = SetPassphraseDialogFragment.newInstance( + messenger, R.string.title_change_passphrase); + + setPassphraseDialog.show(getSupportFragmentManager(), "setPassphraseDialog"); + } + + private void displayResult(OperationResult result) { + result.createNotify(this).show(); + } private void scanQrCode() { Intent scanQrCode = new Intent(this, ImportKeysProxyActivity.class); @@ -430,7 +490,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT); } - private void certifyFingeprint(Uri dataUri, boolean enableWordConfirm) { + private void certifyFingerprint(Uri dataUri, boolean enableWordConfirm) { Intent intent = new Intent(this, CertifyFingerprintActivity.class); intent.setData(dataUri); intent.putExtra(CertifyFingerprintActivity.EXTRA_ENABLE_WORD_CONFIRM, enableWordConfirm); @@ -440,7 +500,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements private void certifyImmediate() { Intent intent = new Intent(this, CertifyKeyActivity.class); - intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { mMasterKeyId }); + intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId}); startActivityForResult(intent, REQUEST_CERTIFY); } @@ -515,9 +575,12 @@ public class ViewKeyActivity extends BaseNfcActivity implements @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + if (mImportOpHelper.handleActivityResult(requestCode, resultCode, data)) { return; } + if (mEditOpHelper != null) { + mEditOpHelper.handleActivityResult(requestCode, resultCode, data); + } switch (requestCode) { case REQUEST_QR_FINGERPRINT: { @@ -538,7 +601,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements Notify.create(this, R.string.error_scan_fp, Notify.LENGTH_LONG, Style.ERROR).show(); return; } - if (mFingerprint.equalsIgnoreCase(fp)) { + if (mFingerprintString.equalsIgnoreCase(fp)) { certifyImmediate(); } else { Notify.create(this, R.string.error_scan_match, Notify.LENGTH_LONG, Style.ERROR).show(); @@ -603,7 +666,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements byte[] candidateFp = ring.getFingerprint(); // if the master key of that key matches this one, just show the yubikey dialog - if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprint)) { + if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) { showYubiKeyFragment(mNfcFingerprints, mNfcUserId, mNfcAid); return; } @@ -692,12 +755,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements } } - private void editKey(Uri dataUri) { - Intent editIntent = new Intent(this, EditKeyActivity.class); - editIntent.setData(KeychainContract.KeyRingData.buildSecretKeyRingUri(dataUri)); - startActivityForResult(editIntent, 0); - } - private void startSafeSlinger(Uri dataUri) { long keyId = 0; try { @@ -808,14 +865,15 @@ public class ViewKeyActivity extends BaseNfcActivity implements /* TODO better error handling? May cause problems when a key is deleted, * because the notification triggers faster than the activity closes. */ - // Avoid NullPointerExceptions... - if (data.getCount() == 0) { - return; - } + // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) switch (loader.getId()) { case LOADER_ID_UNIFIED: { + // Avoid NullPointerExceptions... + if (data.getCount() == 0) { + return; + } if (data.moveToFirst()) { // get name, email, and comment from USER_ID @@ -827,7 +885,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements } mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - mFingerprint = KeyFormattingUtils.convertFingerprintToHex(data.getBlob(INDEX_FINGERPRINT)); + mFingerprint = data.getBlob(INDEX_FINGERPRINT); + mFingerprintString = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); // if it wasn't shown yet, display yubikey fragment if (mShowYubikeyAfterCreation && getIntent().hasExtra(EXTRA_NFC_AID)) { @@ -904,8 +963,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements mStatusImage.setVisibility(View.GONE); color = getResources().getColor(R.color.key_flag_green); // reload qr code only if the fingerprint changed - if (!mFingerprint.equals(mQrCodeLoaded)) { - loadQrCode(mFingerprint); + if (!mFingerprintString.equals(mQrCodeLoaded)) { + loadQrCode(mFingerprintString); } photoTask.execute(mMasterKeyId); mQrCodeLayout.setVisibility(View.VISIBLE); @@ -1045,7 +1104,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements mKeyserver = Preferences.getPreferences(this).getPreferredKeyserver(); - mOperationHelper.cryptoOperation(); + mImportOpHelper.cryptoOperation(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index 94a171f14..b10e5f8d4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -17,16 +17,26 @@ package org.sufficientlysecure.keychain.ui; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; -import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.OvershootInterpolator; import android.widget.Toast; import com.astuetz.PagerSlidingTabStrip; @@ -44,7 +54,7 @@ import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.Log; public class ViewKeyAdvActivity extends BaseActivity implements - LoaderManager.LoaderCallbacks<Cursor> { + LoaderCallbacks<Cursor>, OnPageChangeListener { ProviderHelper mProviderHelper; @@ -61,6 +71,11 @@ public class ViewKeyAdvActivity extends BaseActivity implements private PagerSlidingTabStrip mSlidingTabLayout; private static final int LOADER_ID_UNIFIED = 0; + private ActionMode mActionMode; + private boolean mHasSecret; + private PagerTabStripAdapter mTabAdapter; + private boolean mActionIconShown; + private boolean[] mTabsWithActionMode; @Override protected void onCreate(Bundle savedInstanceState) { @@ -78,9 +93,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements mViewPager = (ViewPager) findViewById(R.id.pager); mSlidingTabLayout = (PagerSlidingTabStrip) findViewById(R.id.sliding_tab_layout); - Intent intent = getIntent(); - int switchToTab = intent.getIntExtra(EXTRA_SELECTED_TAB, TAB_SHARE); - mDataUri = getIntent().getData(); if (mDataUri == null) { Log.e(Constants.TAG, "Data missing. Should be uri of key!"); @@ -102,9 +114,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); initTabs(mDataUri); - - // switch to tab selected by extra - mViewPager.setCurrentItem(switchToTab); } @Override @@ -113,31 +122,45 @@ public class ViewKeyAdvActivity extends BaseActivity implements } private void initTabs(Uri dataUri) { - PagerTabStripAdapter adapter = new PagerTabStripAdapter(this); - mViewPager.setAdapter(adapter); + mTabAdapter = new PagerTabStripAdapter(this); + mViewPager.setAdapter(mTabAdapter); + + // keep track which of these are action mode enabled! + mTabsWithActionMode = new boolean[4]; Bundle shareBundle = new Bundle(); - shareBundle.putParcelable(ViewKeyAdvUserIdsFragment.ARG_DATA_URI, dataUri); - adapter.addTab(ViewKeyAdvShareFragment.class, + shareBundle.putParcelable(ViewKeyAdvShareFragment.ARG_DATA_URI, dataUri); + mTabAdapter.addTab(ViewKeyAdvShareFragment.class, shareBundle, getString(R.string.key_view_tab_share)); + mTabsWithActionMode[0] = false; Bundle userIdsBundle = new Bundle(); userIdsBundle.putParcelable(ViewKeyAdvUserIdsFragment.ARG_DATA_URI, dataUri); - adapter.addTab(ViewKeyAdvUserIdsFragment.class, + mTabAdapter.addTab(ViewKeyAdvUserIdsFragment.class, userIdsBundle, getString(R.string.section_user_ids)); + mTabsWithActionMode[1] = true; Bundle keysBundle = new Bundle(); keysBundle.putParcelable(ViewKeyAdvSubkeysFragment.ARG_DATA_URI, dataUri); - adapter.addTab(ViewKeyAdvSubkeysFragment.class, + mTabAdapter.addTab(ViewKeyAdvSubkeysFragment.class, keysBundle, getString(R.string.key_view_tab_keys)); + mTabsWithActionMode[2] = true; Bundle certsBundle = new Bundle(); certsBundle.putParcelable(ViewKeyAdvCertsFragment.ARG_DATA_URI, dataUri); - adapter.addTab(ViewKeyAdvCertsFragment.class, + mTabAdapter.addTab(ViewKeyAdvCertsFragment.class, certsBundle, getString(R.string.key_view_tab_certs)); + mTabsWithActionMode[3] = false; // update layout after operations mSlidingTabLayout.setViewPager(mViewPager); + mSlidingTabLayout.setOnPageChangeListener(this); + + // switch to tab selected by extra + Intent intent = getIntent(); + int switchToTab = intent.getIntExtra(EXTRA_SELECTED_TAB, TAB_SHARE); + mViewPager.setCurrentItem(switchToTab); + } // These are the rows that we will retrieve. @@ -148,7 +171,8 @@ public class ViewKeyAdvActivity extends BaseActivity implements KeychainContract.KeyRings.IS_REVOKED, KeychainContract.KeyRings.IS_EXPIRED, KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET + KeychainContract.KeyRings.HAS_ANY_SECRET, + KeychainContract.KeyRings.FINGERPRINT, }; static final int INDEX_MASTER_KEY_ID = 1; @@ -157,6 +181,7 @@ public class ViewKeyAdvActivity extends BaseActivity implements static final int INDEX_IS_EXPIRED = 4; static final int INDEX_VERIFIED = 5; static final int INDEX_HAS_ANY_SECRET = 6; + static final int INDEX_FINGERPRINT = 7; @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { @@ -190,11 +215,13 @@ public class ViewKeyAdvActivity extends BaseActivity implements setTitle(R.string.user_id_no_name); } + byte[] fingerprint = data.getBlob(INDEX_FINGERPRINT); + // get key id from MASTER_KEY_ID long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); getSupportActionBar().setSubtitle(KeyFormattingUtils.beautifyKeyIdWithPrefix(this, masterKeyId)); - boolean isSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; boolean isRevoked = data.getInt(INDEX_IS_REVOKED) > 0; boolean isExpired = data.getInt(INDEX_IS_EXPIRED) != 0; boolean isVerified = data.getInt(INDEX_VERIFIED) > 0; @@ -203,7 +230,7 @@ public class ViewKeyAdvActivity extends BaseActivity implements int color; if (isRevoked || isExpired) { color = getResources().getColor(R.color.key_flag_red); - } else if (isSecret) { + } else if (mHasSecret) { color = getResources().getColor(R.color.android_green_light); } else { if (isVerified) { @@ -237,4 +264,85 @@ public class ViewKeyAdvActivity extends BaseActivity implements super.onActivityResult(requestCode, resultCode, data); } } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + if (!mHasSecret) { + return false; + } + + // always add the item, switch its visibility depending on fragment + getMenuInflater().inflate(R.menu.action_mode_edit, menu); + final MenuItem vActionModeItem = menu.findItem(R.id.menu_action_mode_edit); + + boolean isCurrentActionFragment = mTabsWithActionMode[mViewPager.getCurrentItem()]; + + // if the state is as it should be, never mind + if (isCurrentActionFragment == mActionIconShown) { + return isCurrentActionFragment; + } + + // show or hide accordingly + mActionIconShown = isCurrentActionFragment; + vActionModeItem.setEnabled(isCurrentActionFragment); + animateMenuItem(vActionModeItem, isCurrentActionFragment); + + return true; + } + + private void animateMenuItem(final MenuItem vEditSubkeys, final boolean animateShow) { + + View actionView = LayoutInflater.from(this).inflate(R.layout.edit_icon, null); + vEditSubkeys.setActionView(actionView); + actionView.setTranslationX(animateShow ? 150 : 0); + + ViewPropertyAnimator animator = actionView.animate(); + animator.translationX(animateShow ? 0 : 150); + animator.setDuration(300); + animator.setInterpolator(new OvershootInterpolator(1.5f)); + animator.setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (!animateShow) { + vEditSubkeys.setVisible(false); + } + vEditSubkeys.setActionView(null); + } + }); + animator.start(); + + } + + @Override + public void onActionModeStarted(final ActionMode mode) { + super.onActionModeStarted(mode); + mActionMode = mode; + } + + @Override + public void onActionModeFinished(ActionMode mode) { + super.onActionModeFinished(mode); + mActionMode = null; + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + if (mActionMode != null) { + mActionMode.finish(); + mActionMode = null; + } + invalidateOptionsMenu(); + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index c5e575e32..ce2f2def8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -44,6 +44,7 @@ import android.support.v4.content.Loader; import android.support.v7.widget.CardView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.widget.ImageButton; @@ -85,6 +86,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private byte[] mFingerprint; private String mUserId; + private Bitmap mQrCodeBitmapCache; @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { @@ -96,6 +98,34 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements mFingerprintView = (TextView) view.findViewById(R.id.view_key_fingerprint); mQrCode = (ImageView) view.findViewById(R.id.view_key_qr_code); + + // We cache the QR code bitmap in its smallest possible size, then scale + // it manually for the correct size whenever the layout of the ImageView + // changes. The fingerprint qr code loader which runs in the background + // just calls requestLayout when it is finished, this way the loader and + // background task are disconnected from any layouting the ImageView may + // undergo. Please note how these six lines are perfectly right-aligned. + mQrCode.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, + int oldRight, + int oldBottom) { + // bitmap scaling is expensive, avoid doing it if we already have the correct size! + int mCurrentWidth = 0, mCurrentHeight = 0; + if (mQrCodeBitmapCache != null) { + if (mCurrentWidth == mQrCode.getWidth() && mCurrentHeight == mQrCode.getHeight()) { + return; + } + mCurrentWidth = mQrCode.getWidth(); + mCurrentHeight = mQrCode.getHeight(); + // scale the image up to our actual size. we do this in code rather + // than let the ImageView do this because we don't require filtering. + Bitmap scaled = Bitmap.createScaledBitmap(mQrCodeBitmapCache, + mCurrentWidth, mCurrentHeight, false); + mQrCode.setImageBitmap(scaled); + } + } + }); mQrCodeLayout = (CardView) view.findViewById(R.id.view_key_qr_code_layout); mQrCodeLayout.setOnClickListener(new View.OnClickListener() { @Override @@ -379,6 +409,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements */ public void onLoaderReset(Loader<Cursor> loader) { mFingerprint = null; + mQrCodeBitmapCache = null; } /** @@ -390,6 +421,10 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); mFingerprintView.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); + if (mQrCodeBitmapCache != null) { + return; + } + AsyncTask<Void, Void, Bitmap> loadTask = new AsyncTask<Void, Void, Bitmap>() { protected Bitmap doInBackground(Void... unused) { @@ -402,15 +437,11 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } protected void onPostExecute(Bitmap qrCode) { - // only change view, if fragment is attached to activity - if (ViewKeyAdvShareFragment.this.isAdded()) { + // cache for later, and if we are attached request re-layout + mQrCodeBitmapCache = qrCode; - // scale the image up to our actual size. we do this in code rather - // than let the ImageView do this because we don't require filtering. - Bitmap scaled = Bitmap.createScaledBitmap(qrCode, - mQrCode.getHeight(), mQrCode.getHeight(), - false); - mQrCode.setImageBitmap(scaled); + if (ViewKeyAdvShareFragment.this.isAdded()) { + mQrCode.requestLayout(); // simple fade-in animation AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java index bd00c6780..14477723e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java @@ -17,21 +17,43 @@ package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ListView; +import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter; +import org.sufficientlysecure.keychain.ui.adapter.SubkeysAddedAdapter; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyExpiryDialogFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements @@ -39,30 +61,62 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements public static final String ARG_DATA_URI = "data_uri"; + private static final int LOADER_ID_UNIFIED = 0; + private static final int LOADER_ID_SUBKEYS = 1; + private ListView mSubkeysList; + private ListView mSubkeysAddedList; + private View mSubkeysAddedLayout; + private ViewAnimator mSubkeyAddFabLayout; + private SubkeysAdapter mSubkeysAdapter; + private SubkeysAddedAdapter mSubkeysAddedAdapter; - private Uri mDataUriSubkeys; + private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditKeyHelper; - /** - * Creates new instance of this fragment - */ - public static ViewKeyAdvSubkeysFragment newInstance(Uri dataUri) { - ViewKeyAdvSubkeysFragment frag = new ViewKeyAdvSubkeysFragment(); + private Uri mDataUri; - Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); - - frag.setArguments(args); - return frag; - } + private long mMasterKeyId; + private byte[] mFingerprint; + private boolean mHasSecret; + private SaveKeyringParcel mEditModeSaveKeyringParcel; @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.view_key_adv_subkeys_fragment, getContainer()); - mSubkeysList = (ListView) view.findViewById(R.id.keys); + mSubkeysList = (ListView) view.findViewById(R.id.view_key_subkeys); + mSubkeysAddedList = (ListView) view.findViewById(R.id.view_key_subkeys_added); + mSubkeysAddedLayout = view.findViewById(R.id.view_key_subkeys_add_layout); + + mSubkeysList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + editSubkey(position); + } + }); + + View footer = new View(getActivity()); + int spacing = (int) android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_DIP, 72, getResources().getDisplayMetrics() + ); + android.widget.AbsListView.LayoutParams params = new android.widget.AbsListView.LayoutParams( + android.widget.AbsListView.LayoutParams.MATCH_PARENT, + spacing + ); + footer.setLayoutParams(params); + mSubkeysAddedList.addFooterView(footer, null, false); + + mSubkeyAddFabLayout = (ViewAnimator) view.findViewById(R.id.view_key_subkey_fab_layout); + view.findViewById(R.id.view_key_subkey_fab).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + addSubkey(); + } + }); + + setHasOptionsMenu(true); return root; } @@ -81,8 +135,17 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements loadData(dataUri); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mEditKeyHelper != null) { + mEditKeyHelper.handleActivityResult(requestCode, resultCode, data); + } + + super.onActivityResult(requestCode, resultCode, data); + } + private void loadData(Uri dataUri) { - mDataUriSubkeys = KeychainContract.Keys.buildKeysUri(dataUri); + mDataUri = dataUri; // Create an empty adapter we will use to display the loaded data. mSubkeysAdapter = new SubkeysAdapter(getActivity(), null, 0); @@ -90,14 +153,42 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements // Prepare the loaders. Either re-connect with an existing ones, // or start new ones. - getLoaderManager().initLoader(0, null, this); + getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + getLoaderManager().initLoader(LOADER_ID_SUBKEYS, null, this); } + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + KeychainContract.KeyRings._ID, + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.HAS_ANY_SECRET, + KeychainContract.KeyRings.FINGERPRINT, + }; + + static final int INDEX_MASTER_KEY_ID = 1; + static final int INDEX_HAS_ANY_SECRET = 2; + static final int INDEX_FINGERPRINT = 3; + + @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - setContentShown(false); + switch (id) { + case LOADER_ID_UNIFIED: { + Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, + PROJECTION, null, null, null); + } + + case LOADER_ID_SUBKEYS: { + setContentShown(false); - return new CursorLoader(getActivity(), mDataUriSubkeys, - SubkeysAdapter.SUBKEYS_PROJECTION, null, null, null); + Uri subkeysUri = KeychainContract.Keys.buildKeysUri(mDataUri); + return new CursorLoader(getActivity(), subkeysUri, + SubkeysAdapter.SUBKEYS_PROJECTION, null, null, null); + } + + default: + return null; + } } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { @@ -106,12 +197,26 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements return; } - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - mSubkeysAdapter.swapCursor(data); + switch (loader.getId()) { + case LOADER_ID_UNIFIED: { + data.moveToFirst(); + + mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); + mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mFingerprint = data.getBlob(INDEX_FINGERPRINT); + break; + } + case LOADER_ID_SUBKEYS: { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mSubkeysAdapter.swapCursor(data); + + // TODO: maybe show not before both are loaded! + setContentShown(true); + break; + } + } - // TODO: maybe show not before both are loaded! - setContentShown(true); } /** @@ -122,4 +227,254 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements mSubkeysAdapter.swapCursor(null); } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_action_mode_edit: + enterEditMode(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public void enterEditMode() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + activity.startActionMode(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + + mEditModeSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint); + + mSubkeysAddedAdapter = + new SubkeysAddedAdapter(getActivity(), mEditModeSaveKeyringParcel.mAddSubKeys, false); + mSubkeysAddedList.setAdapter(mSubkeysAddedAdapter); + mSubkeysAddedLayout.setVisibility(View.VISIBLE); + mSubkeyAddFabLayout.setDisplayedChild(1); + + mSubkeysAdapter.setEditMode(mEditModeSaveKeyringParcel); + getLoaderManager().restartLoader(LOADER_ID_SUBKEYS, null, ViewKeyAdvSubkeysFragment.this); + + mode.setTitle(R.string.title_edit_subkeys); + mode.getMenuInflater().inflate(R.menu.action_edit_uids, menu); + + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + editKey(mode); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mEditModeSaveKeyringParcel = null; + mSubkeysAdapter.setEditMode(null); + mSubkeysAddedLayout.setVisibility(View.GONE); + mSubkeyAddFabLayout.setDisplayedChild(0); + getLoaderManager().restartLoader(LOADER_ID_SUBKEYS, null, ViewKeyAdvSubkeysFragment.this); + } + }); + } + + private void addSubkey() { + boolean willBeMasterKey; + if (mSubkeysAdapter != null) { + willBeMasterKey = mSubkeysAdapter.getCount() == 0 && mSubkeysAddedAdapter.getCount() == 0; + } else { + willBeMasterKey = mSubkeysAddedAdapter.getCount() == 0; + } + + AddSubkeyDialogFragment addSubkeyDialogFragment = + AddSubkeyDialogFragment.newInstance(willBeMasterKey); + addSubkeyDialogFragment + .setOnAlgorithmSelectedListener( + new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() { + @Override + public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey) { + mSubkeysAddedAdapter.add(newSubkey); + } + } + ); + addSubkeyDialogFragment.show(getActivity().getSupportFragmentManager(), "addSubkeyDialog"); + } + + private void editSubkey(final int position) { + final long keyId = mSubkeysAdapter.getKeyId(position); + + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case EditSubkeyDialogFragment.MESSAGE_CHANGE_EXPIRY: + editSubkeyExpiry(position); + break; + case EditSubkeyDialogFragment.MESSAGE_REVOKE: + // toggle + if (mEditModeSaveKeyringParcel.mRevokeSubKeys.contains(keyId)) { + mEditModeSaveKeyringParcel.mRevokeSubKeys.remove(keyId); + } else { + mEditModeSaveKeyringParcel.mRevokeSubKeys.add(keyId); + } + break; + case EditSubkeyDialogFragment.MESSAGE_STRIP: { + SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); + if (secretKeyType == SecretKeyType.GNU_DUMMY) { + // Key is already stripped; this is a no-op. + break; + } + + SubkeyChange change = mEditModeSaveKeyringParcel.getSubkeyChange(keyId); + if (change == null) { + mEditModeSaveKeyringParcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, false)); + break; + } + // toggle + change.mDummyStrip = !change.mDummyStrip; + if (change.mDummyStrip && change.mMoveKeyToCard) { + // User had chosen to divert key, but now wants to strip it instead. + change.mMoveKeyToCard = false; + } + break; + } + case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: { + // TODO: enable later when Admin PIN handling is resolved + Notify.create(getActivity(), + "This feature will be available in an upcoming OpenKeychain version.", + Notify.Style.WARN).show(); + break; + +// Activity activity = EditKeyFragment.this.getActivity(); +// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); +// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD || +// secretKeyType == SecretKeyType.GNU_DUMMY) { +// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR) +// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); +// break; +// } +// int algorithm = mSubkeysAdapter.getAlgorithm(position); +// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN +// if (algorithm != 1 && algorithm != 2 && algorithm != 3) { +// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR) +// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); +// break; +// } +// if (mSubkeysAdapter.getKeySize(position) != 2048) { +// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR) +// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); +// break; +// } +// +// +// SubkeyChange change; +// change = mSaveKeyringParcel.getSubkeyChange(keyId); +// if (change == null) { +// mSaveKeyringParcel.mChangeSubKeys.add( +// new SubkeyChange(keyId, false, true) +// ); +// break; +// } +// // toggle +// change.mMoveKeyToCard = !change.mMoveKeyToCard; +// if (change.mMoveKeyToCard && change.mDummyStrip) { +// // User had chosen to strip key, but now wants to divert it. +// change.mDummyStrip = false; +// } +// break; + } + } + getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad(); + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + EditSubkeyDialogFragment dialogFragment = + EditSubkeyDialogFragment.newInstance(messenger); + + dialogFragment.show(getActivity().getSupportFragmentManager(), "editSubkeyDialog"); + } + }); + } + + private void editSubkeyExpiry(final int position) { + final long keyId = mSubkeysAdapter.getKeyId(position); + final Long creationDate = mSubkeysAdapter.getCreationDate(position); + final Long expiryDate = mSubkeysAdapter.getExpiryDate(position); + + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case EditSubkeyExpiryDialogFragment.MESSAGE_NEW_EXPIRY: + mEditModeSaveKeyringParcel.getOrCreateSubkeyChange(keyId).mExpiry = + (Long) message.getData().getSerializable( + EditSubkeyExpiryDialogFragment.MESSAGE_DATA_EXPIRY); + break; + } + getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad(); + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + EditSubkeyExpiryDialogFragment dialogFragment = + EditSubkeyExpiryDialogFragment.newInstance(messenger, creationDate, expiryDate); + + dialogFragment.show(getActivity().getSupportFragmentManager(), "editSubkeyExpiryDialog"); + } + }); + } + + + private void editKey(final ActionMode mode) { + CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback + = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() { + + @Override + public SaveKeyringParcel createOperationInput() { + return mEditModeSaveKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + mode.finish(); + result.createNotify(getActivity()).show(); + } + + @Override + public void onCryptoOperationCancelled() { + mode.finish(); + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + mode.finish(); + result.createNotify(getActivity()).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + mEditKeyHelper = new CryptoOperationHelper<>(1, this, editKeyCallback, R.string.progress_saving); + mEditKeyHelper.cryptoOperation(); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java index ad437f924..69ccab162 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java @@ -18,24 +18,41 @@ package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; +import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; +import org.sufficientlysecure.keychain.ui.adapter.UserIdsAddedAdapter; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.dialog.AddUserIdDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditUserIdDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; import org.sufficientlysecure.keychain.util.Log; @@ -44,33 +61,124 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements public static final String ARG_DATA_URI = "uri"; - private ListView mUserIds; - private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_USER_IDS = 1; + private ListView mUserIds; + private ListView mUserIdsAddedList; + private View mUserIdsAddedLayout; + private ViewAnimator mUserIdAddFabLayout; + private UserIdsAdapter mUserIdsAdapter; + private UserIdsAddedAdapter mUserIdsAddedAdapter; + + private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditKeyHelper; private Uri mDataUri; + private long mMasterKeyId; + private byte[] mFingerprint; + private boolean mHasSecret; + private SaveKeyringParcel mEditModeSaveKeyringParcel; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.view_key_adv_main_fragment, getContainer()); + View view = inflater.inflate(R.layout.view_key_adv_user_ids_fragment, getContainer()); mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids); + mUserIdsAddedList = (ListView) view.findViewById(R.id.view_key_user_ids_added); + mUserIdsAddedLayout = view.findViewById(R.id.view_key_user_ids_add_layout); mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - showUserIdInfo(position); + showOrEditUserIdInfo(position); } }); + View footer = new View(getActivity()); + int spacing = (int) android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_DIP, 72, getResources().getDisplayMetrics() + ); + android.widget.AbsListView.LayoutParams params = new android.widget.AbsListView.LayoutParams( + android.widget.AbsListView.LayoutParams.MATCH_PARENT, + spacing + ); + footer.setLayoutParams(params); + mUserIdsAddedList.addFooterView(footer, null, false); + + mUserIdAddFabLayout = (ViewAnimator) view.findViewById(R.id.view_key_subkey_fab_layout); + view.findViewById(R.id.view_key_subkey_fab).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + addUserId(); + } + }); + + setHasOptionsMenu(true); + return root; } + private void showOrEditUserIdInfo(final int position) { + if (mEditModeSaveKeyringParcel != null) { + editUserId(position); + } else { + showUserIdInfo(position); + } + } + + private void editUserId(final int position) { + final String userId = mUserIdsAdapter.getUserId(position); + final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position); + final boolean isRevokedPending = mUserIdsAdapter.getIsRevokedPending(position); + + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case EditUserIdDialogFragment.MESSAGE_CHANGE_PRIMARY_USER_ID: + // toggle + if (mEditModeSaveKeyringParcel.mChangePrimaryUserId != null + && mEditModeSaveKeyringParcel.mChangePrimaryUserId.equals(userId)) { + mEditModeSaveKeyringParcel.mChangePrimaryUserId = null; + } else { + mEditModeSaveKeyringParcel.mChangePrimaryUserId = userId; + } + break; + case EditUserIdDialogFragment.MESSAGE_REVOKE: + // toggle + if (mEditModeSaveKeyringParcel.mRevokeUserIds.contains(userId)) { + mEditModeSaveKeyringParcel.mRevokeUserIds.remove(userId); + } else { + mEditModeSaveKeyringParcel.mRevokeUserIds.add(userId); + // not possible to revoke and change to primary user id + if (mEditModeSaveKeyringParcel.mChangePrimaryUserId != null + && mEditModeSaveKeyringParcel.mChangePrimaryUserId.equals(userId)) { + mEditModeSaveKeyringParcel.mChangePrimaryUserId = null; + } + } + break; + } + getLoaderManager().getLoader(LOADER_ID_USER_IDS).forceLoad(); + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + EditUserIdDialogFragment dialogFragment = + EditUserIdDialogFragment.newInstance(messenger, isRevoked, isRevokedPending); + dialogFragment.show(getActivity().getSupportFragmentManager(), "editUserIdDialog"); + } + }); + } + private void showUserIdInfo(final int position) { + final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position); final int isVerified = mUserIdsAdapter.getIsVerified(position); @@ -84,6 +192,30 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements }); } + private void addUserId() { + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + + // add new user id + mUserIdsAddedAdapter.add(data + .getString(AddUserIdDialogFragment.MESSAGE_DATA_USER_ID)); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + // pre-fill out primary name + AddUserIdDialogFragment addUserIdDialog = + AddUserIdDialogFragment.newInstance(messenger, "", true); + + addUserIdDialog.show(getActivity().getSupportFragmentManager(), "addUserIdDialog"); + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -98,10 +230,19 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements loadData(dataUri); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mEditKeyHelper != null) { + mEditKeyHelper.handleActivityResult(requestCode, resultCode, data); + } + + super.onActivityResult(requestCode, resultCode, data); + } + private void loadData(Uri dataUri) { mDataUri = dataUri; - Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + Log.i(Constants.TAG, "mDataUri: " + mDataUri); mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0); mUserIds.setAdapter(mUserIdsAdapter); @@ -112,27 +253,31 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); } - static final String[] UNIFIED_PROJECTION = new String[]{ - KeyRings._ID, KeyRings.MASTER_KEY_ID, - KeyRings.HAS_ANY_SECRET, KeyRings.IS_REVOKED, KeyRings.IS_EXPIRED, KeyRings.HAS_ENCRYPT + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + KeychainContract.KeyRings._ID, + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.HAS_ANY_SECRET, + KeychainContract.KeyRings.FINGERPRINT, }; - static final int INDEX_UNIFIED_MASTER_KEY_ID = 1; - static final int INDEX_UNIFIED_HAS_ANY_SECRET = 2; - static final int INDEX_UNIFIED_IS_REVOKED = 3; - static final int INDEX_UNIFIED_IS_EXPIRED = 4; - static final int INDEX_UNIFIED_HAS_ENCRYPT = 5; - public Loader<Cursor> onCreateLoader(int id, Bundle args) { - setContentShown(false); + static final int INDEX_MASTER_KEY_ID = 1; + static final int INDEX_HAS_ANY_SECRET = 2; + static final int INDEX_FINGERPRINT = 3; + public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_ID_UNIFIED: { - Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); + Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, + PROJECTION, null, null, null); } + case LOADER_ID_USER_IDS: { - Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, + setContentShown(false); + + Uri userIdUri = UserPackets.buildUserIdsUri(mDataUri); + return new CursorLoader(getActivity(), userIdUri, UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); } @@ -142,31 +287,29 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { - /* TODO better error handling? May cause problems when a key is deleted, - * because the notification triggers faster than the activity closes. - */ - // Avoid NullPointerExceptions... + // Avoid NullPointerExceptions, if we get an empty result set. if (data.getCount() == 0) { return; } - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) + switch (loader.getId()) { case LOADER_ID_UNIFIED: { - if (data.moveToFirst()) { - + data.moveToFirst(); - break; - } + mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); + mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mFingerprint = data.getBlob(INDEX_FINGERPRINT); + break; } - case LOADER_ID_USER_IDS: { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) mUserIdsAdapter.swapCursor(data); + + setContentShown(true); break; } - } - setContentShown(true); } /** @@ -174,11 +317,104 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements * We need to make sure we are no longer using it. */ public void onLoaderReset(Loader<Cursor> loader) { - switch (loader.getId()) { - case LOADER_ID_USER_IDS: - mUserIdsAdapter.swapCursor(null); - break; + if (loader.getId() != LOADER_ID_USER_IDS) { + return; + } + mUserIdsAdapter.swapCursor(null); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_action_mode_edit: + enterEditMode(); + return true; + default: + return super.onOptionsItemSelected(item); } } + public void enterEditMode() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + activity.startActionMode(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + + mEditModeSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint); + + mUserIdsAddedAdapter = + new UserIdsAddedAdapter(getActivity(), mEditModeSaveKeyringParcel.mAddUserIds, false); + mUserIdsAddedList.setAdapter(mUserIdsAddedAdapter); + mUserIdsAddedLayout.setVisibility(View.VISIBLE); + mUserIdAddFabLayout.setDisplayedChild(1); + + mUserIdsAdapter.setEditMode(mEditModeSaveKeyringParcel); + getLoaderManager().restartLoader(LOADER_ID_USER_IDS, null, ViewKeyAdvUserIdsFragment.this); + + mode.setTitle(R.string.title_edit_identities); + mode.getMenuInflater().inflate(R.menu.action_edit_uids, menu); + + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + editKey(mode); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mEditModeSaveKeyringParcel = null; + mUserIdsAdapter.setEditMode(null); + mUserIdsAddedLayout.setVisibility(View.GONE); + mUserIdAddFabLayout.setDisplayedChild(0); + getLoaderManager().restartLoader(LOADER_ID_USER_IDS, null, ViewKeyAdvUserIdsFragment.this); + } + }); + } + + private void editKey(final ActionMode mode) { + CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback + = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() { + + @Override + public SaveKeyringParcel createOperationInput() { + return mEditModeSaveKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + mode.finish(); + result.createNotify(getActivity()).show(); + } + + @Override + public void onCryptoOperationCancelled() { + mode.finish(); + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + mode.finish(); + result.createNotify(getActivity()).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + mEditKeyHelper = new CryptoOperationHelper<>(1, this, editKeyCallback, R.string.progress_saving); + mEditKeyHelper.cryptoOperation(); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java index f75012731..89dd90ff7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java @@ -24,7 +24,6 @@ import java.util.List; import android.Manifest; import android.annotation.TargetApi; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -47,10 +46,10 @@ import android.transition.TransitionInflater; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnPreDrawListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; +import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; @@ -59,12 +58,14 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter; 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.ui.linked.LinkedIdWizard; import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; @@ -76,7 +77,6 @@ public class ViewKeyFragment extends LoaderFragment implements public static final String ARG_POSTPONE_TYPE = "postpone_type"; private ListView mUserIds; - //private ListView mLinkedSystemContact; enum PostponeType { NONE, LINKED; @@ -86,8 +86,8 @@ 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 int LOADER_ID_LINKED_IDS = 2; + private static final int LOADER_ID_LINKED_CONTACT = 3; private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID = "loader_linked_contact_master_key_id"; @@ -107,6 +107,7 @@ public class ViewKeyFragment extends LoaderFragment implements private ListView mLinkedIds; private CardView mLinkedIdsCard; + private TextView mLinkedIdsEmpty; private byte[] mFingerprint; private TextView mLinkedIdsExpander; @@ -130,11 +131,30 @@ 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); + Button userIdsEditButton = (Button) view.findViewById(R.id.view_key_card_user_ids_edit); 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); + mLinkedIdsEmpty = (TextView) view.findViewById(R.id.view_key_linked_ids_empty); + Button linkedIdsAddButton = (Button) view.findViewById(R.id.view_key_card_linked_ids_add); + mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card); + mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout); + mSystemContactName = (TextView) view.findViewById(R.id.system_contact_name); + mSystemContactPicture = (ImageView) view.findViewById(R.id.system_contact_picture); + + userIdsEditButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + editIdentities(mDataUri); + } + }); + + linkedIdsAddButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + addLinkedIdentity(mDataUri); + } + }); mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -149,26 +169,34 @@ public class ViewKeyFragment extends LoaderFragment implements } }); - mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card); - mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout); - mSystemContactName = (TextView) view.findViewById(R.id.system_contact_name); - mSystemContactPicture = (ImageView) view.findViewById(R.id.system_contact_picture); - return root; } + private void editIdentities(Uri dataUri) { + Intent editIntent = new Intent(getActivity(), EditIdentitiesActivity.class); + editIntent.setData(KeychainContract.KeyRingData.buildSecretKeyRingUri(dataUri)); + startActivityForResult(editIntent, 0); + } + + private void addLinkedIdentity(Uri dataUri) { + Intent intent = new Intent(getActivity(), LinkedIdWizard.class); + intent.setData(dataUri); + startActivity(intent); + getActivity().finish(); + } + private void showLinkedId(final int position) { final LinkedIdViewFragment frag; try { frag = mLinkedIdsAdapter.getLinkedIdFragment(mDataUri, position, mFingerprint); } catch (IOException e) { - e.printStackTrace(); + Log.e(Constants.TAG, "IOException", e); return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Transition trans = TransitionInflater.from(getActivity()) - .inflateTransition(R.transition.linked_id_card_trans); + .inflateTransition(R.transition.linked_id_card_trans); // setSharedElementReturnTransition(trans); setExitTransition(new Fade()); frag.setSharedElementEnterTransition(trans); @@ -221,7 +249,7 @@ public class ViewKeyFragment extends LoaderFragment implements */ private void loadLinkedSystemContact(final long contactId) { // contact doesn't exist, stop - if(contactId == -1) return; + if (contactId == -1) return; final Context context = mSystemContactName.getContext(); ContactHelper contactHelper = new ContactHelper(context); @@ -298,7 +326,17 @@ public class ViewKeyFragment extends LoaderFragment implements loadData(dataUri); } - // These are the rows that we will retrieve. + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + // if a result has been returned, display a notify + if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { + OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); + result.createNotify(getActivity()).show(); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + static final String[] UNIFIED_PROJECTION = new String[]{ KeychainContract.KeyRings._ID, KeychainContract.KeyRings.MASTER_KEY_ID, @@ -325,7 +363,7 @@ public class ViewKeyFragment extends LoaderFragment implements @SuppressWarnings("unused") static final int INDEX_HAS_ENCRYPT = 8; - private static final String[] RAWCONTACT_PROJECTION = { + private static final String[] RAW_CONTACT_PROJECTION = { ContactsContract.RawContacts.CONTACT_ID }; @@ -359,29 +397,28 @@ public class ViewKeyFragment extends LoaderFragment implements return LinkedIdsAdapter.createLoader(getActivity(), mDataUri); } - //we need a separate loader for linked contact to ensure refreshing on verification case LOADER_ID_LINKED_CONTACT: { - //passed in args to explicitly specify their need + // we need a separate loader for linked contact + // to ensure refreshing on verification + + // passed in args to explicitly specify their need long masterKeyId = args.getLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID); boolean isSecret = args.getBoolean(LOADER_EXTRA_LINKED_CONTACT_IS_SECRET); - Uri baseUri; - if (isSecret) - baseUri = ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI; - else - baseUri = ContactsContract.RawContacts.CONTENT_URI; + Uri baseUri = isSecret ? ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI : + ContactsContract.RawContacts.CONTENT_URI; return new CursorLoader( getActivity(), baseUri, - RAWCONTACT_PROJECTION, + RAW_CONTACT_PROJECTION, ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + ContactsContract.RawContacts.SOURCE_ID + "=? AND " + ContactsContract.RawContacts.DELETED + "=?", - new String[]{//"0" for "not deleted" + new String[]{ Constants.ACCOUNT_TYPE, Long.toString(masterKeyId), - "0" + "0" // "0" for "not deleted" }, null); } @@ -396,47 +433,46 @@ public class ViewKeyFragment extends LoaderFragment implements /* TODO better error handling? May cause problems when a key is deleted, * because the notification triggers faster than the activity closes. */ - // Avoid NullPointerExceptions... - if (data == null || data.getCount() == 0) { + if (data == null) { return; } // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) switch (loader.getId()) { case LOADER_ID_UNIFIED: { - if (data.moveToFirst()) { + if (data.getCount() == 1 && data.moveToFirst()) { mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; mFingerprint = data.getBlob(INDEX_FINGERPRINT); long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - // 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); - - if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) { - mLinkedIdsAdapter = - new LinkedIdsAdapter(getActivity(), null, 0, mIsSecret, mLinkedIdsExpander); - mLinkedIds.setAdapter(mLinkedIdsAdapter); - getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this); - } - + // init other things after we know if it's a secret key + initUserIds(mIsSecret); + initLinkedIds(mIsSecret); initLinkedContactLoader(masterKeyId, mIsSecret); - - break; + initCardButtonsVisibility(mIsSecret); } + break; } case LOADER_ID_USER_IDS: { setContentShown(true, false); mUserIdsAdapter.swapCursor(data); + break; } case LOADER_ID_LINKED_IDS: { mLinkedIdsAdapter.swapCursor(data); - mLinkedIdsCard.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.VISIBLE : View.GONE); + + if (mIsSecret) { + mLinkedIdsCard.setVisibility(View.VISIBLE); + mLinkedIdsEmpty.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.GONE : View.VISIBLE); + } else { + mLinkedIdsCard.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.VISIBLE : View.GONE); + mLinkedIdsEmpty.setVisibility(View.GONE); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mPostponeType == PostponeType.LINKED) { mLinkedIdsCard.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { @TargetApi(VERSION_CODES.LOLLIPOP) @@ -452,13 +488,27 @@ public class ViewKeyFragment extends LoaderFragment implements } case LOADER_ID_LINKED_CONTACT: { - if (data.moveToFirst()) {// if we have a linked contact + if (data.moveToFirst()) { // if we have a linked contact long contactId = data.getLong(INDEX_CONTACT_ID); loadLinkedSystemContact(contactId); } break; } + } + } + private void initUserIds(boolean isSecret) { + mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !isSecret, null); + mUserIds.setAdapter(mUserIdsAdapter); + getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + } + + private void initLinkedIds(boolean isSecret) { + if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) { + mLinkedIdsAdapter = + new LinkedIdsAdapter(getActivity(), null, 0, isSecret, mLinkedIdsExpander); + mLinkedIds.setAdapter(mLinkedIdsAdapter); + getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this); } } @@ -478,6 +528,20 @@ public class ViewKeyFragment extends LoaderFragment implements getLoaderManager().initLoader(LOADER_ID_LINKED_CONTACT, linkedContactData, this); } + private void initCardButtonsVisibility(boolean isSecret) { + LinearLayout buttonsUserIdsLayout = + (LinearLayout) getActivity().findViewById(R.id.view_key_card_user_ids_buttons); + LinearLayout buttonsLinkedIdsLayout = + (LinearLayout) getActivity().findViewById(R.id.view_key_card_linked_ids_buttons); + if (isSecret) { + buttonsUserIdsLayout.setVisibility(View.VISIBLE); + buttonsLinkedIdsLayout.setVisibility(View.VISIBLE); + } else { + buttonsUserIdsLayout.setVisibility(View.GONE); + buttonsLinkedIdsLayout.setVisibility(View.GONE); + } + } + /** * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. * We need to make sure we are no longer using it. @@ -490,7 +554,6 @@ public class ViewKeyFragment extends LoaderFragment implements break; } case LOADER_ID_LINKED_IDS: { - mLinkedIdsCard.setVisibility(View.GONE); mLinkedIdsAdapter.swapCursor(null); break; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java index 24f5f04a1..84608f2dc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java @@ -22,6 +22,7 @@ import android.content.res.ColorStateList; import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.Typeface; +import android.support.annotation.Nullable; import android.support.v4.widget.CursorAdapter; import android.text.Spannable; import android.text.SpannableString; @@ -49,7 +50,7 @@ public class SubkeysAdapter extends CursorAdapter { private LayoutInflater mInflater; private SaveKeyringParcel mSaveKeyringParcel; - private boolean hasAnySecret; + private boolean mHasAnySecret; private ColorStateList mDefaultTextColor; public static final String[] SUBKEYS_PROJECTION = new String[]{ @@ -85,16 +86,10 @@ public class SubkeysAdapter extends CursorAdapter { private static final int INDEX_EXPIRY = 13; private static final int INDEX_FINGERPRINT = 14; - public SubkeysAdapter(Context context, Cursor c, int flags, - SaveKeyringParcel saveKeyringParcel) { + public SubkeysAdapter(Context context, Cursor c, int flags) { super(context, c, flags); mInflater = LayoutInflater.from(context); - mSaveKeyringParcel = saveKeyringParcel; - } - - public SubkeysAdapter(Context context, Cursor c, int flags) { - this(context, c, flags, null); } public long getKeyId(int position) { @@ -133,12 +128,12 @@ public class SubkeysAdapter extends CursorAdapter { @Override public Cursor swapCursor(Cursor newCursor) { - hasAnySecret = false; + mHasAnySecret = false; if (newCursor != null && newCursor.moveToFirst()) { do { SecretKeyType hasSecret = SecretKeyType.fromNum(newCursor.getInt(INDEX_HAS_SECRET)); if (hasSecret.isUsable()) { - hasAnySecret = true; + mHasAnySecret = true; break; } } while (newCursor.moveToNext()); @@ -354,4 +349,18 @@ public class SubkeysAdapter extends CursorAdapter { } } + /** Set this adapter into edit mode. This mode displays additional info for + * each item from a supplied SaveKeyringParcel reference. + * + * Note that it is up to the caller to reload the underlying cursor after + * updating the SaveKeyringParcel! + * + * @see SaveKeyringParcel + * + * @param saveKeyringParcel The parcel to get info from, or null to leave edit mode. + */ + public void setEditMode(@Nullable SaveKeyringParcel saveKeyringParcel) { + mSaveKeyringParcel = saveKeyringParcel; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java index e0abaf4b0..31f8513fa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java @@ -1,5 +1,6 @@ package org.sufficientlysecure.keychain.ui.adapter; + import android.content.Context; import android.database.Cursor; import android.support.v4.widget.CursorAdapter; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java index 0f4312dad..7dee90b4e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java @@ -23,12 +23,14 @@ import android.content.Context; import android.database.Cursor; import android.graphics.Typeface; import android.net.Uri; +import android.support.annotation.Nullable; import android.support.v4.content.CursorLoader; import android.view.LayoutInflater; import android.view.View; 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.pgp.KeyRing; @@ -52,10 +54,6 @@ public class UserIdsAdapter extends UserAttributesAdapter { mShowStatusImages = showStatusImages; } - public UserIdsAdapter(Context context, Cursor c, int flags, SaveKeyringParcel saveKeyringParcel) { - this(context, c, flags, true, saveKeyringParcel); - } - public UserIdsAdapter(Context context, Cursor c, int flags) { this(context, c, flags, true, null); } @@ -66,7 +64,7 @@ public class UserIdsAdapter extends UserAttributesAdapter { TextView vAddress = (TextView) view.findViewById(R.id.user_id_item_address); TextView vComment = (TextView) view.findViewById(R.id.user_id_item_comment); ImageView vVerified = (ImageView) view.findViewById(R.id.user_id_item_certified); - View vVerifiedLayout = view.findViewById(R.id.user_id_item_certified_layout); + ViewAnimator vVerifiedLayout = (ViewAnimator) view.findViewById(R.id.user_id_icon_animator); ImageView vEditImage = (ImageView) view.findViewById(R.id.user_id_item_edit_image); ImageView vDeleteButton = (ImageView) view.findViewById(R.id.user_id_item_delete_button); vDeleteButton.setVisibility(View.GONE); // not used @@ -114,16 +112,9 @@ public class UserIdsAdapter extends UserAttributesAdapter { } } - vEditImage.setVisibility(View.VISIBLE); - vVerifiedLayout.setVisibility(View.GONE); + vVerifiedLayout.setDisplayedChild(2); } else { - vEditImage.setVisibility(View.GONE); - - if (mShowStatusImages) { - vVerifiedLayout.setVisibility(View.VISIBLE); - } else { - vVerifiedLayout.setVisibility(View.GONE); - } + vVerifiedLayout.setDisplayedChild(mShowStatusImages ? 1 : 0); } if (isRevoked) { @@ -177,6 +168,20 @@ public class UserIdsAdapter extends UserAttributesAdapter { return isRevokedPending; } + /** Set this adapter into edit mode. This mode displays additional info for + * each item from a supplied SaveKeyringParcel reference. + * + * Note that it is up to the caller to reload the underlying cursor after + * updating the SaveKeyringParcel! + * + * @see SaveKeyringParcel + * + * @param saveKeyringParcel The parcel to get info from, or null to leave edit mode. + */ + public void setEditMode(@Nullable SaveKeyringParcel saveKeyringParcel) { + mSaveKeyringParcel = saveKeyringParcel; + } + @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return mInflater.inflate(R.layout.view_key_adv_user_id_item, null); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAddedAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAddedAdapter.java index c7197b46d..b1892b27e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAddedAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAddedAdapter.java @@ -72,7 +72,7 @@ public class UserIdsAddedAdapter extends ArrayAdapter<String> { holder.vDelete.setVisibility(View.VISIBLE); // always visible // not used: - View certifiedLayout = convertView.findViewById(R.id.user_id_item_certified_layout); + View certifiedLayout = convertView.findViewById(R.id.user_id_icon_animator); ImageView editImage = (ImageView) convertView.findViewById(R.id.user_id_item_edit_image); certifiedLayout.setVisibility(View.GONE); editImage.setVisibility(View.GONE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsSelectableAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsSelectableAdapter.java index 947d911c3..3cc2e2044 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsSelectableAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsSelectableAdapter.java @@ -18,8 +18,8 @@ public class UserIdsSelectableAdapter extends UserIdsAdapter implements AdapterV private final ArrayList<Boolean> mCheckStates; - public UserIdsSelectableAdapter(Context context, Cursor c, int flags, SaveKeyringParcel saveKeyringParcel) { - super(context, c, flags, saveKeyringParcel); + public UserIdsSelectableAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); mCheckStates = new ArrayList<Boolean>(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java index bc82feb70..4500ccd24 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java @@ -48,6 +48,7 @@ import org.sufficientlysecure.keychain.util.Log; public class AddUserIdDialogFragment extends DialogFragment implements OnEditorActionListener { private static final String ARG_MESSENGER = "messenger"; private static final String ARG_NAME = "name"; + private static final String ARG_ALLOW_COMMENT = "allow_comment"; public static final int MESSAGE_OKAY = 1; public static final int MESSAGE_CANCEL = 2; @@ -59,12 +60,14 @@ public class AddUserIdDialogFragment extends DialogFragment implements OnEditorA private EmailEditText mEmail; private EditText mComment; - public static AddUserIdDialogFragment newInstance(Messenger messenger, String predefinedName) { + public static AddUserIdDialogFragment newInstance(Messenger messenger, String predefinedName, + boolean allowComment) { AddUserIdDialogFragment frag = new AddUserIdDialogFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_MESSENGER, messenger); args.putString(ARG_NAME, predefinedName); + args.putBoolean(ARG_ALLOW_COMMENT, allowComment); frag.setArguments(args); return frag; @@ -78,6 +81,7 @@ public class AddUserIdDialogFragment extends DialogFragment implements OnEditorA final Activity activity = getActivity(); mMessenger = getArguments().getParcelable(ARG_MESSENGER); String predefinedName = getArguments().getString(ARG_NAME); + boolean allowComment = getArguments().getBoolean(ARG_ALLOW_COMMENT); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); @@ -91,6 +95,12 @@ public class AddUserIdDialogFragment extends DialogFragment implements OnEditorA mEmail = (EmailEditText) view.findViewById(R.id.add_user_id_address); mComment = (EditText) view.findViewById(R.id.add_user_id_comment); + if (allowComment) { + mComment.setVisibility(View.VISIBLE); + } else { + mComment.setVisibility(View.GONE); + } + mName.setText(predefinedName); alert.setPositiveButton(android.R.string.ok, new OnClickListener() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java index 8e45a20e9..758e63eb1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java @@ -1,8 +1,3 @@ -package org.sufficientlysecure.keychain.ui.util; - -/** - * Created by rohan on 20/9/15. - */ /* * Copyright 2012 Google Inc. * @@ -19,14 +14,18 @@ package org.sufficientlysecure.keychain.ui.util; * limitations under the License. */ +package org.sufficientlysecure.keychain.ui.util; + import android.content.Context; import android.graphics.Rect; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.widget.Toast; + public class ContentDescriptionHint { private static final int ESTIMATED_TOAST_HEIGHT_DIPS = 48; + public static void setup(View view) { view.setOnLongClickListener(new View.OnLongClickListener() { @Override |