diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui')
13 files changed, 849 insertions, 239 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java new file mode 100644 index 000000000..ff120c9b5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + + +public class BackupActivity extends BaseActivity { + + public static final String EXTRA_MASTER_KEY_IDS = "master_key_ids"; + public static final String EXTRA_SECRET = "export_secret"; + + @Override + protected void initLayout() { + setContentView(R.layout.backup_activity); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // noinspection ConstantConditions, we know this activity has an action bar + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + if (savedInstanceState == null) { + Intent intent = getIntent(); + boolean exportSecret = intent.getBooleanExtra(EXTRA_SECRET, false); + long[] masterKeyIds = intent.getLongArrayExtra(EXTRA_MASTER_KEY_IDS); + + Fragment frag = BackupCodeFragment.newInstance(masterKeyIds, exportSecret); + + FragmentManager fragMan = getSupportFragmentManager(); + fragMan.beginTransaction() + .setCustomAnimations(0, 0) + .replace(R.id.content_frame, frag) + .commit(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + FragmentManager fragMan = getSupportFragmentManager(); + // pop from back stack, or if nothing was on there finish activity + if ( ! fragMan.popBackStackImmediate()) { + finish(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java new file mode 100644 index 000000000..be1399a30 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java @@ -0,0 +1,550 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.OnBackStackChangedListener; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.widget.EditText; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator; +import org.sufficientlysecure.keychain.util.FileHelper; +import org.sufficientlysecure.keychain.util.Passphrase; + + +public class BackupCodeFragment extends CryptoOperationFragment<ExportKeyringParcel,ExportResult> + implements OnBackStackChangedListener { + + public static final String ARG_BACKUP_CODE = "backup_code"; + public static final String BACK_STACK_INPUT = "state_display"; + public static final String ARG_EXPORT_SECRET = "export_secret"; + public static final String ARG_MASTER_KEY_IDS = "master_key_ids"; + + public static final String ARG_CURRENT_STATE = "current_state"; + + public static final int REQUEST_SAVE = 1; + public static final String ARG_BACK_STACK = "back_stack"; + + // argument variables + private boolean mExportSecret; + private long[] mMasterKeyIds; + String mBackupCode; + + private EditText[] mCodeEditText; + private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator; + + private Integer mBackStackLevel; + + private Uri mCachedExportUri; + private boolean mShareNotSave; + + public static BackupCodeFragment newInstance(long[] masterKeyIds, boolean exportSecret) { + BackupCodeFragment frag = new BackupCodeFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_BACKUP_CODE, generateRandomCode()); + args.putLongArray(ARG_MASTER_KEY_IDS, masterKeyIds); + args.putBoolean(ARG_EXPORT_SECRET, exportSecret); + frag.setArguments(args); + + return frag; + } + + enum BackupCodeState { + STATE_UNINITIALIZED, STATE_DISPLAY, STATE_INPUT, STATE_INPUT_ERROR, STATE_OK + } + + BackupCodeState mCurrentState = BackupCodeState.STATE_UNINITIALIZED; + + void switchState(BackupCodeState state, boolean animate) { + + switch (state) { + case STATE_UNINITIALIZED: + throw new AssertionError("can't switch to uninitialized state, this is a bug!"); + + case STATE_DISPLAY: + mTitleAnimator.setDisplayedChild(0, animate); + mStatusAnimator.setDisplayedChild(0, animate); + mCodeFieldsAnimator.setDisplayedChild(0, animate); + + break; + + case STATE_INPUT: + mTitleAnimator.setDisplayedChild(1, animate); + mStatusAnimator.setDisplayedChild(1, animate); + mCodeFieldsAnimator.setDisplayedChild(1, animate); + + for (EditText editText : mCodeEditText) { + editText.setText(""); + } + + pushBackStackEntry(); + + break; + + case STATE_INPUT_ERROR: { + mTitleAnimator.setDisplayedChild(1, false); + mStatusAnimator.setDisplayedChild(2, animate); + mCodeFieldsAnimator.setDisplayedChild(1, false); + + hideKeyboard(); + + if (animate) { + @ColorInt int black = mCodeEditText[0].getCurrentTextColor(); + @ColorInt int red = getResources().getColor(R.color.android_red_dark); + animateFlashText(mCodeEditText, black, red, false); + } + + break; + } + + case STATE_OK: { + mTitleAnimator.setDisplayedChild(2, animate); + mStatusAnimator.setDisplayedChild(3, animate); + mCodeFieldsAnimator.setDisplayedChild(1, false); + + hideKeyboard(); + + for (EditText editText : mCodeEditText) { + editText.setEnabled(false); + } + + @ColorInt int green = getResources().getColor(R.color.android_green_dark); + if (animate) { + @ColorInt int black = mCodeEditText[0].getCurrentTextColor(); + animateFlashText(mCodeEditText, black, green, true); + } else { + for (TextView textView : mCodeEditText) { + textView.setTextColor(green); + } + } + + popBackStackNoAction(); + + break; + } + + } + + mCurrentState = state; + + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.backup_code_fragment, container, false); + + Bundle args = getArguments(); + mBackupCode = args.getString(ARG_BACKUP_CODE); + mMasterKeyIds = args.getLongArray(ARG_MASTER_KEY_IDS); + mExportSecret = args.getBoolean(ARG_EXPORT_SECRET); + + mCodeEditText = new EditText[4]; + mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1); + mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2); + mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3); + mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4); + + { + TextView[] codeDisplayText = new TextView[4]; + codeDisplayText[0] = (TextView) view.findViewById(R.id.backup_code_display_1); + codeDisplayText[1] = (TextView) view.findViewById(R.id.backup_code_display_2); + codeDisplayText[2] = (TextView) view.findViewById(R.id.backup_code_display_3); + codeDisplayText[3] = (TextView) view.findViewById(R.id.backup_code_display_4); + + // set backup code in code TextViews + char[] backupCode = mBackupCode.toCharArray(); + for (int i = 0; i < codeDisplayText.length; i++) { + codeDisplayText[i].setText(backupCode, i * 7, 6); + } + + // set background to null in TextViews - this will retain padding from EditText style! + for (TextView textView : codeDisplayText) { + // noinspection deprecation, setBackground(Drawable) is API level >=16 + textView.setBackgroundDrawable(null); + } + } + + setupEditTextFocusNext(mCodeEditText); + setupEditTextSuccessListener(mCodeEditText); + + mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator); + mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator); + mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator); + + View backupInput = view.findViewById(R.id.button_backup_input); + backupInput.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + switchState(BackupCodeState.STATE_INPUT, true); + } + }); + + view.findViewById(R.id.button_backup_save).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mShareNotSave = false; + startBackup(); + } + }); + + view.findViewById(R.id.button_backup_share).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mShareNotSave = true; + startBackup(); + } + }); + + view.findViewById(R.id.button_backup_back).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + FragmentManager fragMan = getFragmentManager(); + if (fragMan != null) { + fragMan.popBackStack(); + } + } + }); + + return view; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (savedInstanceState != null) { + int savedBackStack = savedInstanceState.getInt(ARG_BACK_STACK); + if (savedBackStack >= 0) { + mBackStackLevel = savedBackStack; + // unchecked use, we know that this one is available in onViewCreated + getFragmentManager().addOnBackStackChangedListener(this); + } + BackupCodeState savedState = BackupCodeState.values()[savedInstanceState.getInt(ARG_CURRENT_STATE)]; + switchState(savedState, false); + } else if (mCurrentState == BackupCodeState.STATE_UNINITIALIZED) { + switchState(BackupCodeState.STATE_DISPLAY, true); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(ARG_CURRENT_STATE, mCurrentState.ordinal()); + outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel); + } + + private void setupEditTextSuccessListener(final EditText[] backupCodes) { + for (EditText backupCode : backupCodes) { + + backupCode.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() > 6) { + throw new AssertionError("max length of each field is 6!"); + } + + boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT + || mCurrentState == BackupCodeState.STATE_INPUT_ERROR; + boolean partIsComplete = s.length() == 6; + if (!inInputState || !partIsComplete) { + return; + } + + checkIfCodeIsCorrect(); + } + }); + + } + } + + private void checkIfCodeIsCorrect() { + + StringBuilder backupCodeInput = new StringBuilder(26); + for (EditText editText : mCodeEditText) { + if (editText.getText().length() < 6) { + return; + } + backupCodeInput.append(editText.getText()); + backupCodeInput.append('-'); + } + backupCodeInput.deleteCharAt(backupCodeInput.length() - 1); + + // if they don't match, do nothing + if (backupCodeInput.toString().equals(mBackupCode)) { + switchState(BackupCodeState.STATE_OK, true); + return; + } + + // TODO remove debug code + if (backupCodeInput.toString().startsWith("ABC")) { + switchState(BackupCodeState.STATE_OK, true); + return; + } + + switchState(BackupCodeState.STATE_INPUT_ERROR, true); + + } + + private static void animateFlashText( + final TextView[] textViews, int color1, int color2, boolean staySecondColor) { + + ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + for (TextView textView : textViews) { + textView.setTextColor((Integer) animator.getAnimatedValue()); + } + } + }); + anim.setRepeatMode(ValueAnimator.REVERSE); + anim.setRepeatCount(staySecondColor ? 4 : 5); + anim.setDuration(180); + anim.setInterpolator(new AccelerateInterpolator()); + anim.start(); + + } + + private static void setupEditTextFocusNext(final EditText[] backupCodes) { + for (int i = 0; i < backupCodes.length -1; i++) { + + final int next = i+1; + + backupCodes[i].addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + boolean inserting = before < count; + boolean cursorAtEnd = (start + count) == 6; + + if (inserting && cursorAtEnd) { + backupCodes[next].requestFocus(); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + } + } + + private void pushBackStackEntry() { + if (mBackStackLevel != null) { + return; + } + FragmentManager fragMan = getFragmentManager(); + mBackStackLevel = fragMan.getBackStackEntryCount(); + fragMan.beginTransaction().addToBackStack(BACK_STACK_INPUT).commit(); + fragMan.addOnBackStackChangedListener(this); + } + + private void popBackStackNoAction() { + FragmentManager fragMan = getFragmentManager(); + fragMan.removeOnBackStackChangedListener(this); + fragMan.popBackStackImmediate(BACK_STACK_INPUT, FragmentManager.POP_BACK_STACK_INCLUSIVE); + mBackStackLevel = null; + } + + @Override + public void onBackStackChanged() { + FragmentManager fragMan = getFragmentManager(); + if (mBackStackLevel != null && fragMan.getBackStackEntryCount() == mBackStackLevel) { + fragMan.removeOnBackStackChangedListener(this); + switchState(BackupCodeState.STATE_DISPLAY, true); + mBackStackLevel = null; + } + } + + private void startBackup() { + + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + if (mCachedExportUri == null) { + mCachedExportUri = TemporaryStorageProvider.createFile(activity); + cryptoOperation(); + return; + } + + if (mShareNotSave) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("application/octet-stream"); + intent.putExtra(Intent.EXTRA_STREAM, mCachedExportUri); + startActivity(intent); + } else { + saveFile(false); + } + + } + + private void saveFile(boolean overwrite) { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + + // for kitkat and above, we have the document api + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + String filename = "openkeychain_backup_" + date + (mExportSecret ? ".gpg" : ".pub.gpg"); + FileHelper.saveDocument(this, "application/octet-stream", filename, REQUEST_SAVE); + return; + } + + File file = new File(Constants.Path.APP_DIR, "backup_" + date + (mExportSecret ? ".gpg" : ".pub.gpg")); + + if (!overwrite && file.exists()) { + Notify.create(activity, R.string.snack_backup_exists, Style.WARN, new ActionListener() { + @Override + public void onAction() { + saveFile(true); + } + }, R.string.snack_btn_overwrite).show(); + return; + } + + try { + FileHelper.copyUriData(activity, mCachedExportUri, Uri.fromFile(file)); + Notify.create(activity, R.string.snack_backup_saved_dir, Style.OK).show(); + } catch (IOException e) { + Notify.create(activity, R.string.snack_backup_error_saving, Style.ERROR).show(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_SAVE) { + super.onActivityResult(requestCode, resultCode, data); + return; + } + + if (resultCode != FragmentActivity.RESULT_OK) { + return; + } + + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + try { + Uri outputUri = data.getData(); + FileHelper.copyUriData(activity, mCachedExportUri, outputUri); + Notify.create(activity, R.string.snack_backup_saved, Style.OK).show(); + } catch (IOException e) { + Notify.create(activity, R.string.snack_backup_error_saving, Style.ERROR).show(); + } + } + + @Nullable + @Override + public ExportKeyringParcel createOperationInput() { + // TODO replace debug code with real thing + // return new ExportKeyringParcel(new Passphrase(mBackupCode), mMasterKeyIds, mExportSecret, mCachedExportUri); + return new ExportKeyringParcel(new Passphrase("abc"), mMasterKeyIds, mExportSecret, mCachedExportUri); + } + + @Override + public void onCryptoOperationSuccess(ExportResult result) { + startBackup(); + } + + @Override + public void onCryptoOperationError(ExportResult result) { + result.createNotify(getActivity()).show(); + mCachedExportUri = null; + } + + @Override + public void onCryptoOperationCancelled() { + mCachedExportUri = null; + } + + @NonNull + private static String generateRandomCode() { + + Random r = new SecureRandom(); + + // simple generation of a 20 character backup code + StringBuilder code = new StringBuilder(28); + for (int i = 0; i < 24; i++) { + if (i == 6 || i == 12 || i == 18) { + code.append('-'); + } + code.append((char) ('A' + r.nextInt(26))); + } + + return code.toString(); + + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java index 739eb3e35..b79e4454d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java @@ -37,17 +37,17 @@ import org.spongycastle.bcpg.sig.KeyFlags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; +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.ProviderHelper; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; @@ -69,7 +69,7 @@ public class CreateKeyFinalFragment extends Fragment { SaveKeyringParcel mSaveKeyringParcel; - private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mCreateOpHelper; private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mMoveToCardOpHelper; @@ -407,20 +407,20 @@ public class CreateKeyFinalFragment extends Fragment { } // set data uri as path to keyring - final Uri blobUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(saveKeyResult.mMasterKeyId); + final long masterKeyId = saveKeyResult.mMasterKeyId; // upload to favorite keyserver final String keyserver = Preferences.getPreferences(activity).getPreferredKeyserver(); - CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> callback - = new CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult>() { + CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> callback + = new CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult>() { @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(keyserver, blobUri); + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(keyserver, masterKeyId); } @Override - public void onCryptoOperationSuccess(ExportResult result) { + public void onCryptoOperationSuccess(UploadResult result) { handleResult(result); } @@ -430,11 +430,11 @@ public class CreateKeyFinalFragment extends Fragment { } @Override - public void onCryptoOperationError(ExportResult result) { + public void onCryptoOperationError(UploadResult result) { handleResult(result); } - public void handleResult(ExportResult result) { + public void handleResult(UploadResult result) { saveKeyResult.getLog().add(result, 0); finishWithResult(saveKeyResult); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java index a0650f8b1..13838e77c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipDescription; import android.content.Context; @@ -36,6 +37,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Parcelable; import android.support.v7.widget.DefaultItemAnimator; @@ -249,6 +251,7 @@ public class DecryptListFragment } } + @TargetApi(VERSION_CODES.KITKAT) private void saveFileDialog(InputDataResult result, int index) { Activity activity = getActivity(); @@ -260,13 +263,13 @@ public class DecryptListFragment mCurrentSaveFileUri = result.getOutputUris().get(index); String filename = metadata.getFilename(); - if (filename == null) { + if (TextUtils.isEmpty(filename)) { String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(metadata.getMimeType()); filename = "decrypted" + (ext != null ? "."+ext : ""); } - FileHelper.saveDocument(this, filename, metadata.getMimeType(), - REQUEST_CODE_OUTPUT); + // requires >=kitkat + FileHelper.saveDocument(this, metadata.getMimeType(), filename, REQUEST_CODE_OUTPUT); } private void saveFile(Uri saveUri) { @@ -374,6 +377,9 @@ public class DecryptListFragment if (ClipDescription.compareMimeTypes(type, "text/plain")) { // noinspection deprecation, this should be called from Context, but not available in minSdk icon = getResources().getDrawable(R.drawable.ic_chat_black_24dp); + } else if (ClipDescription.compareMimeTypes(type, "application/pgp-keys")) { + // noinspection deprecation, this should be called from Context, but not available in minSdk + icon = getResources().getDrawable(R.drawable.ic_key_plus_grey600_24dp); } else if (ClipDescription.compareMimeTypes(type, "image/*")) { int px = FormattingUtils.dpToPx(context, 32); Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px)); @@ -764,11 +770,14 @@ public class DecryptListFragment String filename; if (metadata == null) { filename = getString(R.string.filename_unknown); - } else if (TextUtils.isEmpty(metadata.getFilename())) { - filename = getString("text/plain".equals(metadata.getMimeType()) - ? R.string.filename_unknown_text : R.string.filename_unknown); - } else { + } else if ( ! TextUtils.isEmpty(metadata.getFilename())) { filename = metadata.getFilename(); + } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), "application/pgp-keys")) { + filename = getString(R.string.filename_keys); + } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), "text/plain")) { + filename = getString(R.string.filename_unknown_text); + } else { + filename = getString(R.string.filename_unknown); } fileHolder.vFilename.setText(filename); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java index a3ea8ad9a..cf47dfc94 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java @@ -18,11 +18,7 @@ package org.sufficientlysecure.keychain.ui; -import java.io.File; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; -import java.util.Locale; import android.app.Activity; import android.content.ContentResolver; @@ -37,13 +33,11 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.util.ExportHelper; -public class BackupFragment extends Fragment { +public class DrawerBackupFragment extends Fragment { // This ids for multiple key export. private ArrayList<Long> mIdsForRepeatAskPassphrase; @@ -51,24 +45,10 @@ public class BackupFragment extends Fragment { private int mIndex; static final int REQUEST_REPEAT_PASSPHRASE = 1; - private ExportHelper mExportHelper; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - // we won't get attached to a non-fragment activity, so the cast should be safe - mExportHelper = new ExportHelper((FragmentActivity) activity); - } - - @Override - public void onDetach() { - super.onDetach(); - mExportHelper = null; - } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.backup_fragment, container, false); + View view = inflater.inflate(R.layout.drawer_backup_fragment, container, false); View backupAll = view.findViewById(R.id.backup_all); View backupPublicKeys = view.findViewById(R.id.backup_public_keys); @@ -187,14 +167,11 @@ public class BackupFragment extends Fragment { } private void startBackup(boolean exportSecret) { - File filename; - String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - if (exportSecret) { - filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc"); - } else { - filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc"); - } - mExportHelper.showExportKeysDialog(null, filename, exportSecret); + + Intent intent = new Intent(getActivity(), BackupActivity.class); + intent.putExtra(BackupActivity.EXTRA_SECRET, exportSecret); + startActivity(intent); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java index 19603a549..bfe9ea290 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java @@ -25,6 +25,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; @@ -34,6 +35,7 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.net.Uri; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v7.widget.DefaultItemAnimator; @@ -215,6 +217,7 @@ public class EncryptFilesFragment mSelectedFiles.requestFocus(); } + @TargetApi(VERSION_CODES.KITKAT) private void showOutputFileDialog() { if (mFilesAdapter.getModelCount() != 1) { throw new IllegalStateException(); @@ -223,8 +226,7 @@ public class EncryptFilesFragment String targetName = (mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri)) + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); - FileHelper.saveDocument(this, targetName, - REQUEST_CODE_OUTPUT); + FileHelper.saveDocument(this, targetName, REQUEST_CODE_OUTPUT); } public void addFile(Intent data) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index c578bcf15..7f7532ddf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -21,8 +21,8 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.os.Message; import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -33,10 +33,8 @@ import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -78,10 +76,8 @@ public class ImportKeysActivity extends BaseNfcActivity public static final String EXTRA_PENDING_INTENT_DATA = "data"; private Intent mPendingIntentData; - // view - private ImportKeysListFragment mListFragment; - private Fragment mTopFragment; - private View mImportButton; + public static final String TAG_FRAG_LIST = "frag_list"; + public static final String TAG_FRAG_TOP = "frag_top"; // for CryptoOperationHelper.Callback private String mKeyserver; @@ -94,15 +90,22 @@ public class ImportKeysActivity extends BaseNfcActivity super.onCreate(savedInstanceState); setFullScreenDialogClose(Activity.RESULT_CANCELED, true); - mImportButton = findViewById(R.id.import_import); - mImportButton.setOnClickListener(new OnClickListener() { + findViewById(R.id.import_import).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - importKeys(); + importSelectedKeys(); } }); - handleActions(savedInstanceState, getIntent()); + // only used for OpenPgpService + if (getIntent().hasExtra(EXTRA_PENDING_INTENT_DATA)) { + mPendingIntentData = getIntent().getParcelableExtra(EXTRA_PENDING_INTENT_DATA); + } + + // if we aren't being restored, initialize fragments + if (savedInstanceState == null) { + handleActions(getIntent()); + } } @Override @@ -110,7 +113,7 @@ public class ImportKeysActivity extends BaseNfcActivity setContentView(R.layout.import_keys_activity); } - protected void handleActions(Bundle savedInstanceState, Intent intent) { + protected void handleActions(Intent intent) { String action = intent.getAction(); Bundle extras = intent.getExtras(); Uri dataUri = intent.getData(); @@ -120,14 +123,8 @@ public class ImportKeysActivity extends BaseNfcActivity extras = new Bundle(); } - if (action == null) { - startCloudFragment(savedInstanceState, null, false, null); - startListFragment(savedInstanceState, null, null, null, null); - return; - } - if (Intent.ACTION_VIEW.equals(action)) { - if (scheme.equals("http") || scheme.equals("https")) { + if ("http".equals(scheme) || "https".equals(scheme)) { action = ACTION_SEARCH_KEYSERVER_FROM_URL; } else { // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) @@ -135,20 +132,24 @@ public class ImportKeysActivity extends BaseNfcActivity action = ACTION_IMPORT_KEY; } } + if (action == null) { + // -> switch to default below + action = ""; + } switch (action) { case ACTION_IMPORT_KEY: { - /* Keychain's own Actions */ - startFileFragment(savedInstanceState); - if (dataUri != null) { // action: directly load data - startListFragment(savedInstanceState, null, dataUri, null, null); + startListFragment(null, dataUri, null, null); } else if (extras.containsKey(EXTRA_KEY_BYTES)) { byte[] importData = extras.getByteArray(EXTRA_KEY_BYTES); // action: directly load data - startListFragment(savedInstanceState, importData, null, null, null); + startListFragment(importData, null, null, null); + } else { + startTopFileFragment(); + startListFragment(null, null, null, null); } break; } @@ -156,10 +157,6 @@ public class ImportKeysActivity extends BaseNfcActivity case ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE: case ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT: { - // only used for OpenPgpService - if (extras.containsKey(EXTRA_PENDING_INTENT_DATA)) { - mPendingIntentData = extras.getParcelable(EXTRA_PENDING_INTENT_DATA); - } if (extras.containsKey(EXTRA_QUERY) || extras.containsKey(EXTRA_KEY_ID)) { /* simple search based on query or key id */ @@ -175,10 +172,10 @@ public class ImportKeysActivity extends BaseNfcActivity if (query != null && query.length() > 0) { // display keyserver fragment with query - startCloudFragment(savedInstanceState, query, false, null); + startTopCloudFragment(query, false, null); // action: search immediately - startListFragment(savedInstanceState, null, null, query, null); + startListFragment(null, null, query, null); } else { Log.e(Constants.TAG, "Query is empty!"); return; @@ -194,10 +191,10 @@ public class ImportKeysActivity extends BaseNfcActivity String query = "0x" + fingerprint; // display keyserver fragment with query - startCloudFragment(savedInstanceState, query, true, null); + startTopCloudFragment(query, true, null); // action: search immediately - startListFragment(savedInstanceState, null, null, query, null); + startListFragment(null, null, query, null); } } else { Log.e(Constants.TAG, @@ -208,14 +205,6 @@ public class ImportKeysActivity extends BaseNfcActivity } break; } - case ACTION_IMPORT_KEY_FROM_FILE: { - // NOTE: this only displays the appropriate fragment, no actions are taken - startFileFragment(savedInstanceState); - - // no immediate actions! - startListFragment(savedInstanceState, null, null, null, null); - break; - } case ACTION_SEARCH_KEYSERVER_FROM_URL: { // need to process URL to get search query and keyserver authority String query = dataUri.getQueryParameter("search"); @@ -223,120 +212,88 @@ public class ImportKeysActivity extends BaseNfcActivity // if query not specified, we still allow users to search the keyserver in the link if (query == null) { Notify.create(this, R.string.import_url_warn_no_search_parameter, Notify.LENGTH_INDEFINITE, - Notify.Style.WARN).show(mTopFragment); + Notify.Style.WARN).show(); // we just set the keyserver - startCloudFragment(savedInstanceState, null, false, keyserver); + startTopCloudFragment(null, false, keyserver); // we don't set the keyserver for ImportKeysListFragment since // it'll be set in the cloudSearchPrefs of ImportKeysCloudFragment // which is used when the user clicks on the search button - startListFragment(savedInstanceState, null, null, null, null); + startListFragment(null, null, null, null); } else { // we allow our users to edit the query if they wish - startCloudFragment(savedInstanceState, query, false, keyserver); + startTopCloudFragment(query, false, keyserver); // search immediately - startListFragment(savedInstanceState, null, null, query, keyserver); + startListFragment(null, null, query, keyserver); } break; } + case ACTION_IMPORT_KEY_FROM_FILE: case ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN: { // NOTE: this only displays the appropriate fragment, no actions are taken - startFileFragment(savedInstanceState); - - // no immediate actions! - startListFragment(savedInstanceState, null, null, null, null); + startTopFileFragment(); + startListFragment(null, null, null, null); break; } default: { - startCloudFragment(savedInstanceState, null, false, null); - startListFragment(savedInstanceState, null, null, null, null); + startTopCloudFragment(null, false, null); + startListFragment(null, null, null, null); break; } } } + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + // the only thing we need to take care of for restoring state is + // that the top layout is shown iff it contains a fragment + Fragment topFragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_TOP); + boolean hasTopFragment = topFragment != null; + findViewById(R.id.import_keys_top_layout).setVisibility(hasTopFragment ? View.VISIBLE : View.GONE); + } /** * if the fragment is started with non-null bytes/dataUri/serverQuery, it will immediately * load content * - * @param savedInstanceState * @param bytes bytes containing list of keyrings to import * @param dataUri uri to file to import keyrings from * @param serverQuery query to search for on the keyserver * @param keyserver keyserver authority to search on. If null will use keyserver from * user preferences */ - private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, - String serverQuery, String keyserver) { - // 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 (mListFragment != null) { - return; - } - - mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, - keyserver); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + private void startListFragment(byte[] bytes, Uri dataUri, String serverQuery, String keyserver) { + Fragment listFragment = + ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, keyserver); getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_list_container, mListFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + .replace(R.id.import_keys_list_container, listFragment, TAG_FRAG_LIST) + .commit(); } - private void startFileFragment(Bundle savedInstanceState) { - // 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 (mTopFragment != null) { - return; - } - - // Create an instance of the fragment - mTopFragment = ImportKeysFileFragment.newInstance(); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + private void startTopFileFragment() { + findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE); + Fragment importFileFragment = ImportKeysFileFragment.newInstance(); getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_top_container, mTopFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + .replace(R.id.import_keys_top_container, importFileFragment, TAG_FRAG_TOP) + .commit(); } /** * loads the CloudFragment, which consists of the search bar, search button and settings icon * visually. * - * @param savedInstanceState * @param query search query * @param disableQueryEdit if true, user will not be able to edit the search query * @param keyserver keyserver authority to use for search, if null will use keyserver * specified in user preferences */ - - private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String - keyserver) { - // 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 (mTopFragment != null) { - return; - } - - // Create an instance of the fragment - mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + private void startTopCloudFragment(String query, boolean disableQueryEdit, String keyserver) { + findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE); + Fragment importCloudFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver); getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_top_container, mTopFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + .replace(R.id.import_keys_top_container, importCloudFragment, TAG_FRAG_TOP) + .commit(); } private boolean isFingerprintValid(String fingerprint) { @@ -350,63 +307,32 @@ public class ImportKeysActivity extends BaseNfcActivity } public void loadCallback(final ImportKeysListFragment.LoaderState loaderState) { - mListFragment.loadNew(loaderState); + FragmentManager fragMan = getSupportFragmentManager(); + ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST); + keyListFragment.loadNew(loaderState); } - private void handleMessage(Message message) { - if (message.arg1 == ServiceProgressHandler.MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - Log.e(Constants.TAG, "result == null"); - return; - } + private void importSelectedKeys() { - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction()) - || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) { - Intent intent = new Intent(); - intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); - ImportKeysActivity.this.setResult(RESULT_OK, intent); - ImportKeysActivity.this.finish(); - return; - } - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) { - ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData); - ImportKeysActivity.this.finish(); - return; - } - - result.createNotify(ImportKeysActivity.this) - .show((ViewGroup) findViewById(R.id.import_snackbar)); - } - } + FragmentManager fragMan = getSupportFragmentManager(); + ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST); - /** - * Import keys with mImportData - */ - public void importKeys() { - - if (mListFragment.getSelectedEntries().size() == 0) { + if (keyListFragment.getSelectedEntries().size() == 0) { Notify.create(this, R.string.error_nothing_import_selected, Notify.Style.ERROR) .show((ViewGroup) findViewById(R.id.import_snackbar)); return; } - mOperationHelper = new CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult>( + mOperationHelper = new CryptoOperationHelper<>( 1, this, this, R.string.progress_importing ); - ImportKeysListFragment.LoaderState ls = mListFragment.getLoaderState(); + ImportKeysListFragment.LoaderState ls = keyListFragment.getLoaderState(); if (ls instanceof ImportKeysListFragment.BytesLoaderState) { Log.d(Constants.TAG, "importKeys started"); // get DATA from selected key entries - IteratorWithSize<ParcelableKeyRing> selectedEntries = mListFragment.getSelectedData(); + IteratorWithSize<ParcelableKeyRing> selectedEntries = keyListFragment.getSelectedData(); // instead of giving the entries by Intent extra, cache them into a // file to prevent Java Binder problems on heavy imports @@ -435,7 +361,7 @@ public class ImportKeysActivity extends BaseNfcActivity ArrayList<ParcelableKeyRing> keys = new ArrayList<>(); { // change the format into ParcelableKeyRing - ArrayList<ImportKeysListEntry> entries = mListFragment.getSelectedEntries(); + ArrayList<ImportKeysListEntry> entries = keyListFragment.getSelectedEntries(); for (ImportKeysListEntry entry : entries) { keys.add(new ParcelableKeyRing( entry.getFingerprintHex(), entry.getKeyIdHex(), entry.getExtraData()) @@ -458,24 +384,28 @@ public class ImportKeysActivity extends BaseNfcActivity @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mOperationHelper == null || - !mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { - super.onActivityResult(requestCode, resultCode, data); + if (mOperationHelper != null && + mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + return; } + super.onActivityResult(requestCode, resultCode, data); } public void handleResult(ImportKeyResult result) { - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction()) - || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) { + String intentAction = getIntent().getAction(); + + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(intentAction) + || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(intentAction)) { Intent intent = new Intent(); intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); - ImportKeysActivity.this.setResult(RESULT_OK, intent); - ImportKeysActivity.this.finish(); + setResult(RESULT_OK, intent); + finish(); return; } - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) { - ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData); - ImportKeysActivity.this.finish(); + + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(intentAction)) { + setResult(RESULT_OK, mPendingIntentData); + finish(); return; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index 6f5d98afd..a5bd84d7e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -204,7 +204,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac private void onBackupSelected() { mToolbar.setTitle(R.string.nav_backup); mDrawer.setSelectionByIdentifier(ID_APPS, false); - Fragment frag = new BackupFragment(); + Fragment frag = new DrawerBackupFragment(); setFragment(frag, true); } @@ -265,7 +265,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac } else if (frag instanceof AppsListFragment) { mToolbar.setTitle(R.string.nav_apps); mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_APPS), false); - } else if (frag instanceof BackupFragment) { + } else if (frag instanceof DrawerBackupFragment) { mToolbar.setTitle(R.string.nav_backup); mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_BACKUP), false); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java index 0415128a2..f38e4928d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.ui; + import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -29,10 +30,12 @@ import android.widget.Spinner; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.util.Log; @@ -42,7 +45,7 @@ import org.sufficientlysecure.keychain.util.Preferences; * Sends the selected public key to a keyserver */ public class UploadKeyActivity extends BaseActivity - implements CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> { + implements CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> { private View mUploadButton; private Spinner mKeyServerSpinner; @@ -50,8 +53,8 @@ public class UploadKeyActivity extends BaseActivity // CryptoOperationHelper.Callback vars private String mKeyserver; - private Uri mUnifiedKeyringUri; - private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private long mMasterKeyId; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; @Override protected void onCreate(Bundle savedInstanceState) { @@ -85,6 +88,16 @@ public class UploadKeyActivity extends BaseActivity finish(); return; } + + try { + mMasterKeyId = new ProviderHelper(this).getCachedPublicKeyRing( + KeyRings.buildUnifiedKeyRingUri(mDataUri)).getMasterKeyId(); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "Intent data pointed to bad key!"); + finish(); + return; + } + } @Override @@ -101,13 +114,10 @@ public class UploadKeyActivity extends BaseActivity } private void uploadKey() { - Uri blobUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - mUnifiedKeyringUri = blobUri; - String server = (String) mKeyServerSpinner.getSelectedItem(); mKeyserver = server; - mUploadOpHelper = new CryptoOperationHelper(1, this, this, R.string.progress_uploading); + mUploadOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_uploading); mUploadOpHelper.cryptoOperation(); } @@ -125,12 +135,12 @@ public class UploadKeyActivity extends BaseActivity } @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(mKeyserver, mUnifiedKeyringUri); + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(mKeyserver, mMasterKeyId); } @Override - public void onCryptoOperationSuccess(ExportResult result) { + public void onCryptoOperationSuccess(UploadResult result) { result.createNotify(this).show(); } @@ -140,7 +150,7 @@ public class UploadKeyActivity extends BaseActivity } @Override - public void onCryptoOperationError(ExportResult result) { + public void onCryptoOperationError(UploadResult result) { result.createNotify(this).show(); } 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 de859724b..48edbcbd6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -65,12 +65,14 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; 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.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; @@ -345,7 +347,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivity(homeIntent); return true; } - case R.id.menu_key_view_export_file: { + case R.id.menu_key_view_backup: { startPassphraseActivity(REQUEST_BACKUP); return true; } @@ -395,7 +397,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements MenuItem editKey = menu.findItem(R.id.menu_key_view_edit); editKey.setVisible(mIsSecret); - MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file); + MenuItem exportKey = menu.findItem(R.id.menu_key_view_backup); exportKey.setVisible(mIsSecret); MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity); @@ -450,15 +452,40 @@ public class ViewKeyActivity extends BaseNfcActivity implements } private void startPassphraseActivity(int requestCode) { - Intent intent = new Intent(this, PassphraseDialogActivity.class); - intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId); - startActivityForResult(intent, requestCode); + + if (keyHasPassphrase()) { + Intent intent = new Intent(this, PassphraseDialogActivity.class); + intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId); + startActivityForResult(intent, requestCode); + } else { + startBackupActivity(); + } + } + + private boolean keyHasPassphrase() { + try { + SecretKeyType secretKeyType = + mProviderHelper.getCachedPublicKeyRing(mMasterKeyId).getSecretKeyType(mMasterKeyId); + switch (secretKeyType) { + // all of these make no sense to ask + case PASSPHRASE_EMPTY: + case GNU_DUMMY: + case DIVERT_TO_CARD: + case UNAVAILABLE: + return false; + default: + return true; + } + } catch (NotFoundException e) { + return false; + } } - private void backupToFile() { - new ExportHelper(this).showExportKeysDialog( - mMasterKeyId, new File(Constants.Path.APP_DIR, - KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true); + private void startBackupActivity() { + Intent intent = new Intent(this, BackupActivity.class); + intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[] { mMasterKeyId }); + intent.putExtra(BackupActivity.EXTRA_SECRET, true); + startActivity(intent); } private void deleteKey() { @@ -514,7 +541,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements return; } - backupToFile(); + startBackupActivity(); return; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java index 44323543f..a320ea3b2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java @@ -133,10 +133,7 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen String targetName = "pgpkey.txt"; // TODO: not supported on Android < 4.4 - FileHelper.saveDocument(this, - targetName, - "text/plain", - REQUEST_CODE_OUTPUT); + FileHelper.saveDocument(this, targetName, "text/plain", REQUEST_CODE_OUTPUT); } private void saveFile(Uri uri) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java index 7dfd56430..71f6ecc1a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java @@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.util.FabContainer; */ public class Notify { - public static enum Style { + public enum Style { OK (R.color.android_green_light), WARN(R.color.android_orange_light), ERROR(R.color.android_red_light); public final int mLineColor; @@ -142,6 +142,11 @@ public class Notify { return create(activity, text, LENGTH_LONG, style); } + public static Showable create(Activity activity, int textResId, Style style, + ActionListener actionListener, int actionResId) { + return create(activity, textResId, LENGTH_LONG, style, actionListener, actionResId); + } + public static Showable create(Activity activity, int textResId, int duration, Style style, ActionListener actionListener, int actionResId) { return create(activity, activity.getString(textResId), duration, style, actionListener, actionResId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java index 18e830139..a8274e45a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java @@ -31,6 +31,7 @@ import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.animation.Animation; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; @@ -73,4 +74,29 @@ public class ToolableViewAnimator extends ViewAnimator { } super.addView(child, index, params); } + + @Override + public void setDisplayedChild(int whichChild) { + if (whichChild != getDisplayedChild()) { + super.setDisplayedChild(whichChild); + } + } + + public void setDisplayedChild(int whichChild, boolean animate) { + if (animate) { + setDisplayedChild(whichChild); + return; + } + + Animation savedInAnim = getInAnimation(); + Animation savedOutAnim = getOutAnimation(); + setInAnimation(null); + setOutAnimation(null); + + setDisplayedChild(whichChild); + + setInAnimation(savedInAnim); + setOutAnimation(savedOutAnim); + } + } |