drop broken secure wifi transfer feature

This feature depended on the unsupported TLS-PSK implementation shipped
with Android's conscrypt implementation. It abused a duck typing
mechanism that allowed using TLS-PSK despite its unsupported status, but
this silently broke somewhere along the way.
This commit is contained in:
Vincent Breitmoser
2021-01-29 12:09:37 +01:00
parent 2cc35ce970
commit 5eaa7518e8
17 changed files with 2 additions and 2324 deletions

View File

@@ -21,19 +21,16 @@ package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
import org.sufficientlysecure.keychain.util.Preferences;
import timber.log.Timber;
@@ -48,7 +45,6 @@ public class CreateKeyStartFragment extends Fragment {
View mImportKey;
View mSecurityToken;
TextView mSkipOrCancel;
View mSecureDeviceSetup;
/**
@@ -72,7 +68,6 @@ public class CreateKeyStartFragment extends Fragment {
mImportKey = view.findViewById(R.id.create_key_import_button);
mSecurityToken = view.findViewById(R.id.create_key_security_token_button);
mSkipOrCancel = view.findViewById(R.id.create_key_cancel);
mSecureDeviceSetup = view.findViewById(R.id.create_key_secure_device_setup);
if (mCreateKeyActivity.mFirstTime) {
mSkipOrCancel.setText(R.string.first_time_skip);
@@ -96,15 +91,6 @@ public class CreateKeyStartFragment extends Fragment {
startActivityForResult(intent, REQUEST_CODE_IMPORT_KEY);
});
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
mSecureDeviceSetup.setOnClickListener(v -> {
TransferFragment frag = new TransferFragment();
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
});
} else {
mSecureDeviceSetup.setVisibility(View.GONE);
}
mSkipOrCancel.setOnClickListener(v -> {
if (!mCreateKeyActivity.mFirstTime) {
mCreateKeyActivity.setResult(Activity.RESULT_CANCELED);

View File

@@ -40,7 +40,6 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
@@ -155,17 +154,6 @@ public class ImportKeysProxyActivity extends FragmentActivity
Timber.d("scanned: " + uri);
// example: pgp+transfer:
if (uri != null && uri.getScheme() != null && uri.getScheme().equalsIgnoreCase(Constants.SKT_SCHEME)) {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_TRANSFER);
intent.putExtra(TransferFragment.EXTRA_OPENPGP_SKT_INFO, uri);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
return;
}
// example: openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282
if (uri == null || uri.getScheme() == null ||
!uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {

View File

@@ -19,9 +19,6 @@ package org.sufficientlysecure.keychain.ui;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.View;
@@ -41,8 +38,6 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferNotAvailableFragment;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -52,7 +47,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
static final int ID_ENCRYPT_DECRYPT = 2;
static final int ID_APPS = 3;
static final int ID_BACKUP = 4;
public static final int ID_TRANSFER = 5;
static final int ID_SETTINGS = 6;
static final int ID_HELP = 7;
@@ -85,11 +79,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
.withIdentifier(ID_APPS).withSelectable(false),
new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore)
.withIdentifier(ID_BACKUP).withSelectable(false),
new PrimaryDrawerItem().withName(R.string.nav_transfer)
.withIcon(R.drawable.ic_wifi_lock_24dp)
.withIconColorRes(R.color.md_grey_600)
.withIconTintingEnabled(true)
.withIdentifier(ID_TRANSFER).withSelectable(false),
new DividerDrawerItem(),
new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withSelectable(false),
new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withSelectable(false)
@@ -113,9 +102,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_BACKUP:
onBackupSelected();
break;
case ID_TRANSFER:
onTransferSelected();
break;
case ID_SETTINGS:
intent = new Intent(MainActivity.this, SettingsActivity.class);
break;
@@ -168,9 +154,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_APPS:
onAppsSelected();
break;
case ID_TRANSFER:
onTransferSelected();
break;
}
}
@@ -190,9 +173,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_APPS:
onAppsSelected();
break;
case ID_TRANSFER:
onTransferSelected();
break;
}
}
}
@@ -234,18 +214,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
setFragment(frag);
}
private void onTransferSelected() {
mToolbar.setTitle(R.string.nav_transfer);
mDrawer.setSelection(ID_TRANSFER, false);
if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
Fragment frag = new TransferNotAvailableFragment();
setFragment(frag);
} else {
Fragment frag = new TransferFragment();
setFragment(frag);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// add the values which need to be saved from the drawer to the bundle

View File

@@ -299,13 +299,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity {
startPassphraseActivity(REQUEST_BACKUP);
return true;
}
case R.id.menu_key_view_skt: {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_TRANSFER);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
}
case R.id.menu_key_view_delete: {
deleteKey();
return true;
@@ -335,7 +328,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity {
}
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
backupKey.setVisible(unifiedKeyInfo.has_any_secret());
menu.findItem(R.id.menu_key_view_skt).setVisible(unifiedKeyInfo.has_any_secret());
MenuItem changePassword = menu.findItem(R.id.menu_key_change_password);
changePassword.setVisible(unifiedKeyInfo.has_any_secret());

View File

@@ -1,477 +0,0 @@
/*
* 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.transfer.presenter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.os.Parcelable;
import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.openintents.openpgp.util.OpenPgpUtils.UserId;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
import org.sufficientlysecure.keychain.network.KeyTransferInteractor;
import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.daos.KeyRepository;
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback;
import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel;
import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.OnClickImportKeyListener;
import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyAdapter;
import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyItem;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.OnClickTransferKeyListener;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.TransferKeyAdapter;
import org.sufficientlysecure.keychain.ui.util.QrCodeUtils;
import timber.log.Timber;
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
public class TransferPresenter implements KeyTransferCallback, OnClickTransferKeyListener, OnClickImportKeyListener {
private static final String DELIMITER_START = "-----BEGIN PGP PRIVATE KEY BLOCK-----";
private static final String DELIMITER_END = "-----END PGP PRIVATE KEY BLOCK-----";
private static final String BACKSTACK_TAG_TRANSFER = "transfer";
private final Context context;
private final TransferMvpView view;
private final KeyRepository keyRepository;
private final TransferKeyAdapter secretKeyAdapter;
private final ReceivedKeyAdapter receivedKeyAdapter;
private final LifecycleOwner lifecycleOwner;
private final GenericViewModel viewModel;
private KeyTransferInteractor keyTransferClientInteractor;
private KeyTransferInteractor keyTransferServerInteractor;
private boolean wasConnected = false;
private boolean sentData = false;
private boolean waitingForWifi = false;
private Long confirmingMasterKeyId;
public TransferPresenter(Context context, LifecycleOwner lifecycleOwner,
GenericViewModel viewModel, TransferMvpView view) {
this.context = context;
this.view = view;
this.lifecycleOwner = lifecycleOwner;
this.viewModel = viewModel;
this.keyRepository = KeyRepository.create(context);
secretKeyAdapter = new TransferKeyAdapter(context, LayoutInflater.from(context), this);
view.setSecretKeyAdapter(secretKeyAdapter);
receivedKeyAdapter = new ReceivedKeyAdapter(context, LayoutInflater.from(context), this);
view.setReceivedKeyAdapter(receivedKeyAdapter);
}
public void onUiInitFromIntentUri(final Uri initUri) {
connectionStartConnect(initUri.toString());
}
public void onUiStart() {
LiveData<List<UnifiedKeyInfo>> liveData =
viewModel.getGenericLiveData(context, keyRepository::getAllUnifiedKeyInfoWithSecret);
liveData.observe(lifecycleOwner, this::onLoadSecretUnifiedKeyInfo);
if (keyTransferServerInteractor == null && keyTransferClientInteractor == null && !wasConnected) {
checkWifiResetAndStartListen();
}
}
private void onLoadSecretUnifiedKeyInfo(List<UnifiedKeyInfo> data) {
secretKeyAdapter.setData(data);
view.setShowSecretKeyEmptyView(data.isEmpty());
}
public void onUiStop() {
connectionClear();
if (wasConnected) {
view.showViewDisconnected();
view.dismissConfirmationIfExists();
secretKeyAdapter.setAllDisabled(true);
}
}
public void onUiClickScan() {
connectionClear();
view.scanQrCode();
}
public void onUiClickScanAgain() {
onUiClickScan();
}
public void onUiClickDone() {
view.finishFragmentOrActivity();
}
public void onUiQrCodeScanned(String qrCodeContent) {
connectionStartConnect(qrCodeContent);
}
public void onUiBackStackPop() {
if (wasConnected) {
checkWifiResetAndStartListen();
}
}
@Override
public void onUiClickTransferKey(long masterKeyId) {
if (sentData) {
prepareAndSendKey(masterKeyId);
} else {
confirmingMasterKeyId = masterKeyId;
view.showConfirmSendDialog();
}
}
public void onUiClickConfirmSend() {
if (confirmingMasterKeyId == null) {
return;
}
long masterKeyId = confirmingMasterKeyId;
confirmingMasterKeyId = null;
prepareAndSendKey(masterKeyId);
}
@Override
public void onUiClickImportKey(final long masterKeyId, String keyData) {
receivedKeyAdapter.focusItem(masterKeyId);
final ImportKeyringParcel importKeyringParcel = ImportKeyringParcel.createImportKeyringParcel(
ParcelableKeyRing.createFromEncodedBytes(keyData.getBytes()));
CryptoOperationHelper<ImportKeyringParcel,ImportKeyResult> op =
view.createCryptoOperationHelper(new Callback<ImportKeyringParcel,ImportKeyResult>() {
@Override
public ImportKeyringParcel createOperationInput() {
return importKeyringParcel;
}
@Override
public void onCryptoOperationSuccess(ImportKeyResult result) {
receivedKeyAdapter.focusItem(null);
receivedKeyAdapter.addToFinishedItems(masterKeyId);
view.releaseCryptoOperationHelper();
view.showResultNotification(result);
}
@Override
public void onCryptoOperationCancelled() {
view.releaseCryptoOperationHelper();
receivedKeyAdapter.focusItem(null);
}
@Override
public void onCryptoOperationError(ImportKeyResult result) {
receivedKeyAdapter.focusItem(null);
view.releaseCryptoOperationHelper();
view.showResultNotification(result);
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
});
op.cryptoOperation();
}
public void onWifiConnected() {
if (waitingForWifi) {
resetAndStartListen();
}
}
@Override
public void onServerStarted(String qrCodeData) {
Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse(qrCodeData));
view.setQrImage(qrCodeBitmap);
}
@Override
public void onConnectionEstablished(String otherName) {
wasConnected = true;
secretKeyAdapter.clearFinishedItems();
secretKeyAdapter.focusItem(null);
secretKeyAdapter.setAllDisabled(false);
receivedKeyAdapter.clear();
view.showConnectionEstablished(otherName);
view.setShowDoneIcon(true);
view.addFakeBackStackItem(BACKSTACK_TAG_TRANSFER);
}
@Override
public void onConnectionLost() {
if (!wasConnected) {
checkWifiResetAndStartListen();
view.showErrorConnectionFailed();
} else {
connectionClear();
view.dismissConfirmationIfExists();
view.showViewDisconnected();
secretKeyAdapter.setAllDisabled(true);
}
}
@Override
public void onDataReceivedOk(String receivedData) {
if (sentData) {
Timber.d("received data, but we already sent a key! race condition, or other side misbehaving?");
return;
}
Timber.d("received data");
UncachedKeyRing uncachedKeyRing;
try {
uncachedKeyRing = UncachedKeyRing.decodeFromData(receivedData.getBytes());
} catch (PgpGeneralException | IOException | RuntimeException e) {
Timber.e(e, "error parsing incoming key");
view.showErrorBadKey();
return;
}
String primaryUserId = uncachedKeyRing.getPublicKey().getPrimaryUserIdWithFallback();
UserId userId = OpenPgpUtils.splitUserId(primaryUserId);
ReceivedKeyItem receivedKeyItem = new ReceivedKeyItem(receivedData, uncachedKeyRing.getMasterKeyId(),
uncachedKeyRing.getCreationTime(), userId.name, userId.email);
receivedKeyAdapter.addItem(receivedKeyItem);
view.showReceivingKeys();
}
@Override
public void onDataSentOk(String passthrough) {
Timber.d("data sent ok!");
final long masterKeyId = Long.parseLong(passthrough);
new Handler().postDelayed(() -> {
secretKeyAdapter.focusItem(null);
secretKeyAdapter.addToFinishedItems(masterKeyId);
}, 750);
}
@Override
public void onConnectionErrorConnect() {
view.showWaitingForConnection();
view.showErrorConnectionFailed();
resetAndStartListen();
}
@Override
public void onConnectionErrorNoRouteToHost(String wifiSsid) {
connectionClear();
String ownWifiSsid = getConnectedWifiSsid();
if (!wifiSsid.equalsIgnoreCase(ownWifiSsid)) {
view.showWifiError(wifiSsid);
} else {
view.showWaitingForConnection();
view.showErrorConnectionFailed();
resetAndStartListen();
}
}
@Override
public void onConnectionErrorListen() {
view.showErrorListenFailed();
}
@Override
public void onConnectionError(String errorMessage) {
view.showErrorConnectionError(errorMessage);
connectionClear();
if (wasConnected) {
view.showViewDisconnected();
secretKeyAdapter.setAllDisabled(true);
}
}
private void connectionStartConnect(String qrCodeContent) {
connectionClear();
view.showEstablishingConnection();
keyTransferClientInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
try {
keyTransferClientInteractor.connectToServer(qrCodeContent, TransferPresenter.this);
} catch (URISyntaxException e) {
view.showErrorConnectionFailed();
}
}
private void checkWifiResetAndStartListen() {
if (!isWifiConnected()) {
waitingForWifi = true;
view.showNotOnWifi();
return;
}
resetAndStartListen();
}
private void resetAndStartListen() {
waitingForWifi = false;
wasConnected = false;
sentData = false;
connectionClear();
String wifiSsid = getConnectedWifiSsid();
keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
keyTransferServerInteractor.startServer(this, wifiSsid);
view.showWaitingForConnection();
view.setShowDoneIcon(false);
}
private boolean isWifiConnected() {
ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connManager == null) {
return false;
}
NetworkInfo wifiNetwork = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
return wifiNetwork.isConnected();
}
private String getConnectedWifiSsid() {
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
if (wifiManager == null) {
return null;
}
WifiInfo info = wifiManager.getConnectionInfo();
if (info == null) {
return null;
}
// getSSID will return the ssid in quotes if it is valid utf-8. we only return it in that case.
String ssid = info.getSSID();
if (ssid.charAt(0) != '"') {
return null;
}
return ssid.substring(1, ssid.length() -1);
}
private void connectionClear() {
if (keyTransferServerInteractor != null) {
keyTransferServerInteractor.closeConnection();
keyTransferServerInteractor = null;
}
if (keyTransferClientInteractor != null) {
keyTransferClientInteractor.closeConnection();
keyTransferClientInteractor = null;
}
}
private void prepareAndSendKey(long masterKeyId) {
try {
byte[] armoredSecretKey = keyRepository.getSecretKeyRingAsArmoredData(masterKeyId);
secretKeyAdapter.focusItem(masterKeyId);
connectionSend(armoredSecretKey, Long.toString(masterKeyId));
} catch (IOException | NotFoundException e) {
// TODO
e.printStackTrace();
}
}
private void connectionSend(byte[] armoredSecretKey, String passthrough) {
sentData = true;
if (keyTransferClientInteractor != null) {
keyTransferClientInteractor.sendData(armoredSecretKey, passthrough);
} else if (keyTransferServerInteractor != null) {
keyTransferServerInteractor.sendData(armoredSecretKey, passthrough);
}
}
public interface TransferMvpView {
void showNotOnWifi();
void showWaitingForConnection();
void showEstablishingConnection();
void showConnectionEstablished(String hostname);
void showWifiError(String wifiSsid);
void showReceivingKeys();
void showViewDisconnected();
void scanQrCode();
void setQrImage(Bitmap qrCode);
void releaseCryptoOperationHelper();
void showErrorBadKey();
void showErrorConnectionFailed();
void showErrorListenFailed();
void showErrorConnectionError(String errorMessage);
void showResultNotification(ImportKeyResult result);
void setShowDoneIcon(boolean showDoneIcon);
void setSecretKeyAdapter(Adapter adapter);
void setShowSecretKeyEmptyView(boolean isEmpty);
void setReceivedKeyAdapter(Adapter secretKeyAdapter);
<T extends Parcelable, S extends OperationResult> CryptoOperationHelper<T,S> createCryptoOperationHelper(Callback<T, S> callback);
void addFakeBackStackItem(String tag);
void finishFragmentOrActivity();
void showConfirmSendDialog();
void dismissConfirmationIfExists();
}
}

View File

@@ -1,215 +0,0 @@
/*
* 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.transfer.view;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
public class ReceivedSecretKeyList extends RecyclerView {
private static final int STATE_INVISIBLE = 0;
// private static final int STATE_BUTTON = 1; // used in TransferSecretKeyList
private static final int STATE_PROGRESS = 2;
private static final int STATE_TRANSFERRED = 3;
private static final int STATE_IMPORT_BUTTON = 4;
public ReceivedSecretKeyList(Context context) {
super(context);
init(context);
}
public ReceivedSecretKeyList(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ReceivedSecretKeyList(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
setLayoutManager(new LinearLayoutManager(context));
addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, true));
}
public static class ReceivedKeyAdapter extends Adapter<ReceivedKeyViewHolder> {
private final Context context;
private final LayoutInflater layoutInflater;
private final OnClickImportKeyListener onClickImportKeyListener;
private Long focusedMasterKeyId;
private List<ReceivedKeyItem> data = new ArrayList<>();
private ArrayList<Long> finishedItems = new ArrayList<>();
public ReceivedKeyAdapter(Context context, LayoutInflater layoutInflater,
OnClickImportKeyListener onClickImportKeyListener) {
this.context = context;
this.layoutInflater = layoutInflater;
this.onClickImportKeyListener = onClickImportKeyListener;
}
@Override
public ReceivedKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ReceivedKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false));
}
@Override
public void onBindViewHolder(ReceivedKeyViewHolder holder, int position) {
ReceivedKeyItem item = data.get(position);
boolean isFinished = finishedItems.contains(item.masterKeyId);
holder.bind(context, item, onClickImportKeyListener, focusedMasterKeyId, isFinished);
}
@Override
public int getItemCount() {
return data != null ? data.size() : 0;
}
@Override
public long getItemId(int position) {
return data.get(position).masterKeyId;
}
public void addToFinishedItems(long masterKeyId) {
finishedItems.add(masterKeyId);
// doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things!
}
public void focusItem(Long masterKeyId) {
focusedMasterKeyId = masterKeyId;
notifyItemRangeChanged(0, getItemCount());
}
public void addItem(ReceivedKeyItem receivedKeyItem) {
data.add(receivedKeyItem);
notifyItemInserted(data.size() -1);
}
public void clear() {
data.clear();
finishedItems.clear();
focusedMasterKeyId = null;
notifyDataSetChanged();
}
}
static class ReceivedKeyViewHolder extends ViewHolder {
private final TextView vName;
private final TextView vEmail;
private final TextView vCreation;
private final View vImportButton;
private final ViewAnimator vState;
ReceivedKeyViewHolder(View itemView) {
super(itemView);
vName = itemView.findViewById(R.id.key_list_item_name);
vEmail = itemView.findViewById(R.id.key_list_item_email);
vCreation = itemView.findViewById(R.id.key_list_item_creation);
vImportButton = itemView.findViewById(R.id.button_import);
vState = itemView.findViewById(R.id.transfer_state);
}
private void bind(Context context, final ReceivedKeyItem item,
final OnClickImportKeyListener onClickReceiveKeyListener, Long focusedMasterKeyId,
boolean isFinished) {
if (item.name != null) {
vName.setText(item.name);
vName.setVisibility(View.VISIBLE);
} else {
vName.setVisibility(View.GONE);
}
if (item.email != null) {
vEmail.setText(item.email);
vEmail.setVisibility(View.VISIBLE);
} else {
vEmail.setVisibility(View.GONE);
}
String dateTime = DateUtils.formatDateTime(context, item.creationMillis,
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME |
DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH);
vCreation.setText(context.getString(R.string.label_key_created, dateTime));
if (focusedMasterKeyId != null) {
if (focusedMasterKeyId != item.masterKeyId) {
itemView.animate().alpha(0.2f).start();
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE);
} else {
itemView.setAlpha(1.0f);
vState.setDisplayedChild(STATE_PROGRESS);
}
} else {
itemView.animate().alpha(1.0f).start();
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_IMPORT_BUTTON);
}
if (focusedMasterKeyId == null && onClickReceiveKeyListener != null) {
vImportButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onClickReceiveKeyListener.onUiClickImportKey(item.masterKeyId, item.keyData);
}
});
} else {
vImportButton.setOnClickListener(null);
}
}
}
public interface OnClickImportKeyListener {
void onUiClickImportKey(long masterKeyId, String keyData);
}
public static class ReceivedKeyItem {
private final String keyData;
private final long masterKeyId;
private final long creationMillis;
private final String name;
private final String email;
public ReceivedKeyItem(String keyData, long masterKeyId, long creationMillis, String name, String email) {
this.keyData = keyData;
this.masterKeyId = masterKeyId;
this.creationMillis = creationMillis;
this.name = name;
this.email = email;
}
}
}

View File

@@ -1,445 +0,0 @@
/*
* 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.transfer.view;
import android.app.Activity;
import androidx.lifecycle.ViewModelProviders;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import android.text.TextUtils;
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.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.zxing.client.android.Intents;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.ui.MainActivity;
import org.sufficientlysecure.keychain.ui.QrCodeCaptureActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback;
import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel;
import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter;
import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.widget.ConnectionStatusView;
import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
public class TransferFragment extends Fragment implements TransferMvpView {
public static final int REQUEST_CODE_SCAN = 1;
public static final int LOADER_ID = 1;
public static final String EXTRA_OPENPGP_SKT_INFO = "openpgp_skt_info";
private ImageView vQrCodeImage;
private TransferPresenter presenter;
private ToolableViewAnimator vTransferAnimator;
private TextView vConnectionStatusText1;
private TextView vConnectionStatusText2;
private ConnectionStatusView vConnectionStatusView1;
private ConnectionStatusView vConnectionStatusView2;
private RecyclerView vTransferKeyList;
private View vTransferKeyListEmptyView;
private RecyclerView vReceivedKeyList;
private CryptoOperationHelper currentCryptoOperationHelper;
private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) {
NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
if (networkInfo != null && networkInfo.isConnected()) {
presenter.onWifiConnected();
}
}
}
};
private boolean showDoneIcon;
private AlertDialog confirmationDialog;
private TextView vWifiErrorInstructions;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.transfer_fragment, container, false);
vTransferAnimator = view.findViewById(R.id.transfer_animator);
vConnectionStatusText1 = view.findViewById(R.id.connection_status_1);
vConnectionStatusText2 = view.findViewById(R.id.connection_status_2);
vConnectionStatusView1 = view.findViewById(R.id.connection_status_icon_1);
vConnectionStatusView2 = view.findViewById(R.id.connection_status_icon_2);
vTransferKeyList = view.findViewById(R.id.transfer_key_list);
vTransferKeyListEmptyView = view.findViewById(R.id.transfer_key_list_empty);
vReceivedKeyList = view.findViewById(R.id.received_key_list);
vWifiErrorInstructions = view.findViewById(R.id.transfer_wifi_error_instructions);
vQrCodeImage = view.findViewById(R.id.qr_code_image);
view.findViewById(R.id.button_scan).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (presenter != null) {
presenter.onUiClickScan();
}
}
});
view.findViewById(R.id.button_scan_again).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (presenter != null) {
presenter.onUiClickScanAgain();
}
}
});
GenericViewModel genericViewModel = ViewModelProviders.of(this).get(GenericViewModel.class);
presenter = new TransferPresenter(getContext(), this, genericViewModel, this);
setHasOptionsMenu(true);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
return;
}
Intent activityIntent = getActivity().getIntent();
if (activityIntent != null && activityIntent.hasExtra(EXTRA_OPENPGP_SKT_INFO)) {
presenter.onUiInitFromIntentUri(activityIntent.getParcelableExtra(EXTRA_OPENPGP_SKT_INFO));
}
}
@Override
public void onStart() {
super.onStart();
presenter.onUiStart();
}
@Override
public void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
getContext().registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onPause() {
super.onPause();
getContext().unregisterReceiver(broadcastReceiver);
}
@Override
public void onStop() {
super.onStop();
presenter.onUiStop();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (showDoneIcon) {
inflater.inflate(R.menu.transfer_menu, menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_done) {
presenter.onUiClickDone();
return true;
}
return false;
}
@Override
public void showNotOnWifi() {
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_no_wifi);
}
@Override
public void showWaitingForConnection() {
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_waiting);
}
@Override
public void showEstablishingConnection() {
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connecting);
}
@Override
public void showConnectionEstablished(String hostname) {
// String statusText = getString(R.string.transfer_status_connected, hostname);
vConnectionStatusText1.setText(R.string.transfer_status_connected);
vConnectionStatusText2.setText(R.string.transfer_status_connected);
vConnectionStatusView1.setConnected(true);
vConnectionStatusView2.setConnected(true);
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connected);
}
@Override
public void showWifiError(String wifiSsid) {
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_wifi_error);
if (!TextUtils.isEmpty(wifiSsid)) {
vWifiErrorInstructions
.setText(getResources().getString(R.string.transfer_error_wifi_text_instructions_ssid, wifiSsid));
} else {
vWifiErrorInstructions.setText(R.string.transfer_error_wifi_text_instructions);
}
}
@Override
public void showReceivingKeys() {
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_passive);
}
@Override
public void showViewDisconnected() {
vConnectionStatusText1.setText(R.string.transfer_status_disconnected);
vConnectionStatusText2.setText(R.string.transfer_status_disconnected);
vConnectionStatusView1.setConnected(false);
vConnectionStatusView2.setConnected(false);
}
@Override
public void setQrImage(final Bitmap qrCode) {
vQrCodeImage.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int viewSize = vQrCodeImage.getWidth();
if (viewSize == 0) {
return;
}
// create actual bitmap in display dimensions
Bitmap scaled = Bitmap.createScaledBitmap(qrCode, viewSize, viewSize, false);
vQrCodeImage.setImageBitmap(scaled);
}
});
vQrCodeImage.requestLayout();
}
@Override
public void scanQrCode() {
Intent intent = new Intent(getActivity(), QrCodeCaptureActivity.class);
startActivityForResult(intent, REQUEST_CODE_SCAN);
}
@Override
public void setShowDoneIcon(boolean showDoneIcon) {
this.showDoneIcon = showDoneIcon;
FragmentActivity activity = getActivity();
if (activity != null) {
activity.invalidateOptionsMenu();
}
}
@Override
public void setSecretKeyAdapter(Adapter adapter) {
vTransferKeyList.setAdapter(adapter);
}
@Override
public void setShowSecretKeyEmptyView(boolean isEmpty) {
vTransferKeyListEmptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
}
@Override
public void setReceivedKeyAdapter(Adapter adapter) {
vReceivedKeyList.setAdapter(adapter);
}
@Override
public <T extends Parcelable, S extends OperationResult> CryptoOperationHelper<T,S> createCryptoOperationHelper(Callback<T, S> callback) {
CryptoOperationHelper<T,S> cryptoOperationHelper = new CryptoOperationHelper<>(1, this, callback, null);
currentCryptoOperationHelper = cryptoOperationHelper;
return cryptoOperationHelper;
}
@Override
public void releaseCryptoOperationHelper() {
currentCryptoOperationHelper = null;
}
@Override
public void showErrorBadKey() {
Notify.create(getActivity(), R.string.transfer_error_read_incoming, Style.ERROR).show();
}
@Override
public void showErrorConnectionFailed() {
Notify.create(getActivity(), R.string.transfer_error_connect, Style.ERROR).show();
}
@Override
public void showErrorListenFailed() {
Notify.create(getActivity(), R.string.transfer_error_listen, Style.ERROR).show();
}
@Override
public void showErrorConnectionError(String errorMessage) {
if (errorMessage != null) {
String text = getString(R.string.transfer_error_generic_msg, errorMessage);
Notify.create(getActivity(), text, Style.ERROR).show();
} else {
Notify.create(getActivity(), R.string.transfer_error_generic, Style.ERROR).show();
}
}
@Override
public void showResultNotification(ImportKeyResult result) {
result.createNotify(getActivity()).show();
}
@Override
public void addFakeBackStackItem(final String tag) {
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.addToBackStack(tag)
.commitAllowingStateLoss();
fragmentManager.executePendingTransactions();
fragmentManager.addOnBackStackChangedListener(new OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
FragmentManager fragMan = getFragmentManager();
if (fragMan == null) {
return;
}
fragMan.popBackStack(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fragMan.removeOnBackStackChangedListener(this);
presenter.onUiBackStackPop();
}
});
}
@Override
public void finishFragmentOrActivity() {
FragmentActivity activity = getActivity();
if (activity != null) {
if (activity instanceof MainActivity) {
((MainActivity) activity).onKeysSelected();
} else {
activity.finish();
}
}
}
@Override
public void showConfirmSendDialog() {
if (confirmationDialog != null) {
return;
}
confirmationDialog = new Builder(getContext())
.setTitle(R.string.transfer_confirm_title)
.setMessage(R.string.transfer_confirm_text)
.setPositiveButton(R.string.transfer_confirm_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (whichButton == DialogInterface.BUTTON_POSITIVE) {
presenter.onUiClickConfirmSend();
} else {
dialog.dismiss();
}
}
})
.setNegativeButton(R.string.transfer_confirm_cancel, null)
.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
confirmationDialog = null;
}
})
.create();
confirmationDialog.show();
}
@Override
public void dismissConfirmationIfExists() {
if (confirmationDialog != null && confirmationDialog.isShowing()) {
confirmationDialog.dismiss();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (currentCryptoOperationHelper != null &&
currentCryptoOperationHelper.handleActivityResult(requestCode, resultCode, data)) {
return;
}
switch (requestCode) {
case REQUEST_CODE_SCAN:
if (resultCode == Activity.RESULT_OK) {
String qrCodeData = data.getStringExtra(Intents.Scan.RESULT);
presenter.onUiQrCodeScanned(qrCodeData);
}
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.transfer.view;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.sufficientlysecure.keychain.R;
public class TransferNotAvailableFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.transfer_not_available_fragment, container, false);
}
}

View File

@@ -1,209 +0,0 @@
/*
* 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.transfer.view;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
public class TransferSecretKeyList extends RecyclerView {
private static final int STATE_INVISIBLE = 0;
private static final int STATE_BUTTON = 1;
private static final int STATE_PROGRESS = 2;
private static final int STATE_TRANSFERRED = 3;
// private static final int STATE_IMPORT_BUTTON = 4; // used in ReceivedSecretKeyList
public TransferSecretKeyList(Context context) {
super(context);
init(context);
}
public TransferSecretKeyList(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TransferSecretKeyList(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
setLayoutManager(new LinearLayoutManager(context));
addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, true));
setItemAnimator(null);
}
public static class TransferKeyAdapter extends RecyclerView.Adapter<TransferKeyViewHolder> {
private final Context context;
private final LayoutInflater layoutInflater;
private final OnClickTransferKeyListener onClickTransferKeyListener;
private Long focusedMasterKeyId;
private List<UnifiedKeyInfo> data;
private ArrayList<Long> finishedItems = new ArrayList<>();
private boolean allItemsDisabled;
public TransferKeyAdapter(Context context, LayoutInflater layoutInflater,
OnClickTransferKeyListener onClickTransferKeyListener) {
this.context = context;
this.layoutInflater = layoutInflater;
this.onClickTransferKeyListener = onClickTransferKeyListener;
}
@NonNull
@Override
public TransferKeyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new TransferKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false));
}
@Override
public void onBindViewHolder(@NonNull TransferKeyViewHolder holder, int position) {
UnifiedKeyInfo item = data.get(position);
boolean isFinished = finishedItems.contains(item.master_key_id());
holder.bind(context, item, onClickTransferKeyListener, focusedMasterKeyId, isFinished, allItemsDisabled);
}
@Override
public int getItemCount() {
return data != null ? data.size() : 0;
}
@Override
public long getItemId(int position) {
return data.get(position).master_key_id();
}
public void setData(List<UnifiedKeyInfo> data) {
this.data = data;
notifyDataSetChanged();
}
public void clearFinishedItems() {
finishedItems.clear();
notifyItemRangeChanged(0, getItemCount());
}
public void addToFinishedItems(long masterKeyId) {
finishedItems.add(masterKeyId);
// doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things!
}
public void focusItem(Long masterKeyId) {
focusedMasterKeyId = masterKeyId;
notifyItemRangeChanged(0, getItemCount());
}
public void setAllDisabled(boolean allItemsdisablde) {
allItemsDisabled = allItemsdisablde;
notifyItemRangeChanged(0, getItemCount());
}
}
static class TransferKeyViewHolder extends RecyclerView.ViewHolder {
private final TextView vName;
private final TextView vEmail;
private final TextView vCreation;
private final View vSendButton;
private final ViewAnimator vState;
TransferKeyViewHolder(View itemView) {
super(itemView);
vName = itemView.findViewById(R.id.key_list_item_name);
vEmail = itemView.findViewById(R.id.key_list_item_email);
vCreation = itemView.findViewById(R.id.key_list_item_creation);
vSendButton = itemView.findViewById(R.id.button_transfer);
vState = itemView.findViewById(R.id.transfer_state);
}
private void bind(Context context, UnifiedKeyInfo item,
final OnClickTransferKeyListener onClickTransferKeyListener, Long focusedMasterKeyId,
boolean isFinished, boolean disableAll) {
if (item.name() != null) {
vName.setText(item.name());
vName.setVisibility(View.VISIBLE);
} else {
vName.setVisibility(View.GONE);
}
if (item.email() != null) {
vEmail.setText(item.email());
vEmail.setVisibility(View.VISIBLE);
} else {
vEmail.setVisibility(View.GONE);
}
String dateTime = DateUtils.formatDateTime(context, item.creation() * 1000,
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME |
DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH);
vCreation.setText(context.getString(R.string.label_key_created, dateTime));
if (disableAll) {
itemView.setAlpha(0.2f);
vState.setDisplayedChild(STATE_INVISIBLE);
vSendButton.setOnClickListener(null);
return;
}
if (focusedMasterKeyId != null) {
if (focusedMasterKeyId != item.master_key_id()) {
itemView.animate().alpha(0.2f).start();
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE);
} else {
itemView.setAlpha(1.0f);
vState.setDisplayedChild(STATE_PROGRESS);
}
} else {
itemView.animate().alpha(1.0f).start();
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_BUTTON);
}
if (focusedMasterKeyId == null && onClickTransferKeyListener != null) {
vSendButton.setOnClickListener(
v -> onClickTransferKeyListener.onUiClickTransferKey(item.master_key_id()));
} else {
vSendButton.setOnClickListener(null);
}
}
}
public interface OnClickTransferKeyListener {
void onUiClickTransferKey(long masterKeyId);
}
}