+ *
+ * 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 .
+ */
+
+package org.sufficientlysecure.keychain.network;
+
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.List;
+
+import android.net.PskKeyManager;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.RequiresApi;
+import android.util.Base64;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.TrustManager;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public class KeyTransferInteractor {
+ private static final int SHOW_CONNECTION_DETAILS = 1;
+ private static final int CONNECTION_ESTABLISHED = 2;
+ private static final int CONNECTION_LOST = 3;
+
+
+ private TransferThread transferThread;
+
+
+ public void connectToServer(String connectionDetails, KeyTransferCallback callback) {
+ Uri uri = Uri.parse(connectionDetails);
+ final byte[] presharedKey = Base64.decode(uri.getUserInfo(), Base64.URL_SAFE | Base64.NO_PADDING);
+ final String host = uri.getHost();
+ final int port = uri.getPort();
+
+ transferThread = TransferThread.createClientTransferThread(callback, presharedKey, host, port);
+ transferThread.start();
+ }
+
+ public void startServer(KeyTransferCallback callback) {
+ byte[] presharedKey = generatePresharedKey();
+
+ transferThread = TransferThread.createServerTransferThread(callback, presharedKey);
+ transferThread.start();
+ }
+
+ private static class TransferThread extends Thread {
+ private final Handler handler;
+ private final KeyTransferCallback callback;
+ private final byte[] presharedKey;
+ private final boolean isServer;
+ private final String clientHost;
+ private final Integer clientPort;
+
+ private SSLServerSocket serverSocket;
+ private byte[] dataToSend;
+
+ static TransferThread createClientTransferThread(KeyTransferCallback callback, byte[] presharedKey,
+ String host, int port) {
+ return new TransferThread(callback, presharedKey, false, host, port);
+ }
+
+ static TransferThread createServerTransferThread(KeyTransferCallback callback, byte[] presharedKey) {
+ return new TransferThread(callback, presharedKey, true, null, null);
+ }
+
+ private TransferThread(KeyTransferCallback callback, byte[] presharedKey, boolean isServer,
+ String clientHost, Integer clientPort) {
+ this.callback = callback;
+ this.presharedKey = presharedKey;
+ this.clientHost = clientHost;
+ this.clientPort = clientPort;
+ this.isServer = isServer;
+
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void run() {
+ SSLContext sslContext = createTlsPskSslContext(presharedKey);
+
+ Socket socket = null;
+ try {
+ if (isServer) {
+ int port = 1336;
+ serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(port);
+
+ String presharedKeyEncoded = Base64.encodeToString(presharedKey, Base64.URL_SAFE | Base64.NO_PADDING);
+ String qrCodeData = presharedKeyEncoded + "@" + getIPAddress(true) + ":" + port;
+ invokeListener(SHOW_CONNECTION_DETAILS, qrCodeData);
+
+ socket = serverSocket.accept();
+ invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString());
+ } else {
+ socket = sslContext.getSocketFactory().createSocket(InetAddress.getByName(clientHost), clientPort);
+ invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString());
+ }
+
+ handleOpenConnection(socket);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "error!", e);
+ } finally {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ try {
+ if (serverSocket != null) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+
+ private static SSLContext createTlsPskSslContext(byte[] presharedKey) {
+ try {
+ PresharedKeyManager pskKeyManager = new PresharedKeyManager(presharedKey);
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null);
+
+ return sslContext;
+ } catch (KeyManagementException | NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private void handleOpenConnection(Socket socket) throws IOException {
+ socket.setSoTimeout(500);
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ while (!isInterrupted() && socket.isConnected()) {
+ if (dataToSend != null) {
+ BufferedOutputStream bufferedOutputStream =
+ new BufferedOutputStream(socket.getOutputStream());
+ bufferedOutputStream.write(dataToSend);
+ bufferedOutputStream.close();
+ dataToSend = null;
+ break;
+ }
+ try {
+ String line = bufferedReader.readLine();
+ if (line == null) {
+ Log.d(Constants.TAG, "eof");
+ break;
+ }
+ Log.d(Constants.TAG, "got line: " + line);
+ } catch (SocketTimeoutException e) {
+ // ignore
+ }
+ }
+ Log.d(Constants.TAG, "disconnected");
+ invokeListener(CONNECTION_LOST, null);
+ }
+
+ private void invokeListener(final int method, final String arg) {
+ if (handler == null) {
+ return;
+ }
+
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ if (callback == null) {
+ return;
+ }
+ switch (method) {
+ case SHOW_CONNECTION_DETAILS:
+ callback.onServerStarted(arg);
+ break;
+ case CONNECTION_ESTABLISHED:
+ callback.onConnectionEstablished(arg);
+ break;
+ case CONNECTION_LOST:
+ callback.onConnectionLost();
+ }
+ }
+ };
+
+ handler.post(runnable);
+ }
+
+ public synchronized void sendDataAndClose(byte[] dataToSend) {
+ this.dataToSend = dataToSend;
+ }
+
+ @Override
+ public void interrupt() {
+ super.interrupt();
+
+ if (serverSocket != null) {
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ private static byte[] generatePresharedKey() {
+ byte[] presharedKey = new byte[16];
+ new SecureRandom().nextBytes(presharedKey);
+ return presharedKey;
+ }
+
+ public void closeConnection() {
+ if (transferThread != null) {
+ transferThread.interrupt();
+ }
+
+ transferThread = null;
+ }
+
+ public void sendData(byte[] dataToSend) {
+ transferThread.sendDataAndClose(dataToSend);
+ }
+
+ public interface KeyTransferCallback {
+ void onServerStarted(String qrCodeData);
+ void onConnectionEstablished(String otherName);
+ void onConnectionLost();
+ }
+
+ /**
+ * from: http://stackoverflow.com/a/13007325
+ *
+ * Get IP address from first non-localhost interface
+ *
+ * @param useIPv4 true=return ipv4, false=return ipv6
+ * @return address or empty string
+ */
+ private static String getIPAddress(boolean useIPv4) {
+ try {
+ List interfaces = Collections.list(NetworkInterface.
+ getNetworkInterfaces());
+ for (NetworkInterface intf : interfaces) {
+ List addrs = Collections.list(intf.getInetAddresses());
+ for (InetAddress addr : addrs) {
+ if (!addr.isLoopbackAddress()) {
+ String sAddr = addr.getHostAddress();
+ boolean isIPv4 = sAddr.indexOf(':') < 0;
+
+ if (useIPv4) {
+ if (isIPv4)
+ return sAddr;
+ } else {
+ if (!isIPv4) {
+ int delim = sAddr.indexOf('%'); // drop ip6 zone suffix
+ return delim < 0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).
+ toUpperCase();
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception ex) {
+ } // for now eat exceptions
+ return "";
+ }
+
+ private static class PresharedKeyManager extends PskKeyManager implements KeyManager {
+ byte[] presharedKey;
+
+ private PresharedKeyManager(byte[] presharedKey) {
+ this.presharedKey = presharedKey;
+ }
+
+ @Override
+ public SecretKey getKey(String identityHint, String identity, Socket socket) {
+ return new SecretKeySpec(presharedKey, "AES");
+ }
+
+ @Override
+ public SecretKey getKey(String identityHint, String identity, SSLEngine engine) {
+ return new SecretKeySpec(presharedKey, "AES");
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java
deleted file mode 100644
index 73fb5e536..000000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright (C) 2017 Tobias Schülke
- *
- * 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 .
- */
-
-package org.sufficientlysecure.keychain.network;
-
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.Socket;
-import java.net.SocketTimeoutException;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import android.net.PskKeyManager;
-import android.os.Build.VERSION_CODES;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.RequiresApi;
-import android.util.Base64;
-
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.TrustManager;
-import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.util.Log;
-
-
-@RequiresApi(api = VERSION_CODES.LOLLIPOP)
-public class KeyTransferServerInteractor {
- private static final int SHOW_CONNECTION_DETAILS = 1;
- private static final int CONNECTION_ESTABLISHED = 2;
- public static final int CONNECTION_LOST = 3;
-
-
- private Thread socketThread;
- private KeyTransferServerCallback callback;
- private Handler handler;
- private SSLServerSocket serverSocket;
-
- public void startServer(KeyTransferServerCallback callback) {
- this.callback = callback;
-
- handler = new Handler(Looper.getMainLooper());
- socketThread = new Thread() {
- @Override
- public void run() {
- serverSocket = null;
- Socket socket = null;
- BufferedReader bufferedReader = null;
- try {
- int port = 1336;
-
- byte[] presharedKey = generatePresharedKey();
- PKM pskKeyManager = new PKM(presharedKey);
-
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null);
- serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(port);
-
- String presharedKeyEncoded = Base64.encodeToString(presharedKey, Base64.URL_SAFE | Base64.NO_PADDING);
- String qrCodeData = presharedKeyEncoded + "@" + getIPAddress(true) + ":" + port;
- invokeListener(SHOW_CONNECTION_DETAILS, qrCodeData);
-
- socket = serverSocket.accept();
- invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString());
- Arrays.fill(presharedKey, (byte) 0);
-
- socket.setSoTimeout(500);
- bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
- while (!isInterrupted() && socket.isConnected()) {
- try {
- String line = bufferedReader.readLine();
- if (line == null) {
- break;
- }
- Log.d(Constants.TAG, "got line: " + line);
- } catch (SocketTimeoutException e) {
- // ignore
- }
- }
- Log.d(Constants.TAG, "disconnected");
- invokeListener(CONNECTION_LOST, null);
- } catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
- Log.e(Constants.TAG, "error!", e);
- } finally {
- try {
- if (bufferedReader != null) {
- bufferedReader.close();
- }
- } catch (IOException e) {
- // ignore
- }
- try {
- if (socket != null) {
- socket.close();
- }
- } catch (IOException e) {
- // ignore
- }
- try {
- if (serverSocket != null) {
- serverSocket.close();
- }
- } catch (IOException e) {
- // ignore
- }
- }
- }
- };
-
- socketThread.start();
- }
-
- public byte[] generatePresharedKey() {
- byte[] presharedKey = new byte[16];
- new SecureRandom().nextBytes(presharedKey);
- return presharedKey;
- }
-
- public void stopServer() {
- if (socketThread != null) {
- socketThread.interrupt();
- }
-
- if (serverSocket != null) {
- try {
- serverSocket.close();
- } catch (IOException e) {
- // ignore
- }
- }
-
- socketThread = null;
- serverSocket = null;
- callback = null;
- }
-
- private void invokeListener(final int method, final String arg) {
- if (handler == null) {
- return;
- }
-
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- if (callback == null) {
- return;
- }
- switch (method) {
- case SHOW_CONNECTION_DETAILS:
- callback.onServerStarted(arg);
- break;
- case CONNECTION_ESTABLISHED:
- callback.onConnectionEstablished(arg);
- break;
- case CONNECTION_LOST:
- callback.onConnectionLost();
- }
- }
- };
-
- handler.post(runnable);
- }
-
- public interface KeyTransferServerCallback {
- void onServerStarted(String qrCodeData);
- void onConnectionEstablished(String otherName);
- void onConnectionLost();
- }
-
- /**
- * from: http://stackoverflow.com/a/13007325
- *
- * Get IP address from first non-localhost interface
- *
- * @param useIPv4 true=return ipv4, false=return ipv6
- * @return address or empty string
- */
- private static String getIPAddress(boolean useIPv4) {
- try {
- List interfaces = Collections.list(NetworkInterface.
- getNetworkInterfaces());
- for (NetworkInterface intf : interfaces) {
- List addrs = Collections.list(intf.getInetAddresses());
- for (InetAddress addr : addrs) {
- if (!addr.isLoopbackAddress()) {
- String sAddr = addr.getHostAddress();
- boolean isIPv4 = sAddr.indexOf(':') < 0;
-
- if (useIPv4) {
- if (isIPv4)
- return sAddr;
- } else {
- if (!isIPv4) {
- int delim = sAddr.indexOf('%'); // drop ip6 zone suffix
- return delim < 0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).
- toUpperCase();
- }
- }
- }
- }
- }
- } catch (Exception ex) {
- } // for now eat exceptions
- return "";
- }
-
- private static class PKM extends PskKeyManager implements KeyManager {
- byte[] presharedKey;
-
- private PKM(byte[] presharedKey) {
- this.presharedKey = presharedKey;
- }
-
- @Override
- public SecretKey getKey(String identityHint, String identity, Socket socket) {
- return new SecretKeySpec(presharedKey, "AES");
- }
-
- @Override
- public SecretKey getKey(String identityHint, String identity, SSLEngine engine) {
- return new SecretKeySpec(presharedKey, "AES");
- }
- }
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java
index 5277cd393..f2626bcf2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java
@@ -13,12 +13,12 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
-import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
@@ -236,19 +236,27 @@ public class KeyRepository {
}
}
- private String getKeyRingAsArmoredString(byte[] data) throws IOException, PgpGeneralException {
- UncachedKeyRing keyRing = UncachedKeyRing.decodeFromData(data);
-
+ private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException, PgpGeneralException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
- keyRing.encodeArmored(bos, null);
+ ArmoredOutputStream aos = new ArmoredOutputStream(bos);
- return bos.toString("UTF-8");
+ aos.write(data);
+ aos.close();
+
+ return bos.toByteArray();
}
public String getPublicKeyRingAsArmoredString(long masterKeyId)
throws NotFoundException, IOException, PgpGeneralException {
byte[] data = loadPublicKeyRingData(masterKeyId);
- return getKeyRingAsArmoredString(data);
+ byte[] armoredData = getKeyRingAsArmoredData(data);
+ return new String(armoredData);
+ }
+
+ public byte[] getSecretKeyRingAsArmoredData(long masterKeyId)
+ throws NotFoundException, IOException, PgpGeneralException {
+ byte[] data = loadSecretKeyRingData(masterKeyId);
+ return getKeyRingAsArmoredData(data);
}
public ContentResolver getContentResolver() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java
new file mode 100644
index 000000000..b4b8e4824
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2017 Vincent Breitmoser
+ *
+ * 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 .
+ */
+
+package org.sufficientlysecure.keychain.ui.transfer.loader;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.content.AsyncTaskLoader;
+import android.util.Log;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem;
+
+
+public class SecretKeyLoader extends AsyncTaskLoader> {
+ public static final String[] PROJECTION = new String[] {
+ KeyRings.MASTER_KEY_ID,
+ KeyRings.CREATION,
+ KeyRings.NAME,
+ KeyRings.EMAIL,
+ KeyRings.HAS_ANY_SECRET
+ };
+ private static final int INDEX_KEY_ID = 0;
+ private static final int INDEX_CREATION = 1;
+ private static final int INDEX_NAME = 2;
+ private static final int INDEX_EMAIL = 3;
+
+
+ private final ContentResolver contentResolver;
+
+ private List cachedResult;
+
+
+ public SecretKeyLoader(Context context, ContentResolver contentResolver) {
+ super(context);
+
+ this.contentResolver = contentResolver;
+ }
+
+ @Override
+ public List loadInBackground() {
+ String where = KeyRings.HAS_ANY_SECRET + " = 1";
+ Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, where, null, null);
+ if (cursor == null) {
+ Log.e(Constants.TAG, "Error loading key items!");
+ return null;
+ }
+
+ try {
+ ArrayList secretKeyItems = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ SecretKeyItem secretKeyItem = new SecretKeyItem(cursor);
+ secretKeyItems.add(secretKeyItem);
+ }
+
+ return Collections.unmodifiableList(secretKeyItems);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public void deliverResult(List keySubkeyStatus) {
+ cachedResult = keySubkeyStatus;
+
+ if (isStarted()) {
+ super.deliverResult(keySubkeyStatus);
+ }
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (cachedResult != null) {
+ deliverResult(cachedResult);
+ }
+
+ if (takeContentChanged() || cachedResult == null) {
+ forceLoad();
+ }
+ }
+
+ public static class SecretKeyItem {
+ final int position;
+ public final long masterKeyId;
+ public final long creationMillis;
+ public final String name;
+ public final String email;
+
+ SecretKeyItem(Cursor cursor) {
+ position = cursor.getPosition();
+
+ masterKeyId = cursor.getLong(INDEX_KEY_ID);
+ creationMillis = cursor.getLong(INDEX_CREATION) * 1000;
+
+ name = cursor.getString(INDEX_NAME);
+ email = cursor.getString(INDEX_EMAIL);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java
index f211bd7ec..9e0d049de 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java
@@ -18,33 +18,61 @@
package org.sufficientlysecure.keychain.ui.transfer.presenter;
+import java.io.IOException;
+import java.util.List;
+
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
import android.support.annotation.RequiresApi;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.view.LayoutInflater;
-import org.sufficientlysecure.keychain.network.KeyTransferClientInteractor;
-import org.sufficientlysecure.keychain.network.KeyTransferClientInteractor.KeyTransferClientCallback;
-import org.sufficientlysecure.keychain.network.KeyTransferServerInteractor;
-import org.sufficientlysecure.keychain.network.KeyTransferServerInteractor.KeyTransferServerCallback;
+import org.sufficientlysecure.keychain.network.KeyTransferInteractor;
+import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.KeyRepository;
+import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
+import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem;
+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;
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
-public class TransferPresenter implements KeyTransferServerCallback, KeyTransferClientCallback {
+public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks>,
+ OnClickTransferKeyListener {
private final Context context;
private final TransferMvpView view;
+ private final LoaderManager loaderManager;
+ private final int loaderId;
- private KeyTransferServerInteractor keyTransferServerInteractor;
- private KeyTransferClientInteractor keyTransferClientInteractor;
+ private KeyTransferInteractor keyTransferClientInteractor;
+ private KeyTransferInteractor keyTransferServerInteractor;
+ private final TransferKeyAdapter secretKeyAdapter;
- public TransferPresenter(Context context, TransferMvpView view) {
+ public TransferPresenter(Context context, LoaderManager loaderManager, int loaderId, TransferMvpView view) {
this.context = context;
this.view = view;
+ this.loaderManager = loaderManager;
+ this.loaderId = loaderId;
+
+ secretKeyAdapter = new TransferKeyAdapter(context, LayoutInflater.from(context), this);
+ view.setSecretKeyAdapter(secretKeyAdapter);
}
- public void onDestroy() {
+ public void onStart() {
+ loaderManager.restartLoader(loaderId, null, this);
+
+ startServer();
+ }
+
+ public void onStop() {
clearConnections();
}
@@ -55,13 +83,13 @@ public class TransferPresenter implements KeyTransferServerCallback, KeyTransfer
}
public void onQrCodeScanned(String qrCodeContent) {
- keyTransferClientInteractor = new KeyTransferClientInteractor();
+ keyTransferClientInteractor = new KeyTransferInteractor();
keyTransferClientInteractor.connectToServer(qrCodeContent, this);
}
private void clearConnections() {
if (keyTransferServerInteractor != null) {
- keyTransferServerInteractor.stopServer();
+ keyTransferServerInteractor.closeConnection();
keyTransferServerInteractor = null;
}
if (keyTransferClientInteractor != null) {
@@ -71,7 +99,7 @@ public class TransferPresenter implements KeyTransferServerCallback, KeyTransfer
}
public void startServer() {
- keyTransferServerInteractor = new KeyTransferServerInteractor();
+ keyTransferServerInteractor = new KeyTransferInteractor();
keyTransferServerInteractor.startServer(this);
}
@@ -92,12 +120,43 @@ public class TransferPresenter implements KeyTransferServerCallback, KeyTransfer
startServer();
}
- public interface TransferMvpView {
- void showConnectionEstablished(String hostname);
- void showWaitingForConnection();
+ @Override
+ public Loader> onCreateLoader(int id, Bundle args) {
+ return secretKeyAdapter.createLoader(context);
+ }
- void setQrImage(Bitmap qrCode);
+ @Override
+ public void onLoadFinished(Loader> loader, List data) {
+ secretKeyAdapter.setData(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader> loader) {
+ 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 {
+ void showWaitingForConnection();
+ void showConnectionEstablished(String hostname);
void scanQrCode();
+ void setQrImage(Bitmap qrCode);
+
+ void setSecretKeyAdapter(Adapter adapter);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java
index 322df19a8..a8cf8f808 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java
@@ -26,6 +26,8 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.Fragment;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -47,12 +49,14 @@ public class TransferFragment extends Fragment implements TransferMvpView {
public static final int VIEW_WAITING = 0;
public static final int VIEW_CONNECTED = 1;
public static final int REQUEST_CODE_SCAN = 1;
+ public static final int LOADER_ID = 1;
private ImageView vQrCodeImage;
private TransferPresenter presenter;
private ViewAnimator vTransferAnimator;
private TextView vConnectionStatusText;
+ private RecyclerView vTransferKeyList;
@Override
@@ -62,6 +66,7 @@ public class TransferFragment extends Fragment implements TransferMvpView {
vTransferAnimator = (ViewAnimator) view.findViewById(R.id.transfer_animator);
vConnectionStatusText = (TextView) view.findViewById(R.id.connection_status);
+ vTransferKeyList = (RecyclerView) view.findViewById(R.id.transfer_key_list);
vQrCodeImage = (ImageView) view.findViewById(R.id.qr_code_image);
@@ -82,22 +87,21 @@ public class TransferFragment extends Fragment implements TransferMvpView {
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- presenter = new TransferPresenter(getContext(), this);
+ presenter = new TransferPresenter(getContext(), getLoaderManager(), LOADER_ID, this);
}
@Override
public void onStart() {
super.onStart();
- presenter.startServer();
+ presenter.onStart();
}
-
@Override
public void onStop() {
super.onStop();
- presenter.onDestroy();
+ presenter.onStop();
}
@Override
@@ -132,6 +136,11 @@ public class TransferFragment extends Fragment implements TransferMvpView {
startActivityForResult(intent, REQUEST_CODE_SCAN);
}
+ @Override
+ public void setSecretKeyAdapter(Adapter adapter) {
+ vTransferKeyList.setAdapter(adapter);
+ }
+
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java
new file mode 100644
index 000000000..46b86be67
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java
@@ -0,0 +1,144 @@
+package org.sufficientlysecure.keychain.ui.transfer.view;
+
+
+import java.util.List;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v4.content.Loader;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.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 org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader;
+import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem;
+import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
+
+
+public class TransferSecretKeyList extends RecyclerView {
+ private OnClickTransferKeyListener onClickTransferKeyListener;
+
+ 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));
+ }
+
+ public static class TransferKeyAdapter extends RecyclerView.Adapter {
+ private final Context context;
+ private final LayoutInflater layoutInflater;
+ private final OnClickTransferKeyListener onClickTransferKeyListener;
+
+ private List data;
+
+
+ public TransferKeyAdapter(Context context, LayoutInflater layoutInflater,
+ OnClickTransferKeyListener onClickTransferKeyListener) {
+ this.context = context;
+ this.layoutInflater = layoutInflater;
+ this.onClickTransferKeyListener = onClickTransferKeyListener;
+ }
+
+ @Override
+ public TransferKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new TransferKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(TransferKeyViewHolder holder, int position) {
+ SecretKeyItem item = data.get(position);
+ holder.bind(context, item, onClickTransferKeyListener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return data != null ? data.size() : 0;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return data.get(position).masterKeyId;
+ }
+
+ public void setData(List data) {
+ this.data = data;
+ notifyDataSetChanged();
+ }
+
+ public Loader> createLoader(Context context) {
+ return new SecretKeyLoader(context, context.getContentResolver());
+ }
+ }
+
+ static class TransferKeyViewHolder extends RecyclerView.ViewHolder {
+ private final TextView vName;
+ private final TextView vEmail;
+ private final TextView vCreation;
+ private final View vSendButton;
+
+ public TransferKeyViewHolder(View itemView) {
+ super(itemView);
+
+ vName = (TextView) itemView.findViewById(R.id.key_list_item_name);
+ vEmail = (TextView) itemView.findViewById(R.id.key_list_item_email);
+ vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation);
+
+ vSendButton = itemView.findViewById(R.id.button_transfer);
+ }
+
+ private void bind(Context context, final SecretKeyItem item, final OnClickTransferKeyListener onClickTransferKeyListener) {
+ 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 (onClickTransferKeyListener != null) {
+ vSendButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onClickTransferKeyListener.onClickTransferKey(item.masterKeyId);
+ }
+ });
+ } else {
+ vSendButton.setOnClickListener(null);
+ }
+ }
+ }
+
+ public interface OnClickTransferKeyListener {
+ void onClickTransferKey(long masterKeyId);
+ }
+}
diff --git a/OpenKeychain/src/main/res/layout/key_transfer_item.xml b/OpenKeychain/src/main/res/layout/key_transfer_item.xml
new file mode 100644
index 000000000..9271f66a2
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/key_transfer_item.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenKeychain/src/main/res/layout/transfer_fragment.xml b/OpenKeychain/src/main/res/layout/transfer_fragment.xml
index b99369751..6e19a1373 100644
--- a/OpenKeychain/src/main/res/layout/transfer_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/transfer_fragment.xml
@@ -55,27 +55,33 @@
+ android:orientation="vertical">
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ style="@style/SectionHeader"
+ />
+
+