open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
2018-07-17 14:25:14 +02:00

364 lines
14 KiB
Java

/*
* Copyright (C) 2017 Schürmann & Breitmoser GbR
*
* 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.base;
import java.util.Date;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Handler;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.os.CancellationSignal;
import org.sufficientlysecure.keychain.operations.results.InputPendingResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.service.KeychainServiceTask;
import org.sufficientlysecure.keychain.service.KeychainServiceTask.OperationCallback;
import org.sufficientlysecure.keychain.service.ProgressDialogManager;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
import org.sufficientlysecure.keychain.ui.RetryUploadDialogActivity;
import org.sufficientlysecure.keychain.ui.SecurityTokenOperationActivity;
import timber.log.Timber;
/**
* Designed to be integrated into activities or fragments used for CryptoOperations.
* Encapsulates the execution of a crypto operation and handling of input pending cases.s
*
* @param <T> The type of input parcel sent to the operation
* @param <S> The type of result returned by the operation
*/
public class CryptoOperationHelper<T extends Parcelable, S extends OperationResult> {
private long operationStartTime;
public interface Callback<T extends Parcelable, S extends OperationResult> {
T createOperationInput();
void onCryptoOperationSuccess(S result);
void onCryptoOperationCancelled();
void onCryptoOperationError(S result);
boolean onCryptoSetProgress(String msg, int progress, int max);
}
public static abstract class AbstractCallback<T extends Parcelable, S extends OperationResult>
implements Callback<T,S> {
@Override
public void onCryptoOperationCancelled() {
throw new UnsupportedOperationException("Unexpectedly cancelled operation!!");
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
}
// request codes from CryptoOperationHelper are created essentially
// a static property, used to identify requestCodes meant for this
// particular helper. a request code looks as follows:
// (id << 9) + (1<<8) + REQUEST_CODE_X
// that is, starting from LSB, there are 8 bits request code, 1
// fixed bit set, then 7 bit helper-id code. the first two
// summands are stored in the mHelperId for easy operation.
private final int mHelperId;
// bitmask for helperId is everything except the least 8 bits
private static final int HELPER_ID_BITMASK = ~0xff;
private static final int REQUEST_CODE_PASSPHRASE = 1;
private static final int REQUEST_CODE_NFC = 2;
private static final int REQUEST_CODE_ENABLE_ORBOT = 3;
private static final int REQUEST_CODE_RETRY_UPLOAD = 4;
private Integer mProgressMessageResource;
private boolean mCancellable = false;
private Long minimumOperationDelay;
private FragmentActivity mActivity;
private Fragment mFragment;
private Callback<T, S> mCallback;
private boolean mUseFragment; // short hand for mActivity == null
/**
* If OperationHelper is being integrated into an activity
*/
public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback,
Integer progressMessageString) {
mHelperId = (id << 9) + (1<<8);
mActivity = activity;
mUseFragment = false;
mCallback = callback;
mProgressMessageResource = progressMessageString;
}
/**
* if OperationHelper is being integrated into a fragment
*/
public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) {
mHelperId = (id << 9) + (1<<8);
mFragment = fragment;
mUseFragment = true;
mProgressMessageResource = progressMessageString;
mCallback = callback;
}
public void setProgressMessageResource(int id) {
mProgressMessageResource = id;
}
public void setOperationMinimumDelay(Long delay) {
this.minimumOperationDelay = delay;
}
public void setProgressCancellable(boolean cancellable) {
mCancellable = cancellable;
}
private void initiateInputActivity(RequiredInputParcel requiredInput,
CryptoInputParcel cryptoInputParcel) {
Activity activity = mUseFragment ? mFragment.getActivity() : mActivity;
switch (requiredInput.mType) {
// always use CryptoOperationHelper.startActivityForResult!
case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
case SECURITY_TOKEN_DECRYPT:
case SECURITY_TOKEN_SIGN: {
Intent intent = new Intent(activity, SecurityTokenOperationActivity.class);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
startActivityForResult(intent, REQUEST_CODE_NFC);
return;
}
case PASSPHRASE:
case PASSPHRASE_SYMMETRIC:
case BACKUP_CODE:
case NUMERIC_9X4:
case NUMERIC_9X4_AUTOCRYPT: {
Intent intent = new Intent(activity, PassphraseDialogActivity.class);
intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput);
intent.putExtra(PassphraseDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
return;
}
case ENABLE_ORBOT: {
Intent intent = new Intent(activity, OrbotRequiredDialogActivity.class);
intent.putExtra(OrbotRequiredDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
startActivityForResult(intent, REQUEST_CODE_ENABLE_ORBOT);
return;
}
case UPLOAD_FAIL_RETRY: {
Intent intent = new Intent(activity, RetryUploadDialogActivity.class);
intent.putExtra(RetryUploadDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
startActivityForResult(intent, REQUEST_CODE_RETRY_UPLOAD);
return;
}
default: {
throw new RuntimeException("Unhandled pending result!");
}
}
}
protected void startActivityForResult(Intent intent, int requestCode) {
if (mUseFragment) {
mFragment.startActivityForResult(intent, mHelperId + requestCode);
} else {
mActivity.startActivityForResult(intent, mHelperId + requestCode);
}
}
/**
* Attempts the result of an activity started by this helper. Returns true if requestCode is
* recognized, false otherwise.
* @return true if requestCode was recognized, false otherwise
*/
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
Timber.d("received activity result in OperationHelper");
if ((requestCode & HELPER_ID_BITMASK) != mHelperId) {
// this wasn't meant for us to handle
return false;
}
Timber.d("handling activity result in OperationHelper");
// filter out mHelperId from requestCode
requestCode ^= mHelperId;
if (resultCode == Activity.RESULT_CANCELED) {
mCallback.onCryptoOperationCancelled();
return true;
}
switch (requestCode) {
case REQUEST_CODE_PASSPHRASE: {
if (resultCode == Activity.RESULT_OK && data != null) {
CryptoInputParcel cryptoInput =
data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
cryptoOperation(cryptoInput);
}
break;
}
case REQUEST_CODE_NFC: {
if (resultCode == Activity.RESULT_OK && data != null) {
CryptoInputParcel cryptoInput =
data.getParcelableExtra(SecurityTokenOperationActivity.RESULT_CRYPTO_INPUT);
cryptoOperation(cryptoInput);
}
break;
}
case REQUEST_CODE_ENABLE_ORBOT: {
if (resultCode == Activity.RESULT_OK && data != null) {
CryptoInputParcel cryptoInput =
data.getParcelableExtra(
OrbotRequiredDialogActivity.RESULT_CRYPTO_INPUT);
cryptoOperation(cryptoInput);
}
break;
}
case REQUEST_CODE_RETRY_UPLOAD: {
if (resultCode == Activity.RESULT_OK) {
CryptoInputParcel cryptoInput =
data.getParcelableExtra(
RetryUploadDialogActivity.RESULT_CRYPTO_INPUT);
cryptoOperation(cryptoInput);
}
break;
}
}
return true;
}
public void cryptoOperation(final CryptoInputParcel cryptoInput) {
T operationInput = mCallback.createOperationInput();
if (operationInput == null) {
return;
}
FragmentActivity activity = mUseFragment ? mFragment.getActivity() : mActivity;
if (activity == null) {
throw new NullPointerException();
}
ProgressDialogManager progressDialogManager;
if (mProgressMessageResource != null) {
progressDialogManager = new ProgressDialogManager(activity);
} else {
progressDialogManager = null;
}
KeychainServiceTask keychainServiceTask = KeychainServiceTask.create(activity);
OperationCallback operationCallback = new OperationCallback() {
@Override
public void operationFinished(OperationResult result) {
if (progressDialogManager != null) {
progressDialogManager.dismissAllowingStateLoss();
}
onHandleResult(result);
}
@Override
public void setProgress(Integer resourceId, int current, int total) {
String msgString = resourceId != null ? activity.getString(resourceId) : null;
if (mCallback.onCryptoSetProgress(msgString, current, total)) {
return;
}
if (progressDialogManager != null) {
progressDialogManager.onSetProgress(resourceId, current, total);
}
}
@Override
public void setPreventCancel() {
if (progressDialogManager != null) {
progressDialogManager.setPreventCancel();
}
}
};
CancellationSignal cancellationSignal =
keychainServiceTask.startOperationInBackground(operationInput, cryptoInput, operationCallback);
if (progressDialogManager != null) {
progressDialogManager.showProgressDialog(activity.getString(mProgressMessageResource),
ProgressDialog.STYLE_HORIZONTAL, mCancellable ? cancellationSignal : null);
}
}
public void cryptoOperation() {
operationStartTime = SystemClock.elapsedRealtime();
cryptoOperation(CryptoInputParcel.createCryptoInputParcel(new Date()));
}
@UiThread
private void onHandleResult(OperationResult result) {
Timber.d("Handling result in OperationHelper success: %s", result.success());
if (result instanceof InputPendingResult) {
InputPendingResult pendingResult = (InputPendingResult) result;
if (pendingResult.isPending()) {
RequiredInputParcel requiredInput = pendingResult.getRequiredInputParcel();
initiateInputActivity(requiredInput, pendingResult.mCryptoInputParcel);
return;
}
}
long elapsedTime = SystemClock.elapsedRealtime() - operationStartTime;
if (minimumOperationDelay == null || elapsedTime > minimumOperationDelay) {
returnResultToCallback(result);
return;
}
long artificialDelay = minimumOperationDelay - elapsedTime;
new Handler().postDelayed(() -> returnResultToCallback(result), artificialDelay);
}
private void returnResultToCallback(OperationResult result) {
try {
if (result.success()) {
// noinspection unchecked, because type erasure :(
mCallback.onCryptoOperationSuccess((S) result);
} else {
// noinspection unchecked, because type erasure :(
mCallback.onCryptoOperationError((S) result);
}
} catch (ClassCastException e) {
throw new AssertionError("bad return class ("
+ result.getClass().getSimpleName() + "), this is a programming error!");
}
}
}