+ *
+ * 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.Closeable;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.NoRouteToHostException;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.URISyntaxException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.net.PskKeyManager;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+
+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.SSLHandshakeException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+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 String[] ALLOWED_CIPHERSUITES = new String[] {
+ // only allow ephemeral diffie-hellman based PSK ciphers!
+ "TLS_DHE_PSK_WITH_AES_128_CBC_SHA",
+ "TLS_DHE_PSK_WITH_AES_256_CBC_SHA",
+ "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA",
+ "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA",
+ "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256"
+ };
+
+ private static final int CONNECTION_LISTENING = 1;
+ private static final int CONNECTION_ESTABLISHED = 2;
+ private static final int CONNECTION_SEND_OK = 3;
+ private static final int CONNECTION_RECEIVE_OK = 4;
+ private static final int CONNECTION_LOST = 5;
+ private static final int CONNECTION_ERROR_NO_ROUTE_TO_HOST = 6;
+ private static final int CONNECTION_ERROR_CONNECT = 7;
+ private static final int CONNECTION_ERROR_WHILE_CONNECTED = 8;
+ private static final int CONNECTION_ERROR_LISTEN = 0;
+
+ private static final int TIMEOUT_CONNECTING = 1500;
+ private static final int TIMEOUT_RECEIVING = 2000;
+ private static final int TIMEOUT_WAITING = 500;
+ private static final int PSK_BYTE_LENGTH = 16;
+
+
+ private final String delimiterStart;
+ private final String delimiterEnd;
+
+ private TransferThread transferThread;
+
+
+ public KeyTransferInteractor(String delimiterStart, String delimiterEnd) {
+ this.delimiterStart = delimiterStart;
+ this.delimiterEnd = delimiterEnd;
+ }
+
+ public void connectToServer(String qrCodeContent, KeyTransferCallback callback) throws URISyntaxException {
+ SktUri sktUri = SktUri.parse(qrCodeContent);
+
+ transferThread = TransferThread.createClientTransferThread(delimiterStart, delimiterEnd, callback,
+ sktUri.getPresharedKey(), sktUri.getHost(), sktUri.getPort(), sktUri.getWifiSsid());
+ transferThread.start();
+ }
+
+ public void startServer(KeyTransferCallback callback, String wifiSsid) {
+ byte[] presharedKey = generatePresharedKey();
+
+ transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey, wifiSsid);
+ transferThread.start();
+ }
+
+ private static class TransferThread extends Thread {
+ private final String delimiterStart;
+ private final String delimiterEnd;
+
+ private final Handler handler;
+ private final byte[] presharedKey;
+ private final boolean isServer;
+ private final String clientHost;
+ private final Integer clientPort;
+ private final String wifiSsid;
+
+ private KeyTransferCallback callback;
+ private SSLServerSocket serverSocket;
+ private byte[] dataToSend;
+ private String sendPassthrough;
+
+ static TransferThread createClientTransferThread(String delimiterStart, String delimiterEnd,
+ KeyTransferCallback callback, byte[] presharedKey, String host, int port, String wifiSsid) {
+ return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, false, host, port, wifiSsid);
+ }
+
+ static TransferThread createServerTransferThread(String delimiterStart, String delimiterEnd,
+ KeyTransferCallback callback, byte[] presharedKey, String wifiSsid) {
+ return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, true, null, null, wifiSsid);
+ }
+
+ private TransferThread(String delimiterStart, String delimiterEnd,
+ KeyTransferCallback callback, byte[] presharedKey, boolean isServer,
+ String clientHost, Integer clientPort, String wifiSsid) {
+ super("TLS-PSK Key Transfer Thread");
+
+ this.delimiterStart = delimiterStart;
+ this.delimiterEnd = delimiterEnd;
+
+ this.callback = callback;
+ this.presharedKey = presharedKey;
+ this.clientHost = clientHost;
+ this.clientPort = clientPort;
+ this.wifiSsid = wifiSsid;
+ this.isServer = isServer;
+
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void run() {
+ SSLContext sslContext = createTlsPskSslContext(presharedKey);
+
+ Socket socket = null;
+ try {
+ socket = getSocketListenOrConnect(sslContext);
+ if (socket == null) {
+ return;
+ }
+
+ try {
+ handleOpenConnection(socket);
+ Log.d(Constants.TAG, "connection closed ok!");
+ } catch (SSLHandshakeException e) {
+ Log.d(Constants.TAG, "ssl handshake error!", e);
+ invokeListener(CONNECTION_ERROR_CONNECT, null);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "communication error!", e);
+ invokeListener(CONNECTION_ERROR_WHILE_CONNECTED, e.getLocalizedMessage());
+ }
+ } finally {
+ closeQuietly(socket);
+ closeQuietly(serverSocket);
+ }
+ }
+
+ @Nullable
+ private Socket getSocketListenOrConnect(SSLContext sslContext) {
+ Socket socket;
+ if (isServer) {
+ try {
+ serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(0);
+ String[] supportedCipherSuites = serverSocket.getSupportedCipherSuites();
+ String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES);
+ serverSocket.setEnabledCipherSuites(enabledCipherSuites);
+
+ SktUri sktUri = SktUri.create(getIPAddress(true), serverSocket.getLocalPort(), presharedKey, wifiSsid);
+ invokeListener(CONNECTION_LISTENING, sktUri.toUriString());
+
+ socket = serverSocket.accept();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "error while listening!", e);
+ invokeListener(CONNECTION_ERROR_LISTEN, null);
+ return null;
+ }
+ } else {
+ try {
+ SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket();
+ String[] supportedCipherSuites = sslSocket.getSupportedCipherSuites();
+ String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES);
+ sslSocket.setEnabledCipherSuites(enabledCipherSuites);
+
+ socket = sslSocket;
+ socket.connect(new InetSocketAddress(InetAddress.getByName(clientHost), clientPort), TIMEOUT_CONNECTING);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "error while connecting!", e);
+ if (e instanceof NoRouteToHostException) {
+ invokeListener(CONNECTION_ERROR_NO_ROUTE_TO_HOST, wifiSsid);
+ } else {
+ invokeListener(CONNECTION_ERROR_CONNECT, null);
+ }
+ return null;
+ }
+ }
+ return socket;
+ }
+
+ 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 {
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ OutputStream outputStream = new BufferedOutputStream(socket.getOutputStream());
+
+ invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString());
+
+ socket.setSoTimeout(TIMEOUT_WAITING);
+ while (!isInterrupted() && socket.isConnected() && !socket.isClosed()) {
+ sendDataIfAvailable(socket, outputStream);
+ boolean connectionTerminated = receiveDataIfAvailable(socket, bufferedReader);
+ if (connectionTerminated) {
+ break;
+ }
+ }
+ Log.d(Constants.TAG, "disconnected");
+ 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) {
+ return true;
+ }
+
+ boolean lineIsDelimiter = delimiterStart.equals(firstLine);
+ if (!lineIsDelimiter) {
+ Log.d(Constants.TAG, "bad beginning of key block?");
+ return false;
+ }
+
+ socket.setSoTimeout(TIMEOUT_RECEIVING);
+ String receivedData = receiveLinesUntilEndDelimiter(bufferedReader, firstLine);
+ socket.setSoTimeout(TIMEOUT_WAITING);
+
+ invokeListener(CONNECTION_RECEIVE_OK, receivedData);
+ return false;
+ }
+
+ private boolean sendDataIfAvailable(Socket socket, OutputStream outputStream) throws IOException {
+ if (dataToSend != null) {
+ byte[] data = dataToSend;
+ dataToSend = null;
+
+ socket.setSoTimeout(TIMEOUT_RECEIVING);
+ outputStream.write(data);
+ outputStream.flush();
+ socket.setSoTimeout(TIMEOUT_WAITING);
+
+ invokeListener(CONNECTION_SEND_OK, sendPassthrough);
+ sendPassthrough = null;
+ return true;
+ }
+ return false;
+ }
+
+ private String receiveLinesUntilEndDelimiter(BufferedReader bufferedReader, String line) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ do {
+ boolean lineIsDelimiter = delimiterEnd.equals(line);
+ if (lineIsDelimiter) {
+ break;
+ }
+
+ builder.append(line).append('\n');
+
+ line = bufferedReader.readLine();
+ } while (line != null);
+
+ return builder.toString();
+ }
+
+ 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 CONNECTION_LISTENING:
+ callback.onServerStarted(arg);
+ break;
+ case CONNECTION_ESTABLISHED:
+ callback.onConnectionEstablished(arg);
+ break;
+ case CONNECTION_RECEIVE_OK:
+ callback.onDataReceivedOk(arg);
+ break;
+ case CONNECTION_SEND_OK:
+ callback.onDataSentOk(arg);
+ break;
+ case CONNECTION_LOST:
+ callback.onConnectionLost();
+ break;
+ case CONNECTION_ERROR_WHILE_CONNECTED:
+ callback.onConnectionError(arg);
+ break;
+ case CONNECTION_ERROR_NO_ROUTE_TO_HOST:
+ callback.onConnectionErrorNoRouteToHost(wifiSsid);
+ break;
+ case CONNECTION_ERROR_CONNECT:
+ callback.onConnectionErrorConnect();
+ break;
+ case CONNECTION_ERROR_LISTEN:
+ callback.onConnectionErrorListen();
+ break;
+ }
+ }
+ };
+
+ handler.post(runnable);
+ }
+
+ synchronized void sendData(byte[] dataToSend, String passthrough) {
+ this.dataToSend = dataToSend;
+ this.sendPassthrough = passthrough;
+ }
+
+ @Override
+ public void interrupt() {
+ callback = null;
+ super.interrupt();
+ closeQuietly(serverSocket);
+ }
+ }
+
+ private static byte[] generatePresharedKey() {
+ byte[] presharedKey = new byte[PSK_BYTE_LENGTH];
+ new SecureRandom().nextBytes(presharedKey);
+ return presharedKey;
+ }
+
+ public void closeConnection() {
+ if (transferThread != null) {
+ transferThread.interrupt();
+ }
+
+ transferThread = null;
+ }
+
+ public void sendData(byte[] dataToSend, String passthrough) {
+ transferThread.sendData(dataToSend, passthrough);
+ }
+
+ public interface KeyTransferCallback {
+ void onServerStarted(String qrCodeData);
+ void onConnectionEstablished(String otherName);
+ void onConnectionLost();
+
+ void onDataReceivedOk(String receivedData);
+ void onDataSentOk(String passthrough);
+
+ void onConnectionErrorConnect();
+ void onConnectionErrorNoRouteToHost(String wifiSsid);
+ void onConnectionErrorListen();
+ void onConnectionError(String arg);
+ }
+
+ /**
+ * 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()) {
+ continue;
+ }
+ String sAddr = addr.getHostAddress();
+ boolean isIPv4 = sAddr.indexOf(':') < 0;
+ if (useIPv4) {
+ if (isIPv4) {
+ return sAddr;
+ }
+ } else {
+ int delimIndex = sAddr.indexOf('%'); // drop ip6 zone suffix
+ if (delimIndex >= 0) {
+ sAddr = sAddr.substring(0, delimIndex);
+ }
+ return sAddr.toUpperCase();
+ }
+ }
+ }
+ } catch (Exception ex) {
+ // ignore
+ }
+ return "";
+ }
+
+ private static class PresharedKeyManager extends PskKeyManager implements KeyManager {
+ byte[] presharedKey;
+
+ private PresharedKeyManager(byte[] presharedKey) {
+ this.presharedKey = presharedKey;
+ }
+
+ @Override
+ public String chooseClientKeyIdentity(String identityHint, Socket socket) {
+ return identityHint;
+ }
+
+ @Override
+ public String chooseClientKeyIdentity(String identityHint, SSLEngine engine) {
+ return identityHint;
+ }
+
+ @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");
+ }
+ }
+
+ private static void closeQuietly(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+
+ private static String[] intersectArrays(String[] array1, String[] array2) {
+ Set s1 = new HashSet<>(Arrays.asList(array1));
+ Set s2 = new HashSet<>(Arrays.asList(array2));
+ s1.retainAll(s2);
+
+ return s1.toArray(new String[0]);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/SktUri.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/SktUri.java
new file mode 100644
index 000000000..e73f756d6
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/SktUri.java
@@ -0,0 +1,90 @@
+package org.sufficientlysecure.keychain.network;
+
+
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+
+import android.annotation.SuppressLint;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.auto.value.AutoValue;
+import org.bouncycastle.util.encoders.DecoderException;
+import org.bouncycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+@AutoValue
+abstract class SktUri {
+ private static final String QRCODE_URI_FORMAT = Constants.SKT_SCHEME + ":%s/%d/%s";
+ private static final String QRCODE_URI_FORMAT_SSID = Constants.SKT_SCHEME + ":%s/%d/%s/SSID:%s";
+
+
+ public abstract String getHost();
+ public abstract int getPort();
+ public abstract byte[] getPresharedKey();
+
+ @Nullable
+ public abstract String getWifiSsid();
+
+ @NonNull
+ public static SktUri parse(String input) throws URISyntaxException {
+ if (!input.startsWith(Constants.SKT_SCHEME + ":")) {
+ throw new URISyntaxException(input, "invalid scheme");
+ }
+
+ String[] pieces = input.substring(input.indexOf(":") +1).split("/");
+ if (pieces.length < 3) {
+ throw new URISyntaxException(input, "invalid syntax");
+ }
+
+ String address = pieces[0];
+ int port;
+ try {
+ port = Integer.parseInt(pieces[1]);
+ } catch (NumberFormatException e) {
+ throw new URISyntaxException(input, "error parsing port");
+ }
+ byte[] psk;
+ try {
+ psk = Hex.decode(pieces[2]);
+ } catch (DecoderException e) {
+ throw new URISyntaxException(input, "error parsing hex psk");
+ }
+
+ String wifiSsid = null;
+ for (int i = 3; i < pieces.length; i++) {
+ String[] optarg = pieces[i].split(":", 2);
+ if (optarg.length == 2 && "SSID".equals(optarg[0])) {
+ try {
+ wifiSsid = new String(Hex.decode(optarg[1]));
+ } catch (DecoderException e) {
+ Log.d(Constants.TAG, "error parsing ssid in skt uri, ignoring: " + input);
+ }
+ }
+ }
+
+ return new AutoValue_SktUri(address, port, psk, wifiSsid);
+ }
+
+ @SuppressLint("DefaultLocale")
+ String toUriString() {
+ String sktHex = Hex.toHexString(getPresharedKey());
+ String wifiSsid = getWifiSsid();
+
+ String result;
+ if (wifiSsid != null) {
+ String encodedWifiSsid = Hex.toHexString(getWifiSsid().getBytes(Charset.defaultCharset()));
+ result = String.format(QRCODE_URI_FORMAT_SSID, getHost(), getPort(), sktHex, encodedWifiSsid);
+ } else {
+ result = String.format(QRCODE_URI_FORMAT, getHost(), getPort(), sktHex);
+ }
+
+ return result.toUpperCase();
+ }
+
+ static SktUri create(String host, int port, byte[] presharedKey, @Nullable String wifiSsid) {
+ return new AutoValue_SktUri(host, port, presharedKey, wifiSsid);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java
index 4d584331a..a6bc0233b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java
@@ -22,6 +22,7 @@ package org.sufficientlysecure.keychain.operations;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
@@ -477,7 +478,7 @@ public class ImportOperation extends BaseReadWriteOperation
@NonNull
@Override
public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
- ArrayList keyList = importInput.getKeyList();
+ List keyList = importInput.getKeyList();
HkpKeyserverAddress keyServer = importInput.getKeyserver();
boolean skipSave = importInput.isSkipSave();
@@ -510,7 +511,7 @@ public class ImportOperation extends BaseReadWriteOperation
}
@NonNull
- private ImportKeyResult multiThreadedKeyImport(ArrayList keyList,
+ private ImportKeyResult multiThreadedKeyImport(List keyList,
final HkpKeyserverAddress keyServer, final ParcelableProxy proxy,
final boolean skipSave) {
Log.d(Constants.TAG, "Multi-threaded key import starting");
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
index acdbb93fc..850c24640 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
@@ -103,6 +103,10 @@ public class UncachedKeyRing {
return mRing.getPublicKey().getKeyID();
}
+ public long getCreationTime() {
+ return mRing.getPublicKey().getCreationTime().getTime();
+ }
+
public UncachedPublicKey getPublicKey() {
return new UncachedPublicKey(mRing.getPublicKey());
}
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/remote/ui/dialog/RemoteDeduplicateActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java
index 6597e91e1..6aee2a219 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java
@@ -122,7 +122,8 @@ public class RemoteDeduplicateActivity extends FragmentActivity {
keyChoiceList = (RecyclerView) view.findViewById(R.id.duplicate_key_list);
keyChoiceList.setLayoutManager(new LinearLayoutManager(activity));
- keyChoiceList.addItemDecoration(new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST));
+ keyChoiceList.addItemDecoration(
+ new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true));
setupListenersForPresenter();
mvpView = createMvpView(view, layoutInflater);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java
index 7b9c4f60d..e7a0e2d51 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java
@@ -20,6 +20,8 @@ package org.sufficientlysecure.keychain.service;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -31,21 +33,25 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
@AutoValue
public abstract class ImportKeyringParcel implements Parcelable {
@Nullable // If null, keys are expected to be read from a cache file in ImportExportOperations
- public abstract ArrayList getKeyList();
+ public abstract List getKeyList();
@Nullable // must be set if keys are to be imported from a keyserver
public abstract HkpKeyserverAddress getKeyserver();
public abstract boolean isSkipSave();
- public static ImportKeyringParcel createImportKeyringParcel(ArrayList keyList,
+ public static ImportKeyringParcel createImportKeyringParcel(List keyList,
HkpKeyserverAddress keyserver) {
return new AutoValue_ImportKeyringParcel(keyList, keyserver, false);
}
- public static ImportKeyringParcel createWithSkipSave(ArrayList keyList,
+ public static ImportKeyringParcel createWithSkipSave(List keyList,
HkpKeyserverAddress keyserver) {
return new AutoValue_ImportKeyringParcel(keyList, keyserver, true);
}
+ public static ImportKeyringParcel createImportKeyringParcel(ParcelableKeyRing key) {
+ return new AutoValue_ImportKeyringParcel(Collections.singletonList(key), null, false);
+ }
+
public static ImportKeyringParcel createFromFileCacheWithSkipSave() {
return new AutoValue_ImportKeyringParcel(null, null, true);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java
index 007049e15..e3e1719a2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java
@@ -17,23 +17,31 @@
package org.sufficientlysecure.keychain.ui;
+
+import android.annotation.TargetApi;
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 android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
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.Log;
import org.sufficientlysecure.keychain.util.Preferences;
public class CreateKeyStartFragment extends Fragment {
+ public static final int REQUEST_CODE_IMPORT_KEY = 0x00007012;
+
CreateKeyActivity mCreateKeyActivity;
@@ -41,7 +49,8 @@ public class CreateKeyStartFragment extends Fragment {
View mImportKey;
View mSecurityToken;
TextView mSkipOrCancel;
- public static final int REQUEST_CODE_IMPORT_KEY = 0x00007012;
+ View mSecureDeviceSetup;
+
/**
* Creates new instance of this fragment
@@ -64,6 +73,7 @@ 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 = (TextView) 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,6 +106,19 @@ public class CreateKeyStartFragment extends Fragment {
}
});
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ mSecureDeviceSetup.setOnClickListener(new OnClickListener() {
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ @Override
+ public void onClick(View v) {
+ TransferFragment frag = new TransferFragment();
+ mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
+ }
+ });
+ } else {
+ mSecureDeviceSetup.setVisibility(View.GONE);
+ }
+
mSkipOrCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java
index b7260d34f..0eb74f8a5 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java
@@ -40,6 +40,7 @@ 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.util.Log;
@@ -143,6 +144,17 @@ public class ImportKeysProxyActivity extends FragmentActivity
Log.d(Constants.TAG, "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)) {
@@ -153,6 +165,7 @@ public class ImportKeysProxyActivity extends FragmentActivity
returnResult(intent);
return;
}
+
final String fingerprintHex = uri.getEncodedSchemeSpecificPart().toLowerCase(Locale.ENGLISH);
if (!fingerprintHex.matches("[a-fA-F0-9]{40}")) {
SingletonResult result = new SingletonResult(
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
index 13df0b539..015161c9e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
@@ -20,6 +20,8 @@
package org.sufficientlysecure.keychain.ui;
import android.content.Intent;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
@@ -41,6 +43,8 @@ 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;
@@ -50,8 +54,9 @@ 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;
- static final int ID_SETTINGS = 5;
- static final int ID_HELP = 6;
+ public static final int ID_TRANSFER = 5;
+ static final int ID_SETTINGS = 6;
+ static final int ID_HELP = 7;
// both of these are used for instrumentation testing only
public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time";
@@ -82,6 +87,11 @@ 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)
@@ -105,6 +115,9 @@ 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;
@@ -160,13 +173,36 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_APPS:
onAppsSelected();
break;
+ case ID_TRANSFER:
+ onTransferSelected();
+ break;
}
}
}
- private void setFragment(Fragment fragment, boolean addToBackStack) {
+ @Override
+ public void onNewIntent(Intent data) {
+ super.onNewIntent(data);
+ setIntent(data);
+ if (data != null && data.hasExtra(EXTRA_INIT_FRAG)) {
+ // initialize FragmentLayout with KeyListFragment at first
+ switch (data.getIntExtra(EXTRA_INIT_FRAG, -1)) {
+ case ID_ENCRYPT_DECRYPT:
+ onEnDecryptSelected();
+ break;
+ case ID_APPS:
+ onAppsSelected();
+ break;
+ case ID_TRANSFER:
+ onTransferSelected();
+ break;
+ }
+ }
+ }
+
+ private void setFragment(Fragment fragment, boolean addToBackStack) {
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
@@ -175,11 +211,12 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
if (addToBackStack) {
ft.addToBackStack(null);
}
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
- private void onKeysSelected() {
+ public void onKeysSelected() {
mToolbar.setTitle(R.string.app_name);
mDrawer.setSelection(ID_KEYS, false);
Fragment frag = new KeyListFragment();
@@ -207,6 +244,18 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
setFragment(frag, true);
}
+ 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, true);
+ } else {
+ Fragment frag = new TransferFragment();
+ setFragment(frag, true);
+ }
+ }
+
@Override
protected void onSaveInstanceState(Bundle outState) {
// add the values which need to be saved from the drawer to the bundle
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java
index b811d51b5..26f5dbee7 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java
@@ -75,7 +75,8 @@ public class SettingsCacheTTLFragment extends Fragment {
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(mAdapter);
- recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST));
+ recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST,
+ true));
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java
index f7b08944a..670439481 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java
@@ -371,6 +371,13 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements
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;
@@ -405,6 +412,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
backupKey.setVisible(mIsSecret);
+ menu.findItem(R.id.menu_key_view_skt).setVisible(mIsSecret);
MenuItem changePassword = menu.findItem(R.id.menu_key_change_password);
changePassword.setVisible(mIsSecret);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java
index 0ddc6b160..b587eb9d3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java
@@ -46,7 +46,7 @@ public class IdentitiesCardView extends CardView implements IdentitiesMvpView {
vIdentities = (RecyclerView) view.findViewById(R.id.view_key_user_ids);
vIdentities.setLayoutManager(new LinearLayoutManager(context));
- vIdentities.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST));
+ vIdentities.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, false));
Button userIdsEditButton = (Button) view.findViewById(R.id.view_key_card_user_ids_edit);
userIdsEditButton.setOnClickListener(new OnClickListener() {
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
new file mode 100644
index 000000000..40e674a09
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java
@@ -0,0 +1,489 @@
+/*
+ * 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.presenter;
+
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+
+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.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+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.openintents.openpgp.util.OpenPgpUtils;
+import org.openintents.openpgp.util.OpenPgpUtils.UserId;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
+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.provider.KeyRepository;
+import org.sufficientlysecure.keychain.provider.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.transfer.loader.SecretKeyLoader.SecretKeyItem;
+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 org.sufficientlysecure.keychain.util.Log;
+
+
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks>,
+ 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 LoaderManager loaderManager;
+ private final int loaderId;
+ private final KeyRepository databaseInteractor;
+
+ private final TransferKeyAdapter secretKeyAdapter;
+ private final ReceivedKeyAdapter receivedKeyAdapter;
+
+
+ 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, LoaderManager loaderManager, int loaderId, TransferMvpView view) {
+ this.context = context;
+ this.view = view;
+ this.loaderManager = loaderManager;
+ this.loaderId = loaderId;
+ this.databaseInteractor = KeyRepository.createDatabaseInteractor(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() {
+ loaderManager.restartLoader(loaderId, null, this);
+
+ if (keyTransferServerInteractor == null && keyTransferClientInteractor == null && !wasConnected) {
+ checkWifiResetAndStartListen();
+ }
+ }
+
+ 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 op =
+ view.createCryptoOperationHelper(new Callback() {
+ @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) {
+ Log.d(Constants.TAG, "received data, but we already sent a key! race condition, or other side misbehaving?");
+ return;
+ }
+
+ Log.d(Constants.TAG, "received data");
+ UncachedKeyRing uncachedKeyRing;
+ try {
+ uncachedKeyRing = UncachedKeyRing.decodeFromData(receivedData.getBytes());
+ } catch (PgpGeneralException | IOException | RuntimeException e) {
+ Log.e(Constants.TAG, "error parsing incoming key", e);
+ 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) {
+ Log.d(Constants.TAG, "data sent ok!");
+ final long masterKeyId = Long.parseLong(passthrough);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ 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);
+ 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 = databaseInteractor.getSecretKeyRingAsArmoredData(masterKeyId);
+ secretKeyAdapter.focusItem(masterKeyId);
+ connectionSend(armoredSecretKey, Long.toString(masterKeyId));
+ } catch (IOException | NotFoundException | PgpGeneralException 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);
+ }
+ }
+
+
+ @Override
+ public Loader> onCreateLoader(int id, Bundle args) {
+ return secretKeyAdapter.createLoader(context);
+ }
+
+ @Override
+ public void onLoadFinished(Loader> loader, List data) {
+ secretKeyAdapter.setData(data);
+ view.setShowSecretKeyEmptyView(data.isEmpty());
+ }
+
+ @Override
+ public void onLoaderReset(Loader> loader) {
+ secretKeyAdapter.setData(null);
+ }
+
+
+ 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);
+
+ CryptoOperationHelper createCryptoOperationHelper(Callback callback);
+
+ void addFakeBackStackItem(String tag);
+
+ void finishFragmentOrActivity();
+
+ void showConfirmSendDialog();
+ void dismissConfirmationIfExists();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/ReceivedSecretKeyList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/ReceivedSecretKeyList.java
new file mode 100644
index 000000000..5ba1f3517
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/ReceivedSecretKeyList.java
@@ -0,0 +1,215 @@
+/*
+ * 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.view;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+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 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 {
+ private final Context context;
+ private final LayoutInflater layoutInflater;
+ private final OnClickImportKeyListener onClickImportKeyListener;
+
+ private Long focusedMasterKeyId;
+ private List data = new ArrayList<>();
+ private ArrayList 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 = (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);
+
+ vImportButton = itemView.findViewById(R.id.button_import);
+ vState = (ViewAnimator) 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;
+ }
+ }
+}
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
new file mode 100644
index 000000000..aa16ea353
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java
@@ -0,0 +1,440 @@
+/*
+ * 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.view;
+
+
+import android.app.Activity;
+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.Uri;
+import android.net.wifi.WifiManager;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AlertDialog.Builder;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.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.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 = (ToolableViewAnimator) view.findViewById(R.id.transfer_animator);
+
+ vConnectionStatusText1 = (TextView) view.findViewById(R.id.connection_status_1);
+ vConnectionStatusText2 = (TextView) view.findViewById(R.id.connection_status_2);
+ vConnectionStatusView1 = (ConnectionStatusView) view.findViewById(R.id.connection_status_icon_1);
+ vConnectionStatusView2 = (ConnectionStatusView) view.findViewById(R.id.connection_status_icon_2);
+ vTransferKeyList = (RecyclerView) view.findViewById(R.id.transfer_key_list);
+ vTransferKeyListEmptyView = view.findViewById(R.id.transfer_key_list_empty);
+ vReceivedKeyList = (RecyclerView) view.findViewById(R.id.received_key_list);
+ vWifiErrorInstructions = (TextView) view.findViewById(R.id.transfer_wifi_error_instructions);
+
+ vQrCodeImage = (ImageView) 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();
+ }
+ }
+ });
+
+ presenter = new TransferPresenter(getContext(), getLoaderManager(), LOADER_ID, 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 CryptoOperationHelper createCryptoOperationHelper(Callback callback) {
+ CryptoOperationHelper 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();
+ 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);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferNotAvailableFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferNotAvailableFragment.java
new file mode 100644
index 000000000..25e0058fd
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferNotAvailableFragment.java
@@ -0,0 +1,36 @@
+/*
+ * 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.view;
+
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.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);
+ }
+}
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..1f8407309
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java
@@ -0,0 +1,217 @@
+/*
+ * 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.view;
+
+
+import java.util.ArrayList;
+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 android.widget.ViewAnimator;
+
+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 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 {
+ private final Context context;
+ private final LayoutInflater layoutInflater;
+ private final OnClickTransferKeyListener onClickTransferKeyListener;
+
+ private Long focusedMasterKeyId;
+ private List data;
+ private ArrayList finishedItems = new ArrayList<>();
+ private boolean allItemsDisabled;
+
+
+ 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);
+ boolean isFinished = finishedItems.contains(item.masterKeyId);
+ 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).masterKeyId;
+ }
+
+ public void setData(List 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 Loader> createLoader(Context context) {
+ return new SecretKeyLoader(context, context.getContentResolver());
+ }
+
+ 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 = (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);
+ vState = (ViewAnimator) itemView.findViewById(R.id.transfer_state);
+ }
+
+ private void bind(Context context, final SecretKeyItem 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.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 (disableAll) {
+ itemView.setAlpha(0.2f);
+ vState.setDisplayedChild(STATE_INVISIBLE);
+ vSendButton.setOnClickListener(null);
+ return;
+ }
+
+ 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_BUTTON);
+ }
+
+ if (focusedMasterKeyId == null && onClickTransferKeyListener != null) {
+ vSendButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onClickTransferKeyListener.onUiClickTransferKey(item.masterKeyId);
+ }
+ });
+ } else {
+ vSendButton.setOnClickListener(null);
+ }
+ }
+ }
+
+ public interface OnClickTransferKeyListener {
+ void onUiClickTransferKey(long masterKeyId);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java
index a6394a3fb..dad6fe2c2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java
@@ -41,6 +41,10 @@ import java.util.Locale;
*/
public class QrCodeUtils {
+ public static Bitmap getQRCodeBitmap(final Uri uri) {
+ return getQRCodeBitmap(uri.toString(), 0);
+ }
+
public static Bitmap getQRCodeBitmap(final Uri uri, final int size) {
// for URIs we want alphanumeric encoding to save space, thus make everything upper case!
// zxing will then select Mode.ALPHANUMERIC internally
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java
index 2af4e062d..d31243c40 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java
@@ -34,12 +34,14 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
+ private final boolean showAfterLast;
private Drawable mDivider;
private int mOrientation;
- public DividerItemDecoration(Context context, int orientation) {
+ public DividerItemDecoration(Context context, int orientation, boolean showAfterLast) {
+ this.showAfterLast = showAfterLast;
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
@@ -66,8 +68,11 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
- final int childCount = parent.getChildCount();
- for (int i = 0; i < childCount -1; i++) {
+ int childCount = parent.getChildCount();
+ if (!showAfterLast) {
+ childCount -= 1;
+ }
+ for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ConnectionStatusView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ConnectionStatusView.java
new file mode 100644
index 000000000..9bceea5bf
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ConnectionStatusView.java
@@ -0,0 +1,186 @@
+/*
+ * 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.widget;
+
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.View;
+
+
+public class ConnectionStatusView extends View {
+ private static final int ARC_COUNT = 3;
+ public static final int COLOR_CONNECTED = 0xff394baf;
+ public static final int COLOR_DISCONNECTED = 0xffcccccc;
+
+
+ private Arc[] arcs;
+ private ValueAnimator[] animators;
+ private boolean isConnected = false;
+
+
+ public ConnectionStatusView(Context context) {
+ super(context);
+ }
+
+ public ConnectionStatusView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ConnectionStatusView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ initializeObjects();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int measuredWidth = resolveSize(150, widthMeasureSpec);
+ final int measuredHeight = resolveSize(150, heightMeasureSpec);
+
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ for (int i = 0; i < ARC_COUNT; i++) {
+ Arc arc = arcs[i];
+ canvas.drawArc(arc.oval, 225, 90, false, arc.paint);
+ }
+
+ if (isConnected != isAnimationInitiated()) {
+ resetAnimations();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ cancelAnimations();
+ }
+
+ public void setConnected(boolean isConnected) {
+ this.isConnected = isConnected;
+
+ if (arcs != null) {
+ for (int i = 0; i < ARC_COUNT; i++) {
+ arcs[i].paint.setColor(isConnected ? COLOR_CONNECTED : COLOR_DISCONNECTED);
+ }
+ }
+
+ invalidate();
+ }
+
+ private void resetAnimations() {
+ if (isConnected != isAnimationInitiated()) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ if (isConnected) {
+ setupAnimations();
+ } else {
+ cancelAnimations();
+ }
+ }
+ });
+ }
+ }
+
+ private boolean isAnimationInitiated() {
+ return animators != null;
+ }
+
+ private void setupAnimations() {
+ if (isAnimationInitiated()) {
+ return;
+ }
+
+ animators = new ValueAnimator[ARC_COUNT];
+ for (int i = 0; i < ARC_COUNT; i++) {
+ final int index = i;
+ ValueAnimator animator = ValueAnimator.ofInt(100, 255, 100);
+ animator.setRepeatCount(ValueAnimator.INFINITE);
+ animator.setDuration(2000);
+ animator.setStartDelay(i * 300);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ arcs[index].paint.setAlpha((int) animation.getAnimatedValue());
+ invalidate();
+ }
+ });
+ animator.start();
+
+ animators[i] = animator;
+ }
+ }
+
+ private void cancelAnimations() {
+ if (!isAnimationInitiated()) {
+ return;
+ }
+
+ for (int i = 0; i < ARC_COUNT; i++) {
+ animators[i].cancel();
+ }
+ animators = null;
+ }
+
+ private void initializeObjects() {
+ int width = getWidth();
+ int height = getHeight();
+ float centerX = width / 2.0f;
+ float centerY = height / 2.0f;
+ float r = Math.min(width, height) / 2f;
+
+ arcs = new Arc[ARC_COUNT];
+ for (int i = 0; i < ARC_COUNT; i++) {
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(r / 10f);
+ paint.setColor(isConnected ? COLOR_CONNECTED : COLOR_DISCONNECTED);
+
+ float d = r / 4 + i * r / 4;
+ RectF oval = new RectF(centerX - d, centerY - d + r / 3, centerX + d, centerY + d + r / 3);
+
+ arcs[i] = new Arc(paint, oval);
+ }
+ }
+
+ private static class Arc {
+ private final Paint paint;
+ private final RectF oval;
+
+ Arc(Paint paint, RectF oval) {
+ this.paint = paint;
+ this.oval = oval;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java
index a8274e45a..172009d24 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java
@@ -99,4 +99,21 @@ public class ToolableViewAnimator extends ViewAnimator {
setOutAnimation(savedOutAnim);
}
+ public void setDisplayedChildId(int id) {
+ if (getDisplayedChildId() == id) {
+ return;
+ }
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ if (getChildAt(i).getId() == id) {
+ setDisplayedChild(i);
+ return;
+ }
+ }
+ String name = getResources().getResourceEntryName(id);
+ throw new IllegalArgumentException("No view with ID " + name);
+ }
+
+ public int getDisplayedChildId() {
+ return getChildAt(getDisplayedChild()).getId();
+ }
}
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png
new file mode 100644
index 000000000..e5c94abed
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png
new file mode 100644
index 000000000..7bb1015d4
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png
new file mode 100644
index 000000000..efb0c3432
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png
new file mode 100644
index 000000000..3881c3f1b
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png
new file mode 100644
index 000000000..fae3d36c9
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_question_96dp.png
new file mode 100644
index 000000000..36dbfcfcc
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_question_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png
new file mode 100644
index 000000000..376485ca1
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png
new file mode 100644
index 000000000..1fe391018
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png
new file mode 100644
index 000000000..e352a5495
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png
new file mode 100644
index 000000000..5d4b657d3
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png
new file mode 100644
index 000000000..76a879e75
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_question_96dp.png
new file mode 100644
index 000000000..335547b9c
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_question_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png
new file mode 100644
index 000000000..d423e1ed1
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png
new file mode 100644
index 000000000..db50954cb
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png
new file mode 100644
index 000000000..eb7220029
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png
new file mode 100644
index 000000000..621506209
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png
new file mode 100644
index 000000000..bd0adcd05
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_question_96dp.png
new file mode 100644
index 000000000..7e200fdd9
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_question_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png
new file mode 100644
index 000000000..a7071a8aa
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png
new file mode 100644
index 000000000..ac256838a
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png
new file mode 100644
index 000000000..65fdc289d
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png
new file mode 100644
index 000000000..e80019793
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png
new file mode 100644
index 000000000..e4e09119f
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_question_96dp.png
new file mode 100644
index 000000000..5300ff766
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_question_96dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png
new file mode 100644
index 000000000..5d4b0f3fc
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png
new file mode 100644
index 000000000..43a1b4899
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png differ
diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png
new file mode 100644
index 000000000..447c5d155
Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png differ
diff --git a/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml b/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml
index e6e92c451..6b8d4a027 100644
--- a/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml
@@ -115,6 +115,24 @@
android:clickable="true"
style="?android:attr/borderlessButtonStyle" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenKeychain/src/main/res/layout/transfer_fragment.xml b/OpenKeychain/src/main/res/layout/transfer_fragment.xml
new file mode 100644
index 000000000..fc47c9ffe
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/transfer_fragment.xml
@@ -0,0 +1,374 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/transfer_not_available_fragment.xml b/OpenKeychain/src/main/res/layout/transfer_not_available_fragment.xml
new file mode 100644
index 000000000..ddac3005c
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/transfer_not_available_fragment.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/OpenKeychain/src/main/res/menu/key_view.xml b/OpenKeychain/src/main/res/menu/key_view.xml
index 0e5a43007..e16b3a152 100644
--- a/OpenKeychain/src/main/res/menu/key_view.xml
+++ b/OpenKeychain/src/main/res/menu/key_view.xml
@@ -18,6 +18,11 @@
android:title="@string/menu_export_key"
app:showAsAction="never" />
+
+
-
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index a070d764d..efa724e27 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -848,6 +848,7 @@
"Close navigation drawer"
"My Keys"
"Backup/Restore"
+ "Secure Wifi Transfer"
"Type text"
@@ -1888,4 +1889,34 @@
Forget
+ "Scan"
+ "More Info"
+ "To share or import your key setup, establish a secure connection by scanning with either device."
+ Use with your own devices only!
+ "Connection status:"
+ "Available keys for transfer"
+ "Received keys"
+ "This feature can only be used on Wifi."
+ "Import"
+ "Connected"
+ "Disconnected!"
+ "Failed reading incoming key!"
+ "Connection failed!"
+ "Error waiting for connection!"
+ "Unknown communication error!"
+ "Communication error: %s"
+ "Sorry, this feature can only be used on Android 5 or newer :("
+ "No keys on this device that could be sent. Waiting for incoming keys…"
+ "Connecting…"
+ "Secure Wifi Transfer"
+ "Secure Wifi Transfer"
+ "Cancel"
+ "Send"
+ "Send your key?"
+ "This will transfer full access to your key to the connected device. You should never send your own keys to devices you don't own!"
+ "Scan Again"
+ "Connection failed!"
+ "Make sure you are on the same network, then scan again."
+ "Make sure you are on the "%s" network, then scan again."
+
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/network/KeyTransferInteractorTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/network/KeyTransferInteractorTest.java
new file mode 100644
index 000000000..caaac6d2b
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/network/KeyTransferInteractorTest.java
@@ -0,0 +1,122 @@
+package org.sufficientlysecure.keychain.network;
+
+
+import java.net.URISyntaxException;
+
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.RequiresApi;
+
+import junit.framework.Assert;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowLooper;
+import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
+
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+
+@SuppressWarnings("WeakerAccess")
+// disabled, because we can't easily mock the tls-psk ciphersuite (it's removed in bouncycastle) :(
+//@RunWith(KeychainTestRunner.class)
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public class KeyTransferInteractorTest {
+ private static final String DELIM_START = "--";
+ private static final String DELIM_END = "--";
+
+ private String receivedQrCodeData;
+ private boolean clientConnectionEstablished;
+ private boolean serverConnectionEstablished;
+
+// @Before
+ public void setUp() throws Exception {
+ ShadowLog.stream = System.out;
+ }
+
+// @Test
+ public void testServerShouldGiveSuccessCallback() throws URISyntaxException {
+ KeyTransferInteractor serverKeyTransferInteractor = new KeyTransferInteractor(DELIM_START, DELIM_END);
+
+ serverKeyTransferInteractor.startServer(new SimpleKeyTransferCallback() {
+ @Override
+ public void onServerStarted(String qrCodeData) {
+ receivedQrCodeData = qrCodeData;
+ }
+
+ @Override
+ public void onConnectionEstablished(String otherName) {
+ serverConnectionEstablished = true;
+ }
+ }, null);
+ waitForLooperCallback();
+ Assert.assertNotNull(receivedQrCodeData);
+
+ final KeyTransferInteractor clientKeyTransferInteractor = new KeyTransferInteractor(DELIM_START, DELIM_END);
+ clientKeyTransferInteractor.connectToServer(receivedQrCodeData, new SimpleKeyTransferCallback() {
+ @Override
+ public void onConnectionEstablished(String otherName) {
+ clientConnectionEstablished = true;
+ }
+ });
+ waitForLooperCallback();
+ waitForLooperCallback();
+
+ assertTrue(clientConnectionEstablished);
+ assertTrue(serverConnectionEstablished);
+
+ serverKeyTransferInteractor.sendData(new byte[] { (byte) 1, (byte) 2 }, "passthrough");
+ waitForLooperCallback();
+ }
+
+ private void waitForLooperCallback() {
+ while (!ShadowLooper.getShadowMainLooper().getScheduler().runOneTask());
+ }
+
+
+ static class SimpleKeyTransferCallback implements KeyTransferCallback {
+ @Override
+ public void onServerStarted(String qrCodeData) {
+ fail("unexpected callback: onServerStarted");
+ }
+
+ @Override
+ public void onConnectionEstablished(String otherName) {
+ fail("unexpected callback: onConnectionEstablished");
+ }
+
+ @Override
+ public void onConnectionLost() {
+ fail("unexpected callback: onConnectionLost");
+ }
+
+ @Override
+ public void onDataReceivedOk(String receivedData) {
+ fail("unexpected callback: onDataReceivedOk");
+ }
+
+ @Override
+ public void onDataSentOk(String passthrough) {
+ fail("unexpected callback: onDataSentOk");
+ }
+
+ @Override
+ public void onConnectionErrorNoRouteToHost(String wifiSsid) {
+ fail("unexpected callback: onConnectionErrorNoRouteToHost");
+ }
+
+ @Override
+ public void onConnectionErrorConnect() {
+ fail("unexpected callback: onConnectionErrorConnect");
+ }
+
+ @Override
+ public void onConnectionErrorListen() {
+ fail("unexpected callback: onConnectionErrorListen");
+ }
+
+ @Override
+ public void onConnectionError(String arg) {
+ fail("unexpected callback: onConnectionError");
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/network/SktUriTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/network/SktUriTest.java
new file mode 100644
index 000000000..8e15691c5
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/network/SktUriTest.java
@@ -0,0 +1,101 @@
+package org.sufficientlysecure.keychain.network;
+
+
+import java.net.URISyntaxException;
+
+import android.annotation.SuppressLint;
+
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+
+@SuppressWarnings("WeakerAccess")
+@SuppressLint("DefaultLocale")
+public class SktUriTest {
+ static final String HOST = "127.0.0.1";
+ static final int PORT = 1234;
+ static final byte[] PRESHARED_KEY = { 1, 2 };
+ static final String SSID = "ssid";
+
+ static final String ENCODED_SKT = String.format("OPGPSKT:%s/%d/%s/SSID:%s",
+ HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes()));
+
+ @Test
+ public void testCreate() {
+ SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, null);
+
+ assertEquals(HOST, sktUri.getHost());
+ assertEquals(PORT, sktUri.getPort());
+ assertArrayEquals(PRESHARED_KEY, sktUri.getPresharedKey());
+ assertEquals(null, sktUri.getWifiSsid());
+ }
+
+ @Test
+ public void testCreateWithSsid() {
+ SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, SSID);
+
+ assertEquals(HOST, sktUri.getHost());
+ assertEquals(PORT, sktUri.getPort());
+ assertArrayEquals(PRESHARED_KEY, sktUri.getPresharedKey());
+ assertEquals(SSID, sktUri.getWifiSsid());
+ }
+
+ @Test
+ public void testCreate_isAllUppercase() {
+ SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, SSID);
+
+ String encodedSktUri = sktUri.toUriString();
+ assertEquals(encodedSktUri.toUpperCase(), encodedSktUri);
+ }
+
+ @Test
+ public void testParse() throws URISyntaxException {
+ SktUri sktUri = SktUri.parse(ENCODED_SKT);
+
+ assertNotNull(sktUri);
+ assertEquals(HOST, sktUri.getHost());
+ assertEquals(PORT, sktUri.getPort());
+ assertArrayEquals(PRESHARED_KEY, sktUri.getPresharedKey());
+ assertEquals(SSID, sktUri.getWifiSsid());
+ }
+
+ @Test
+ public void testBackAndForth() throws URISyntaxException {
+ SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, null);
+ String encodedSktUri = sktUri.toUriString();
+ SktUri decodedSktUri = SktUri.parse(encodedSktUri);
+
+ assertEquals(sktUri, decodedSktUri);
+ }
+
+ @Test
+ public void testBackAndForthWithSsid() throws URISyntaxException {
+ SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, SSID);
+ String encodedSktUri = sktUri.toUriString();
+ SktUri decodedSktUri = SktUri.parse(encodedSktUri);
+
+ assertEquals(sktUri, decodedSktUri);
+ }
+
+ @Test(expected = URISyntaxException.class)
+ public void testParse_withBadScheme_shouldFail() throws URISyntaxException {
+ SktUri.parse(String.format("XXXGPSKT:%s/%d/%s/SSID:%s",
+ HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes())));
+ }
+
+ @Test(expected = URISyntaxException.class)
+ public void testParse_withBadPsk_shouldFail() throws URISyntaxException {
+ SktUri.parse(String.format("OPGPSKT:%s/%d/xx%s/SSID:%s",
+ HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes())));
+ }
+
+ @Test(expected = URISyntaxException.class)
+ public void testParse_withBadPort_shouldFail() throws URISyntaxException {
+ SktUri.parse(String.format("OPGPSKT:%s/x%d/%s/SSID:%s",
+ HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes())));
+ }
+}
\ No newline at end of file
diff --git a/graphics/drawables/ic_android.svg b/graphics/drawables/ic_android.svg
new file mode 100644
index 000000000..b3ee9b114
--- /dev/null
+++ b/graphics/drawables/ic_android.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/graphics/drawables/ic_live_help.svg b/graphics/drawables/ic_live_help.svg
new file mode 100644
index 000000000..5765a9dd6
--- /dev/null
+++ b/graphics/drawables/ic_live_help.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/graphics/drawables/ic_send.svg b/graphics/drawables/ic_send.svg
new file mode 100644
index 000000000..c43d9b45e
--- /dev/null
+++ b/graphics/drawables/ic_send.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/graphics/drawables/ic_wifi_lock.svg b/graphics/drawables/ic_wifi_lock.svg
new file mode 100644
index 000000000..b06541558
--- /dev/null
+++ b/graphics/drawables/ic_wifi_lock.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/graphics/drawables/ic_wifi_off.svg b/graphics/drawables/ic_wifi_off.svg
new file mode 100644
index 000000000..b4332030b
--- /dev/null
+++ b/graphics/drawables/ic_wifi_off.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/graphics/drawables/ic_wifi_question.svg b/graphics/drawables/ic_wifi_question.svg
new file mode 100644
index 000000000..c9678516b
--- /dev/null
+++ b/graphics/drawables/ic_wifi_question.svg
@@ -0,0 +1,80 @@
+
+
diff --git a/graphics/update-drawables.sh b/graphics/update-drawables.sh
index f4c998e4b..83b4bad47 100755
--- a/graphics/update-drawables.sh
+++ b/graphics/update-drawables.sh
@@ -22,7 +22,7 @@ SRC_DIR=./drawables/
#inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg
-for NAME in "ic_cloud_unknown" "ic_cloud_off" "broken_heart" "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_paste" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link"
+for NAME in "ic_live_help" "ic_send" "ic_cloud_unknown" "ic_cloud_off" "ic_wifi_lock" "broken_heart" "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_paste" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link"
do
echo $NAME
inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
@@ -32,7 +32,7 @@ inkscape -w 72 -h 72 -e "$XXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
inkscape -w 96 -h 96 -e "$XXXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
done
-for NAME in "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "status_signature_verified_inner"
+for NAME in "ic_android" "ic_wifi_question" "ic_wifi_off" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "status_signature_verified_inner"
do
echo $NAME
inkscape -w 96 -h 96 -e "$MDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"