tls-psk: working version, with fake progress too!

This commit is contained in:
Vincent Breitmoser
2017-05-30 14:39:31 +02:00
parent 747feaa100
commit 0c3a247d9e
5 changed files with 293 additions and 134 deletions

View File

@@ -22,6 +22,7 @@ import java.io.BufferedOutputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.Socket; import java.net.Socket;
@@ -53,9 +54,11 @@ import org.sufficientlysecure.keychain.util.Log;
@RequiresApi(api = VERSION_CODES.LOLLIPOP) @RequiresApi(api = VERSION_CODES.LOLLIPOP)
public class KeyTransferInteractor { public class KeyTransferInteractor {
private static final int SHOW_CONNECTION_DETAILS = 1; private static final int CONNECTION_LISTENING = 1;
private static final int CONNECTION_ESTABLISHED = 2; private static final int CONNECTION_ESTABLISHED = 2;
private static final int CONNECTION_LOST = 3; private static final int CONNECTION_SEND_OK = 3;
private static final int CONNECTION_RECEIVE_OK = 4;
private static final int CONNECTION_LOST = 5;
private TransferThread transferThread; private TransferThread transferThread;
@@ -121,7 +124,7 @@ public class KeyTransferInteractor {
String presharedKeyEncoded = Base64.encodeToString(presharedKey, Base64.URL_SAFE | Base64.NO_PADDING); String presharedKeyEncoded = Base64.encodeToString(presharedKey, Base64.URL_SAFE | Base64.NO_PADDING);
String qrCodeData = presharedKeyEncoded + "@" + getIPAddress(true) + ":" + port; String qrCodeData = presharedKeyEncoded + "@" + getIPAddress(true) + ":" + port;
invokeListener(SHOW_CONNECTION_DETAILS, qrCodeData); invokeListener(CONNECTION_LISTENING, qrCodeData);
socket = serverSocket.accept(); socket = serverSocket.accept();
invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString());
@@ -131,6 +134,7 @@ public class KeyTransferInteractor {
} }
handleOpenConnection(socket); handleOpenConnection(socket);
Log.d(Constants.TAG, "connection closed ok!");
} catch (IOException e) { } catch (IOException e) {
Log.e(Constants.TAG, "error!", e); Log.e(Constants.TAG, "error!", e);
} finally { } finally {
@@ -164,33 +168,70 @@ public class KeyTransferInteractor {
} }
private void handleOpenConnection(Socket socket) throws IOException { private void handleOpenConnection(Socket socket) throws IOException {
socket.setSoTimeout(500);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
socket.setSoTimeout(500);
while (!isInterrupted() && socket.isConnected()) { while (!isInterrupted() && socket.isConnected()) {
if (dataToSend != null) { if (sendDataIfAvailable(socket)) {
BufferedOutputStream bufferedOutputStream = return;
new BufferedOutputStream(socket.getOutputStream());
bufferedOutputStream.write(dataToSend);
bufferedOutputStream.close();
dataToSend = null;
break;
} }
try {
String line = bufferedReader.readLine(); if (receiveDataIfAvailable(socket, bufferedReader)) {
if (line == null) { return;
Log.d(Constants.TAG, "eof");
break;
}
Log.d(Constants.TAG, "got line: " + line);
} catch (SocketTimeoutException e) {
// ignore
} }
} }
Log.d(Constants.TAG, "disconnected"); Log.d(Constants.TAG, "disconnected");
invokeListener(CONNECTION_LOST, null); invokeListener(CONNECTION_LOST, null);
} }
private boolean receiveDataIfAvailable(Socket socket, BufferedReader bufferedReader) throws IOException {
String firstLine;
try {
firstLine = bufferedReader.readLine();
} catch (SocketTimeoutException e) {
return false;
}
if (firstLine == null) {
invokeListener(CONNECTION_LOST, null);
return true;
}
socket.setSoTimeout(2000);
String receivedData = receiveAfterFirstLineAndClose(bufferedReader, firstLine);
invokeListener(CONNECTION_RECEIVE_OK, receivedData);
return true;
}
private boolean sendDataIfAvailable(Socket socket) throws IOException {
if (dataToSend != null) {
byte[] data = dataToSend;
dataToSend = null;
socket.setSoTimeout(2000);
sendDataAndClose(socket.getOutputStream(), data);
invokeListener(CONNECTION_SEND_OK, null);
return true;
}
return false;
}
private String receiveAfterFirstLineAndClose(BufferedReader bufferedReader, String line) throws IOException {
StringBuilder builder = new StringBuilder();
do {
builder.append(line);
line = bufferedReader.readLine();
} while (line != null);
return builder.toString();
}
private void sendDataAndClose(OutputStream outputStream, byte[] data) throws IOException {
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
bufferedOutputStream.write(data);
bufferedOutputStream.close();
}
private void invokeListener(final int method, final String arg) { private void invokeListener(final int method, final String arg) {
if (handler == null) { if (handler == null) {
return; return;
@@ -203,12 +244,18 @@ public class KeyTransferInteractor {
return; return;
} }
switch (method) { switch (method) {
case SHOW_CONNECTION_DETAILS: case CONNECTION_LISTENING:
callback.onServerStarted(arg); callback.onServerStarted(arg);
break; break;
case CONNECTION_ESTABLISHED: case CONNECTION_ESTABLISHED:
callback.onConnectionEstablished(arg); callback.onConnectionEstablished(arg);
break; break;
case CONNECTION_RECEIVE_OK:
callback.onDataReceivedOk(arg);
break;
case CONNECTION_SEND_OK:
callback.onDataSentOk(arg);
break;
case CONNECTION_LOST: case CONNECTION_LOST:
callback.onConnectionLost(); callback.onConnectionLost();
} }
@@ -218,7 +265,7 @@ public class KeyTransferInteractor {
handler.post(runnable); handler.post(runnable);
} }
public synchronized void sendDataAndClose(byte[] dataToSend) { synchronized void sendDataAndClose(byte[] dataToSend) {
this.dataToSend = dataToSend; this.dataToSend = dataToSend;
} }
@@ -258,6 +305,9 @@ public class KeyTransferInteractor {
void onServerStarted(String qrCodeData); void onServerStarted(String qrCodeData);
void onConnectionEstablished(String otherName); void onConnectionEstablished(String otherName);
void onConnectionLost(); void onConnectionLost();
void onDataReceivedOk(String receivedData);
void onDataSentOk(String arg);
} }
/** /**
@@ -293,7 +343,8 @@ public class KeyTransferInteractor {
} }
} }
} catch (Exception ex) { } catch (Exception ex) {
} // for now eat exceptions // ignore
}
return ""; return "";
} }

View File

@@ -33,6 +33,7 @@ import android.support.v4.content.Loader;
import android.support.v7.widget.RecyclerView.Adapter; import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.network.KeyTransferInteractor; import org.sufficientlysecure.keychain.network.KeyTransferInteractor;
import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback; import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
@@ -42,6 +43,7 @@ import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.Secret
import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.OnClickTransferKeyListener; import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.OnClickTransferKeyListener;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.TransferKeyAdapter; import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.TransferKeyAdapter;
import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils;
import org.sufficientlysecure.keychain.util.Log;
@RequiresApi(api = VERSION_CODES.LOLLIPOP) @RequiresApi(api = VERSION_CODES.LOLLIPOP)
@@ -66,43 +68,45 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
view.setSecretKeyAdapter(secretKeyAdapter); view.setSecretKeyAdapter(secretKeyAdapter);
} }
public void onStart() {
public void onUiStart() {
loaderManager.restartLoader(loaderId, null, this); loaderManager.restartLoader(loaderId, null, this);
startServer(); if (keyTransferServerInteractor == null && keyTransferClientInteractor == null) {
connectionStartListen();
}
} }
public void onStop() { public void onUiStop() {
clearConnections(); connectionClear();
} }
public void onClickScan() { public void onUiClickScan() {
clearConnections(); connectionClear();
view.scanQrCode(); view.scanQrCode();
} }
public void onQrCodeScanned(String qrCodeContent) { public void onUiQrCodeScanned(String qrCodeContent) {
keyTransferClientInteractor = new KeyTransferInteractor(); connectionStartConnect(qrCodeContent);
keyTransferClientInteractor.connectToServer(qrCodeContent, this);
} }
private void clearConnections() { @Override
if (keyTransferServerInteractor != null) { public void onUiClickTransferKey(long masterKeyId) {
keyTransferServerInteractor.closeConnection(); try {
keyTransferServerInteractor = null; byte[] armoredSecretKey =
} KeyRepository.createDatabaseInteractor(context).getSecretKeyRingAsArmoredData(masterKeyId);
if (keyTransferClientInteractor != null) { connectionSend(armoredSecretKey);
keyTransferClientInteractor.closeConnection(); } catch (IOException | NotFoundException | PgpGeneralException e) {
keyTransferClientInteractor = null; e.printStackTrace();
} }
} }
public void startServer() { public void onUiFakeProgressFinished() {
keyTransferServerInteractor = new KeyTransferInteractor(); view.showKeySentOk();
keyTransferServerInteractor.startServer(this);
} }
@Override @Override
public void onServerStarted(String qrCodeData) { public void onServerStarted(String qrCodeData) {
Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse("pgp+transfer://" + qrCodeData)); Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse("pgp+transfer://" + qrCodeData));
@@ -116,10 +120,57 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
@Override @Override
public void onConnectionLost() { public void onConnectionLost() {
view.showWaitingForConnection(); connectionStartListen();
startServer();
} }
@Override
public void onDataReceivedOk(String receivedData) {
Log.d(Constants.TAG, "received: " + receivedData);
}
@Override
public void onDataSentOk(String arg) {
Log.d(Constants.TAG, "data sent ok!");
view.showFakeSendProgressDialog();
}
private void connectionStartConnect(String qrCodeContent) {
connectionClear();
keyTransferClientInteractor = new KeyTransferInteractor();
keyTransferClientInteractor.connectToServer(qrCodeContent, this);
}
private void connectionStartListen() {
connectionClear();
keyTransferServerInteractor = new KeyTransferInteractor();
keyTransferServerInteractor.startServer(this);
view.showWaitingForConnection();
}
private void connectionClear() {
if (keyTransferServerInteractor != null) {
keyTransferServerInteractor.closeConnection();
keyTransferServerInteractor = null;
}
if (keyTransferClientInteractor != null) {
keyTransferClientInteractor.closeConnection();
keyTransferClientInteractor = null;
}
}
private void connectionSend(byte[] armoredSecretKey) {
if (keyTransferClientInteractor != null) {
keyTransferClientInteractor.sendData(armoredSecretKey);
} else if (keyTransferServerInteractor != null) {
keyTransferServerInteractor.sendData(armoredSecretKey);
}
}
@Override @Override
public Loader<List<SecretKeyItem>> onCreateLoader(int id, Bundle args) { public Loader<List<SecretKeyItem>> onCreateLoader(int id, Bundle args) {
return secretKeyAdapter.createLoader(context); return secretKeyAdapter.createLoader(context);
@@ -135,20 +186,6 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
secretKeyAdapter.setData(null); secretKeyAdapter.setData(null);
} }
@Override
public void onClickTransferKey(long masterKeyId) {
try {
byte[] armoredSecretKey =
KeyRepository.createDatabaseInteractor(context).getSecretKeyRingAsArmoredData(masterKeyId);
if (keyTransferClientInteractor != null) {
keyTransferClientInteractor.sendData(armoredSecretKey);
} else if (keyTransferServerInteractor != null) {
keyTransferServerInteractor.sendData(armoredSecretKey);
}
} catch (IOException | NotFoundException | PgpGeneralException e) {
e.printStackTrace();
}
}
public interface TransferMvpView { public interface TransferMvpView {
void showWaitingForConnection(); void showWaitingForConnection();
@@ -158,5 +195,8 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
void setQrImage(Bitmap qrCode); void setQrImage(Bitmap qrCode);
void setSecretKeyAdapter(Adapter adapter); void setSecretKeyAdapter(Adapter adapter);
void showFakeSendProgressDialog();
void showKeySentOk();
} }
} }

View File

@@ -18,11 +18,16 @@
package org.sufficientlysecure.keychain.ui.transfer.view; package org.sufficientlysecure.keychain.ui.transfer.view;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@@ -40,6 +45,7 @@ import android.widget.ViewAnimator;
import com.google.zxing.client.android.Intents; import com.google.zxing.client.android.Intents;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.QrCodeCaptureActivity; import org.sufficientlysecure.keychain.ui.QrCodeCaptureActivity;
import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter; import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter;
import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView; import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView;
@@ -48,6 +54,8 @@ import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.T
public class TransferFragment extends Fragment implements TransferMvpView { public class TransferFragment extends Fragment implements TransferMvpView {
public static final int VIEW_WAITING = 0; public static final int VIEW_WAITING = 0;
public static final int VIEW_CONNECTED = 1; public static final int VIEW_CONNECTED = 1;
public static final int VIEW_SEND_OK = 2;
public static final int REQUEST_CODE_SCAN = 1; public static final int REQUEST_CODE_SCAN = 1;
public static final int LOADER_ID = 1; public static final int LOADER_ID = 1;
@@ -74,7 +82,7 @@ public class TransferFragment extends Fragment implements TransferMvpView {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (presenter != null) { if (presenter != null) {
presenter.onClickScan(); presenter.onUiClickScan();
} }
} }
}); });
@@ -94,14 +102,14 @@ public class TransferFragment extends Fragment implements TransferMvpView {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
presenter.onStart(); presenter.onUiStart();
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
presenter.onStop(); presenter.onUiStop();
} }
@Override @Override
@@ -111,8 +119,13 @@ public class TransferFragment extends Fragment implements TransferMvpView {
@Override @Override
public void showConnectionEstablished(String hostname) { public void showConnectionEstablished(String hostname) {
vTransferAnimator.setDisplayedChild(VIEW_CONNECTED);
vConnectionStatusText.setText("Connected to: " + hostname); vConnectionStatusText.setText("Connected to: " + hostname);
vTransferAnimator.setDisplayedChild(VIEW_CONNECTED);
}
@Override
public void showKeySentOk() {
vTransferAnimator.setDisplayedChild(VIEW_SEND_OK);
} }
@Override @Override
@@ -141,13 +154,44 @@ public class TransferFragment extends Fragment implements TransferMvpView {
vTransferKeyList.setAdapter(adapter); vTransferKeyList.setAdapter(adapter);
} }
@Override
public void showFakeSendProgressDialog() {
final ProgressDialogFragment progressDialogFragment =
ProgressDialogFragment.newInstance("Sending key…", ProgressDialog.STYLE_HORIZONTAL, false);
progressDialogFragment.show(getFragmentManager(), "progress");
final Handler handler = new Handler();
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
int fakeProgress = 0;
@Override
public void run() {
fakeProgress += 6;
if (fakeProgress > 100) {
cancel();
progressDialogFragment.dismissAllowingStateLoss();
handler.post(new Runnable() {
@Override
public void run() {
presenter.onUiFakeProgressFinished();
}
});
return;
}
progressDialogFragment.setProgress(fakeProgress, 100);
}
}, 0, 100);
}
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { switch (requestCode) {
case REQUEST_CODE_SCAN: case REQUEST_CODE_SCAN:
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
String qrCodeData = data.getStringExtra(Intents.Scan.RESULT); String qrCodeData = data.getStringExtra(Intents.Scan.RESULT);
presenter.onQrCodeScanned(qrCodeData); presenter.onUiQrCodeScanned(qrCodeData);
} }
break; break;
default: default:

View File

@@ -129,7 +129,7 @@ public class TransferSecretKeyList extends RecyclerView {
vSendButton.setOnClickListener(new OnClickListener() { vSendButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
onClickTransferKeyListener.onClickTransferKey(item.masterKeyId); onClickTransferKeyListener.onUiClickTransferKey(item.masterKeyId);
} }
}); });
} else { } else {
@@ -139,6 +139,6 @@ public class TransferSecretKeyList extends RecyclerView {
} }
public interface OnClickTransferKeyListener { public interface OnClickTransferKeyListener {
void onClickTransferKey(long masterKeyId); void onUiClickTransferKey(long masterKeyId);
} }
} }

View File

@@ -1,80 +1,80 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:custom="http://schemas.android.com/apk/res-auto"> android:id="@+id/transfer_animator"
android:inAnimation="@anim/fade_in_delayed"
android:outAnimation="@anim/fade_out"
custom:initialView="0">
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:id="@+id/transfer_animator" android:gravity="center_vertical"
android:inAnimation="@anim/fade_in_delayed" android:orientation="vertical"
android:outAnimation="@anim/fade_out" android:padding="16dp">
custom:initialView="01">
<LinearLayout <ImageView
android:layout_width="match_parent" android:layout_width="250dp"
android:layout_height="250dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:id="@+id/qr_code_image"
tools:src="@drawable/ic_qrcode_white_24dp"
tools:tint="@color/md_black_1000"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_gravity="center_horizontal"
android:paddingLeft="16dp" android:layout_margin="8dp"
android:paddingRight="16dp" android:id="@+id/button_scan"
android:paddingTop="16dp"> android:text="Scan"
android:drawableLeft="@drawable/ic_qrcode_white_24dp"
android:drawablePadding="8dp"
/>
<ImageView <TextView
android:layout_width="250dp" android:layout_width="wrap_content"
android:layout_height="250dp"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:id="@+id/qr_code_image"
tools:src="@drawable/ic_qrcode_white_24dp"
tools:tint="@color/md_black_1000"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:id="@+id/button_scan"
android:text="Scan"
android:drawableLeft="@drawable/ic_qrcode_white_24dp"
android:drawablePadding="8dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:text="Scan with either device to establish a secure connection for device setup."
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:text="Scan with either device to establish a secure connection for device setup."
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView </LinearLayout>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:id="@+id/connection_status"
android:text="Connected to: 123.456.123.123"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center_horizontal" android:orientation="vertical">
android:text="Available Keys:"
android:textAppearance="?android:attr/textAppearanceMedium" <TextView
style="@style/SectionHeader" android:layout_width="wrap_content"
/> android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:id="@+id/connection_status"
android:text="Connected to: 123.456.123.123"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Available Keys:"
android:textAppearance="?android:attr/textAppearanceMedium"
style="@style/SectionHeader"
/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList <org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -83,8 +83,32 @@
android:padding="16dp" android:padding="16dp"
/> />
</LinearLayout> </ScrollView>
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator> </LinearLayout>
</ScrollView> <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:text="Key transfer ok!"
android:textAppearance="?android:attr/textAppearanceMedium" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Finish"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
</LinearLayout>
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>