open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java
2017-10-31 15:40:14 +01:00

546 lines
21 KiB
Java

/*
* Copyright (C) 2017 Vincent Breitmoser <look@my.amazin.horse>
*
* 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.token;
import java.util.List;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AlertDialog.Builder;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.PromoteKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.service.input.SecurityTokenChangePinParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
import org.sufficientlysecure.keychain.ui.LogDisplayActivity;
import org.sufficientlysecure.keychain.ui.LogDisplayFragment;
import org.sufficientlysecure.keychain.ui.SecurityTokenOperationActivity;
import org.sufficientlysecure.keychain.ui.SecurityTokenChangePinOperationActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.AbstractCallback;
import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity;
import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpPresenter;
import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpView;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.ui.widget.StatusIndicator;
import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status;
import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
import org.sufficientlysecure.keychain.util.FileHelper;
public class ManageSecurityTokenFragment extends Fragment implements ManageSecurityTokenMvpView, OnClickListener {
private static final String ARG_TOKEN_INFO = "token_info";
public static final int REQUEST_CODE_OPEN_FILE = 0;
public static final int REQUEST_CODE_RESET = 1;
public static final int REQUEST_CODE_CHANGE_PIN = 2;
public static final int PERMISSION_READ_STORAGE = 0;
ManageSecurityTokenMvpPresenter presenter;
private ViewGroup statusLayoutGroup;
private ToolableViewAnimator actionAnimator;
private TextView unlockSubtitle;
ImportKeyringParcel currentImportKeyringParcel;
PromoteKeyringParcel currentPromoteKeyringParcel;
private LayoutInflater layoutInflater;
private StatusIndicator latestStatusIndicator;
public static Fragment newInstance(SecurityTokenInfo tokenInfo) {
ManageSecurityTokenFragment frag = new ManageSecurityTokenFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_TOKEN_INFO, tokenInfo);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
SecurityTokenInfo tokenInfo = args.getParcelable(ARG_TOKEN_INFO);
presenter = new ManageSecurityTokenPresenter(getContext(), getLoaderManager(), tokenInfo);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
this.layoutInflater = inflater;
View view = inflater.inflate(R.layout.create_security_token_import_fragment, container, false);
statusLayoutGroup = (ViewGroup) view.findViewById(R.id.status_indicator_layout);
actionAnimator = (ToolableViewAnimator) view.findViewById(R.id.action_animator);
unlockSubtitle = (TextView) view.findViewById(R.id.button_unlock_subtitle);
view.findViewById(R.id.button_import).setOnClickListener(this);
view.findViewById(R.id.button_view_key).setOnClickListener(this);
view.findViewById(R.id.button_retry).setOnClickListener(this);
view.findViewById(R.id.button_reset_token_1).setOnClickListener(this);
view.findViewById(R.id.button_reset_token_2).setOnClickListener(this);
view.findViewById(R.id.button_reset_token_3).setOnClickListener(this);
view.findViewById(R.id.button_reset_token_4).setOnClickListener(this);
view.findViewById(R.id.button_reset_token_5).setOnClickListener(this);
view.findViewById(R.id.button_unlock).setOnClickListener(this);
view.findViewById(R.id.button_unlock_impossible).setOnClickListener(this);
view.findViewById(R.id.button_load_file).setOnClickListener(this);
view.findViewById(R.id.button_setup).setOnClickListener(this);
setHasOptionsMenu(true);
presenter.setView(this);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.detach();
currentImportKeyringParcel = null;
currentPromoteKeyringParcel = null;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.token_setup, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.view_log: {
presenter.onMenuClickViewLog();
return true;
}
case R.id.change_pin: {
presenter.onMenuClickChangePin();
return true;
}
default: {
return super.onOptionsItemSelected(item);
}
}
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
presenter.onActivityCreated();
}
@Override
public void finishAndShowKey(long masterKeyId) {
Activity activity = getActivity();
Intent viewKeyIntent = new Intent(activity, ViewKeyActivity.class);
// use the imported masterKeyId, not the one from the token, because
// that one might* just have been a subkey of the imported key
viewKeyIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
if (activity instanceof CreateKeyActivity) {
((CreateKeyActivity) activity).finishWithFirstTimeHandling(viewKeyIntent);
} else {
activity.startActivity(viewKeyIntent);
activity.finish();
}
}
@Override
public void statusLineAdd(StatusLine statusLine) {
if (latestStatusIndicator != null) {
throw new IllegalStateException("Cannot set next status line before completing previous!");
}
View line = layoutInflater.inflate(R.layout.status_indicator_line, statusLayoutGroup, false);
latestStatusIndicator = (StatusIndicator) line.findViewById(R.id.status_indicator);
latestStatusIndicator.setDisplayedChild(Status.PROGRESS);
TextView latestStatusText = (TextView) line.findViewById(R.id.status_text);
latestStatusText.setText(statusLine.stringRes);
statusLayoutGroup.addView(line);
}
@Override
public void statusLineOk() {
latestStatusIndicator.setDisplayedChild(Status.OK);
latestStatusIndicator = null;
}
@Override
public void statusLineError() {
latestStatusIndicator.setDisplayedChild(Status.ERROR);
latestStatusIndicator = null;
}
@Override
public void resetStatusLines() {
latestStatusIndicator = null;
statusLayoutGroup.removeAllViews();
}
@Override
public void showActionImport() {
actionAnimator.setDisplayedChildId(R.id.token_layout_import);
}
@Override
public void showActionViewKey() {
actionAnimator.setDisplayedChildId(R.id.token_layout_ok);
}
@Override
public void showActionRetryOrFromFile() {
actionAnimator.setDisplayedChildId(R.id.token_layout_not_found);
}
@Override
public void showActionLocked(int attemptsLeft) {
if (attemptsLeft > 0) {
actionAnimator.setDisplayedChildId(R.id.token_layout_locked);
String unlockAttemptsText = getResources().getQuantityString(
R.plurals.token_unlock_attempts, attemptsLeft, attemptsLeft);
unlockSubtitle.setText(unlockAttemptsText);
} else {
actionAnimator.setDisplayedChildId(R.id.token_layout_locked_hard);
}
}
@Override
public void showActionEmptyToken() {
actionAnimator.setDisplayedChildId(R.id.token_layout_empty);
}
@Override
public void showActionUnsupportedToken() {
actionAnimator.setDisplayedChildId(R.id.token_layout_unsupported);
}
@Override
public void hideAction() {
actionAnimator.setDisplayedChild(0);
}
@Override
public void operationImportKey(byte[] importKeyData) {
if (currentImportKeyringParcel != null) {
throw new IllegalStateException("Cannot trigger import operation twice!");
}
currentImportKeyringParcel =
ImportKeyringParcel.createImportKeyringParcel(ParcelableKeyRing.createFromEncodedBytes(importKeyData));
cryptoImportOperationHelper.setOperationMinimumDelay(1000L);
cryptoImportOperationHelper.cryptoOperation();
}
@Override
public void operationPromote(long masterKeyId, byte[] cardAid, List<byte[]> fingerprints) {
if (currentImportKeyringParcel != null) {
throw new IllegalStateException("Cannot trigger import operation twice!");
}
currentPromoteKeyringParcel = PromoteKeyringParcel.createPromoteKeyringParcel(
masterKeyId, cardAid, fingerprints);
cryptoPromoteOperationHelper.setOperationMinimumDelay(1000L);
cryptoPromoteOperationHelper.cryptoOperation();
}
@Override
public void operationResetSecurityToken() {
Intent intent = new Intent(getActivity(), SecurityTokenOperationActivity.class);
RequiredInputParcel resetP = RequiredInputParcel.createSecurityTokenReset();
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, resetP);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, CryptoInputParcel.createCryptoInputParcel());
startActivityForResult(intent, REQUEST_CODE_RESET);
}
@Override
public void operationChangePinSecurityToken(String adminPin, String newPin) {
Intent intent = new Intent(getActivity(), SecurityTokenChangePinOperationActivity.class);
SecurityTokenChangePinParcel changePinParcel =
SecurityTokenChangePinParcel.createSecurityTokenUnlock(adminPin, newPin);
intent.putExtra(SecurityTokenChangePinOperationActivity.EXTRA_CHANGE_PIN_PARCEL, changePinParcel);
startActivityForResult(intent, REQUEST_CODE_CHANGE_PIN);
}
@Override
public void showFileSelectDialog() {
FileHelper.openDocument(this, null, "*/*", false, REQUEST_CODE_OPEN_FILE);
}
@Override
public void showConfirmResetDialog() {
new Builder(ThemeChanger.getDialogThemeWrapper(getContext()))
.setTitle(R.string.token_reset_confirm_title)
.setMessage(R.string.token_reset_confirm_message)
.setNegativeButton(R.string.button_cancel, null)
.setPositiveButton(R.string.token_reset_confirm_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
presenter.onClickConfirmReset();
}
}).show();
}
@Override
public void showAdminPinDialog() {
AlertDialog adminPinDialog = ChangePinDialogHelper.createAdminPinDialog(getContext(), presenter);
adminPinDialog.show();
}
@Override
public void startCreateKeyForToken(SecurityTokenInfo tokenInfo) {
CreateKeyActivity activity = (CreateKeyActivity) getActivity();
activity.startCreateKeyForSecurityToken(tokenInfo);
}
@Override
public void showErrorCannotUnlock() {
Notify.create(getActivity(), R.string.token_error_locked_indefinitely, Style.ERROR).show();
}
@Override
public void showErrorCannotReset(boolean isGnuk) {
if (isGnuk) {
Notify.create(getActivity(), R.string.token_error_cannot_reset_gnuk_old, Style.ERROR).show();
} else {
Notify.create(getActivity(), R.string.token_error_cannot_reset, Style.ERROR).show();
}
}
@Override
public void showDisplayLogActivity(OperationResult result) {
Intent intent = new Intent(getActivity(), LogDisplayActivity.class);
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result);
startActivity(intent);
}
@TargetApi(VERSION_CODES.JELLY_BEAN)
@Override
public void requestStoragePermission() {
requestPermissions(new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, PERMISSION_READ_STORAGE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode != PERMISSION_READ_STORAGE) {
return;
}
boolean permissionWasGranted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (permissionWasGranted) {
presenter.onStoragePermissionGranted();
} else {
presenter.onStoragePermissionDenied();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_OPEN_FILE: {
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
Uri fileUri = data.getData();
presenter.onFileSelected(fileUri);
}
break;
}
case REQUEST_CODE_RESET: {
SecurityTokenInfo tokenInfo = data == null ? null :
data.<SecurityTokenInfo>getParcelableExtra(SecurityTokenOperationActivity.RESULT_TOKEN_INFO);
if (resultCode == Activity.RESULT_OK) {
presenter.onSecurityTokenResetSuccess(tokenInfo);
} else {
presenter.onSecurityTokenResetCanceled(tokenInfo);
}
break;
}
case REQUEST_CODE_CHANGE_PIN: {
SecurityTokenInfo tokenInfo = data == null ? null :
data.<SecurityTokenInfo>getParcelableExtra(SecurityTokenOperationActivity.RESULT_TOKEN_INFO);
if (resultCode == Activity.RESULT_OK) {
presenter.onSecurityTokenChangePinSuccess(tokenInfo);
} else {
presenter.onSecurityTokenChangePinCanceled(tokenInfo);
}
break;
}
default: {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button_import: {
presenter.onClickImport();
break;
}
case R.id.button_retry: {
presenter.onClickRetry();
break;
}
case R.id.button_view_key: {
presenter.onClickViewKey();
break;
}
case R.id.button_load_file: {
presenter.onClickLoadFile();
break;
}
case R.id.button_reset_token_1:
case R.id.button_reset_token_2:
case R.id.button_reset_token_3:
case R.id.button_reset_token_4:
case R.id.button_reset_token_5: {
presenter.onClickResetToken();
break;
}
case R.id.button_unlock: {
presenter.onClickUnlockToken();
break;
}
case R.id.button_unlock_impossible: {
presenter.onClickUnlockTokenImpossible();
break;
}
case R.id.button_setup: {
presenter.onClickSetupToken();
break;
}
}
}
CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> cryptoImportOperationHelper =
new CryptoOperationHelper<>(0, this, new AbstractCallback<ImportKeyringParcel, ImportKeyResult>() {
@Override
public ImportKeyringParcel createOperationInput() {
return currentImportKeyringParcel;
}
@Override
public void onCryptoOperationSuccess(ImportKeyResult result) {
if (currentImportKeyringParcel == null) {
return;
}
currentImportKeyringParcel = null;
presenter.onImportSuccess(result);
}
@Override
public void onCryptoOperationError(ImportKeyResult result) {
if (currentImportKeyringParcel == null) {
return;
}
currentImportKeyringParcel = null;
presenter.onImportError(result);
}
}, null);
CryptoOperationHelper<PromoteKeyringParcel, PromoteKeyResult> cryptoPromoteOperationHelper =
new CryptoOperationHelper<>(1, this, new AbstractCallback<PromoteKeyringParcel, PromoteKeyResult>() {
@Override
public PromoteKeyringParcel createOperationInput() {
return currentPromoteKeyringParcel;
}
@Override
public void onCryptoOperationSuccess(PromoteKeyResult result) {
if (currentPromoteKeyringParcel == null) {
return;
}
currentPromoteKeyringParcel = null;
presenter.onPromoteSuccess(result);
}
@Override
public void onCryptoOperationError(PromoteKeyResult result) {
if (currentPromoteKeyringParcel == null) {
return;
}
currentPromoteKeyringParcel = null;
presenter.onPromoteError(result);
}
}, null);
enum StatusLine {
CHECK_KEY (R.string.status_check_key),
SEARCH_LOCAL (R.string.status_search_local),
SEARCH_URI (R.string.status_search_uri),
SEARCH_KEYSERVER (R.string.status_search_keyserver),
IMPORT (R.string.status_import),
TOKEN_PROMOTE(R.string.status_token_promote),
TOKEN_CHECK (R.string.status_token_check),
SEARCH_CONTENT_URI (R.string.status_content_uri);
@StringRes
private int stringRes;
StatusLine(@StringRes int stringRes) {
this.stringRes = stringRes;
}
}
}