Merge pull request #2117 from open-keychain/tls-psk-transfer
Key transfer using TLS-PSK
@@ -82,6 +82,7 @@
|
|||||||
<!-- other group (for free) -->
|
<!-- other group (for free) -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ public final class Constants {
|
|||||||
// used by QR Codes (Guardian Project, Monkeysphere compatibility)
|
// used by QR Codes (Guardian Project, Monkeysphere compatibility)
|
||||||
public static final String FINGERPRINT_SCHEME = "openpgp4fpr";
|
public static final String FINGERPRINT_SCHEME = "openpgp4fpr";
|
||||||
|
|
||||||
|
// used by openpgp-skt
|
||||||
|
public static final String SKT_SCHEME = "OPGPSKT";
|
||||||
|
|
||||||
public static final String BOUNCY_CASTLE_PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME;
|
public static final String BOUNCY_CASTLE_PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME;
|
||||||
|
|
||||||
// prefix packagename for exported Intents
|
// prefix packagename for exported Intents
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.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
|
||||||
|
* <p>
|
||||||
|
* 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<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||||
|
for (NetworkInterface intf : interfaces) {
|
||||||
|
List<InetAddress> 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<String> s1 = new HashSet<>(Arrays.asList(array1));
|
||||||
|
Set<String> s2 = new HashSet<>(Arrays.asList(array2));
|
||||||
|
s1.retainAll(s2);
|
||||||
|
|
||||||
|
return s1.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ package org.sufficientlysecure.keychain.operations;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorCompletionService;
|
import java.util.concurrent.ExecutorCompletionService;
|
||||||
@@ -477,7 +478,7 @@ public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel>
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
|
public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
|
||||||
ArrayList<ParcelableKeyRing> keyList = importInput.getKeyList();
|
List<ParcelableKeyRing> keyList = importInput.getKeyList();
|
||||||
HkpKeyserverAddress keyServer = importInput.getKeyserver();
|
HkpKeyserverAddress keyServer = importInput.getKeyserver();
|
||||||
boolean skipSave = importInput.isSkipSave();
|
boolean skipSave = importInput.isSkipSave();
|
||||||
|
|
||||||
@@ -510,7 +511,7 @@ public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private ImportKeyResult multiThreadedKeyImport(ArrayList<ParcelableKeyRing> keyList,
|
private ImportKeyResult multiThreadedKeyImport(List<ParcelableKeyRing> keyList,
|
||||||
final HkpKeyserverAddress keyServer, final ParcelableProxy proxy,
|
final HkpKeyserverAddress keyServer, final ParcelableProxy proxy,
|
||||||
final boolean skipSave) {
|
final boolean skipSave) {
|
||||||
Log.d(Constants.TAG, "Multi-threaded key import starting");
|
Log.d(Constants.TAG, "Multi-threaded key import starting");
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ public class UncachedKeyRing {
|
|||||||
return mRing.getPublicKey().getKeyID();
|
return mRing.getPublicKey().getKeyID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getCreationTime() {
|
||||||
|
return mRing.getPublicKey().getCreationTime().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
public UncachedPublicKey getPublicKey() {
|
public UncachedPublicKey getPublicKey() {
|
||||||
return new UncachedPublicKey(mRing.getPublicKey());
|
return new UncachedPublicKey(mRing.getPublicKey());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import android.net.Uri;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
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.PgpGeneralException;
|
||||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||||
@@ -236,19 +236,27 @@ public class KeyRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getKeyRingAsArmoredString(byte[] data) throws IOException, PgpGeneralException {
|
private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException, PgpGeneralException {
|
||||||
UncachedKeyRing keyRing = UncachedKeyRing.decodeFromData(data);
|
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
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)
|
public String getPublicKeyRingAsArmoredString(long masterKeyId)
|
||||||
throws NotFoundException, IOException, PgpGeneralException {
|
throws NotFoundException, IOException, PgpGeneralException {
|
||||||
byte[] data = loadPublicKeyRingData(masterKeyId);
|
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() {
|
public ContentResolver getContentResolver() {
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ public class RemoteDeduplicateActivity extends FragmentActivity {
|
|||||||
|
|
||||||
keyChoiceList = (RecyclerView) view.findViewById(R.id.duplicate_key_list);
|
keyChoiceList = (RecyclerView) view.findViewById(R.id.duplicate_key_list);
|
||||||
keyChoiceList.setLayoutManager(new LinearLayoutManager(activity));
|
keyChoiceList.setLayoutManager(new LinearLayoutManager(activity));
|
||||||
keyChoiceList.addItemDecoration(new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST));
|
keyChoiceList.addItemDecoration(
|
||||||
|
new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true));
|
||||||
|
|
||||||
setupListenersForPresenter();
|
setupListenersForPresenter();
|
||||||
mvpView = createMvpView(view, layoutInflater);
|
mvpView = createMvpView(view, layoutInflater);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ package org.sufficientlysecure.keychain.service;
|
|||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
@@ -31,21 +33,25 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
|||||||
@AutoValue
|
@AutoValue
|
||||||
public abstract class ImportKeyringParcel implements Parcelable {
|
public abstract class ImportKeyringParcel implements Parcelable {
|
||||||
@Nullable // If null, keys are expected to be read from a cache file in ImportExportOperations
|
@Nullable // If null, keys are expected to be read from a cache file in ImportExportOperations
|
||||||
public abstract ArrayList<ParcelableKeyRing> getKeyList();
|
public abstract List<ParcelableKeyRing> getKeyList();
|
||||||
@Nullable // must be set if keys are to be imported from a keyserver
|
@Nullable // must be set if keys are to be imported from a keyserver
|
||||||
public abstract HkpKeyserverAddress getKeyserver();
|
public abstract HkpKeyserverAddress getKeyserver();
|
||||||
public abstract boolean isSkipSave();
|
public abstract boolean isSkipSave();
|
||||||
|
|
||||||
public static ImportKeyringParcel createImportKeyringParcel(ArrayList<ParcelableKeyRing> keyList,
|
public static ImportKeyringParcel createImportKeyringParcel(List<ParcelableKeyRing> keyList,
|
||||||
HkpKeyserverAddress keyserver) {
|
HkpKeyserverAddress keyserver) {
|
||||||
return new AutoValue_ImportKeyringParcel(keyList, keyserver, false);
|
return new AutoValue_ImportKeyringParcel(keyList, keyserver, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ImportKeyringParcel createWithSkipSave(ArrayList<ParcelableKeyRing> keyList,
|
public static ImportKeyringParcel createWithSkipSave(List<ParcelableKeyRing> keyList,
|
||||||
HkpKeyserverAddress keyserver) {
|
HkpKeyserverAddress keyserver) {
|
||||||
return new AutoValue_ImportKeyringParcel(keyList, keyserver, true);
|
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() {
|
public static ImportKeyringParcel createFromFileCacheWithSkipSave() {
|
||||||
return new AutoValue_ImportKeyringParcel(null, null, true);
|
return new AutoValue_ImportKeyringParcel(null, null, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,23 +17,31 @@
|
|||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui;
|
package org.sufficientlysecure.keychain.ui;
|
||||||
|
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
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.Log;
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
import org.sufficientlysecure.keychain.util.Preferences;
|
||||||
|
|
||||||
public class CreateKeyStartFragment extends Fragment {
|
public class CreateKeyStartFragment extends Fragment {
|
||||||
|
public static final int REQUEST_CODE_IMPORT_KEY = 0x00007012;
|
||||||
|
|
||||||
|
|
||||||
CreateKeyActivity mCreateKeyActivity;
|
CreateKeyActivity mCreateKeyActivity;
|
||||||
|
|
||||||
@@ -41,7 +49,8 @@ public class CreateKeyStartFragment extends Fragment {
|
|||||||
View mImportKey;
|
View mImportKey;
|
||||||
View mSecurityToken;
|
View mSecurityToken;
|
||||||
TextView mSkipOrCancel;
|
TextView mSkipOrCancel;
|
||||||
public static final int REQUEST_CODE_IMPORT_KEY = 0x00007012;
|
View mSecureDeviceSetup;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new instance of this fragment
|
* Creates new instance of this fragment
|
||||||
@@ -64,6 +73,7 @@ public class CreateKeyStartFragment extends Fragment {
|
|||||||
mImportKey = view.findViewById(R.id.create_key_import_button);
|
mImportKey = view.findViewById(R.id.create_key_import_button);
|
||||||
mSecurityToken = view.findViewById(R.id.create_key_security_token_button);
|
mSecurityToken = view.findViewById(R.id.create_key_security_token_button);
|
||||||
mSkipOrCancel = (TextView) view.findViewById(R.id.create_key_cancel);
|
mSkipOrCancel = (TextView) view.findViewById(R.id.create_key_cancel);
|
||||||
|
mSecureDeviceSetup = view.findViewById(R.id.create_key_secure_device_setup);
|
||||||
|
|
||||||
if (mCreateKeyActivity.mFirstTime) {
|
if (mCreateKeyActivity.mFirstTime) {
|
||||||
mSkipOrCancel.setText(R.string.first_time_skip);
|
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() {
|
mSkipOrCancel.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
|
|||||||
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
|
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
|
||||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
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.ui.util.KeyFormattingUtils;
|
||||||
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
|
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
|
||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
@@ -143,6 +144,17 @@ public class ImportKeysProxyActivity extends FragmentActivity
|
|||||||
|
|
||||||
Log.d(Constants.TAG, "scanned: " + uri);
|
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
|
// example: openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282
|
||||||
if (uri == null || uri.getScheme() == null ||
|
if (uri == null || uri.getScheme() == null ||
|
||||||
!uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {
|
!uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {
|
||||||
@@ -153,6 +165,7 @@ public class ImportKeysProxyActivity extends FragmentActivity
|
|||||||
returnResult(intent);
|
returnResult(intent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String fingerprintHex = uri.getEncodedSchemeSpecificPart().toLowerCase(Locale.ENGLISH);
|
final String fingerprintHex = uri.getEncodedSchemeSpecificPart().toLowerCase(Locale.ENGLISH);
|
||||||
if (!fingerprintHex.matches("[a-fA-F0-9]{40}")) {
|
if (!fingerprintHex.matches("[a-fA-F0-9]{40}")) {
|
||||||
SingletonResult result = new SingletonResult(
|
SingletonResult result = new SingletonResult(
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
package org.sufficientlysecure.keychain.ui;
|
package org.sufficientlysecure.keychain.ui;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentManager;
|
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.operations.results.OperationResult;
|
||||||
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
|
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
|
||||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
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.FabContainer;
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
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_ENCRYPT_DECRYPT = 2;
|
||||||
static final int ID_APPS = 3;
|
static final int ID_APPS = 3;
|
||||||
static final int ID_BACKUP = 4;
|
static final int ID_BACKUP = 4;
|
||||||
static final int ID_SETTINGS = 5;
|
public static final int ID_TRANSFER = 5;
|
||||||
static final int ID_HELP = 6;
|
static final int ID_SETTINGS = 6;
|
||||||
|
static final int ID_HELP = 7;
|
||||||
|
|
||||||
// both of these are used for instrumentation testing only
|
// both of these are used for instrumentation testing only
|
||||||
public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time";
|
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),
|
.withIdentifier(ID_APPS).withSelectable(false),
|
||||||
new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore)
|
new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore)
|
||||||
.withIdentifier(ID_BACKUP).withSelectable(false),
|
.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 DividerDrawerItem(),
|
||||||
new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withSelectable(false),
|
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)
|
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:
|
case ID_BACKUP:
|
||||||
onBackupSelected();
|
onBackupSelected();
|
||||||
break;
|
break;
|
||||||
|
case ID_TRANSFER:
|
||||||
|
onTransferSelected();
|
||||||
|
break;
|
||||||
case ID_SETTINGS:
|
case ID_SETTINGS:
|
||||||
intent = new Intent(MainActivity.this, SettingsActivity.class);
|
intent = new Intent(MainActivity.this, SettingsActivity.class);
|
||||||
break;
|
break;
|
||||||
@@ -160,13 +173,36 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
case ID_APPS:
|
case ID_APPS:
|
||||||
onAppsSelected();
|
onAppsSelected();
|
||||||
break;
|
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 fragmentManager = getSupportFragmentManager();
|
||||||
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||||
|
|
||||||
@@ -175,11 +211,12 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
if (addToBackStack) {
|
if (addToBackStack) {
|
||||||
ft.addToBackStack(null);
|
ft.addToBackStack(null);
|
||||||
}
|
}
|
||||||
|
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
|
||||||
ft.commit();
|
ft.commit();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onKeysSelected() {
|
public void onKeysSelected() {
|
||||||
mToolbar.setTitle(R.string.app_name);
|
mToolbar.setTitle(R.string.app_name);
|
||||||
mDrawer.setSelection(ID_KEYS, false);
|
mDrawer.setSelection(ID_KEYS, false);
|
||||||
Fragment frag = new KeyListFragment();
|
Fragment frag = new KeyListFragment();
|
||||||
@@ -207,6 +244,18 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
setFragment(frag, true);
|
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
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
// add the values which need to be saved from the drawer to the bundle
|
// add the values which need to be saved from the drawer to the bundle
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ public class SettingsCacheTTLFragment extends Fragment {
|
|||||||
recyclerView.setHasFixedSize(true);
|
recyclerView.setHasFixedSize(true);
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||||
recyclerView.setAdapter(mAdapter);
|
recyclerView.setAdapter(mAdapter);
|
||||||
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST));
|
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST,
|
||||||
|
true));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,13 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements
|
|||||||
startPassphraseActivity(REQUEST_BACKUP);
|
startPassphraseActivity(REQUEST_BACKUP);
|
||||||
return true;
|
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: {
|
case R.id.menu_key_view_delete: {
|
||||||
deleteKey();
|
deleteKey();
|
||||||
return true;
|
return true;
|
||||||
@@ -405,6 +412,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements
|
|||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
|
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
|
||||||
backupKey.setVisible(mIsSecret);
|
backupKey.setVisible(mIsSecret);
|
||||||
|
menu.findItem(R.id.menu_key_view_skt).setVisible(mIsSecret);
|
||||||
MenuItem changePassword = menu.findItem(R.id.menu_key_change_password);
|
MenuItem changePassword = menu.findItem(R.id.menu_key_change_password);
|
||||||
changePassword.setVisible(mIsSecret);
|
changePassword.setVisible(mIsSecret);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class IdentitiesCardView extends CardView implements IdentitiesMvpView {
|
|||||||
|
|
||||||
vIdentities = (RecyclerView) view.findViewById(R.id.view_key_user_ids);
|
vIdentities = (RecyclerView) view.findViewById(R.id.view_key_user_ids);
|
||||||
vIdentities.setLayoutManager(new LinearLayoutManager(context));
|
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);
|
Button userIdsEditButton = (Button) view.findViewById(R.id.view_key_card_user_ids_edit);
|
||||||
userIdsEditButton.setOnClickListener(new OnClickListener() {
|
userIdsEditButton.setOnClickListener(new OnClickListener() {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.transfer.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<List<SecretKeyItem>> {
|
||||||
|
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<SecretKeyItem> cachedResult;
|
||||||
|
|
||||||
|
|
||||||
|
public SecretKeyLoader(Context context, ContentResolver contentResolver) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
this.contentResolver = contentResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SecretKeyItem> 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<SecretKeyItem> 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<SecretKeyItem> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.transfer.presenter;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import 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<List<SecretKeyItem>>,
|
||||||
|
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<ImportKeyringParcel,ImportKeyResult> op =
|
||||||
|
view.createCryptoOperationHelper(new Callback<ImportKeyringParcel,ImportKeyResult>() {
|
||||||
|
@Override
|
||||||
|
public ImportKeyringParcel createOperationInput() {
|
||||||
|
return importKeyringParcel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCryptoOperationSuccess(ImportKeyResult result) {
|
||||||
|
receivedKeyAdapter.focusItem(null);
|
||||||
|
receivedKeyAdapter.addToFinishedItems(masterKeyId);
|
||||||
|
view.releaseCryptoOperationHelper();
|
||||||
|
view.showResultNotification(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCryptoOperationCancelled() {
|
||||||
|
view.releaseCryptoOperationHelper();
|
||||||
|
receivedKeyAdapter.focusItem(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCryptoOperationError(ImportKeyResult result) {
|
||||||
|
receivedKeyAdapter.focusItem(null);
|
||||||
|
view.releaseCryptoOperationHelper();
|
||||||
|
view.showResultNotification(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCryptoSetProgress(String msg, int progress, int max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
op.cryptoOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onWifiConnected() {
|
||||||
|
if (waitingForWifi) {
|
||||||
|
resetAndStartListen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServerStarted(String qrCodeData) {
|
||||||
|
Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse(qrCodeData));
|
||||||
|
view.setQrImage(qrCodeBitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionEstablished(String otherName) {
|
||||||
|
wasConnected = true;
|
||||||
|
|
||||||
|
secretKeyAdapter.clearFinishedItems();
|
||||||
|
secretKeyAdapter.focusItem(null);
|
||||||
|
secretKeyAdapter.setAllDisabled(false);
|
||||||
|
receivedKeyAdapter.clear();
|
||||||
|
|
||||||
|
view.showConnectionEstablished(otherName);
|
||||||
|
view.setShowDoneIcon(true);
|
||||||
|
view.addFakeBackStackItem(BACKSTACK_TAG_TRANSFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionLost() {
|
||||||
|
if (!wasConnected) {
|
||||||
|
checkWifiResetAndStartListen();
|
||||||
|
|
||||||
|
view.showErrorConnectionFailed();
|
||||||
|
} else {
|
||||||
|
connectionClear();
|
||||||
|
|
||||||
|
view.dismissConfirmationIfExists();
|
||||||
|
view.showViewDisconnected();
|
||||||
|
secretKeyAdapter.setAllDisabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDataReceivedOk(String receivedData) {
|
||||||
|
if (sentData) {
|
||||||
|
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<List<SecretKeyItem>> onCreateLoader(int id, Bundle args) {
|
||||||
|
return secretKeyAdapter.createLoader(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<List<SecretKeyItem>> loader, List<SecretKeyItem> data) {
|
||||||
|
secretKeyAdapter.setData(data);
|
||||||
|
view.setShowSecretKeyEmptyView(data.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaderReset(Loader<List<SecretKeyItem>> 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);
|
||||||
|
|
||||||
|
<T extends Parcelable, S extends OperationResult> CryptoOperationHelper<T,S> createCryptoOperationHelper(Callback<T, S> callback);
|
||||||
|
|
||||||
|
void addFakeBackStackItem(String tag);
|
||||||
|
|
||||||
|
void finishFragmentOrActivity();
|
||||||
|
|
||||||
|
void showConfirmSendDialog();
|
||||||
|
void dismissConfirmationIfExists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <look@my.amazin.horse>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.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<ReceivedKeyViewHolder> {
|
||||||
|
private final Context context;
|
||||||
|
private final LayoutInflater layoutInflater;
|
||||||
|
private final OnClickImportKeyListener onClickImportKeyListener;
|
||||||
|
|
||||||
|
private Long focusedMasterKeyId;
|
||||||
|
private List<ReceivedKeyItem> data = new ArrayList<>();
|
||||||
|
private ArrayList<Long> finishedItems = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
public ReceivedKeyAdapter(Context context, LayoutInflater layoutInflater,
|
||||||
|
OnClickImportKeyListener onClickImportKeyListener) {
|
||||||
|
this.context = context;
|
||||||
|
this.layoutInflater = layoutInflater;
|
||||||
|
this.onClickImportKeyListener = onClickImportKeyListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReceivedKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
return new ReceivedKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ReceivedKeyViewHolder holder, int position) {
|
||||||
|
ReceivedKeyItem item = data.get(position);
|
||||||
|
boolean isFinished = finishedItems.contains(item.masterKeyId);
|
||||||
|
holder.bind(context, item, onClickImportKeyListener, focusedMasterKeyId, isFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return data != null ? data.size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return data.get(position).masterKeyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addToFinishedItems(long masterKeyId) {
|
||||||
|
finishedItems.add(masterKeyId);
|
||||||
|
// doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things!
|
||||||
|
}
|
||||||
|
|
||||||
|
public void focusItem(Long masterKeyId) {
|
||||||
|
focusedMasterKeyId = masterKeyId;
|
||||||
|
notifyItemRangeChanged(0, getItemCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addItem(ReceivedKeyItem receivedKeyItem) {
|
||||||
|
data.add(receivedKeyItem);
|
||||||
|
notifyItemInserted(data.size() -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
data.clear();
|
||||||
|
finishedItems.clear();
|
||||||
|
focusedMasterKeyId = null;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ReceivedKeyViewHolder extends ViewHolder {
|
||||||
|
private final TextView vName;
|
||||||
|
private final TextView vEmail;
|
||||||
|
private final TextView vCreation;
|
||||||
|
private final View vImportButton;
|
||||||
|
private final ViewAnimator vState;
|
||||||
|
|
||||||
|
ReceivedKeyViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
vName = (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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <look@my.amazin.horse>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.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.<Uri>getParcelableExtra(EXTRA_OPENPGP_SKT_INFO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
|
||||||
|
presenter.onUiStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
IntentFilter intentFilter = new IntentFilter();
|
||||||
|
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
|
||||||
|
getContext().registerReceiver(broadcastReceiver, intentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
getContext().unregisterReceiver(broadcastReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
presenter.onUiStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
if (showDoneIcon) {
|
||||||
|
inflater.inflate(R.menu.transfer_menu, menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.menu_done) {
|
||||||
|
presenter.onUiClickDone();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showNotOnWifi() {
|
||||||
|
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_no_wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showWaitingForConnection() {
|
||||||
|
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_waiting);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showEstablishingConnection() {
|
||||||
|
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connecting);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showConnectionEstablished(String hostname) {
|
||||||
|
// String statusText = getString(R.string.transfer_status_connected, hostname);
|
||||||
|
|
||||||
|
vConnectionStatusText1.setText(R.string.transfer_status_connected);
|
||||||
|
vConnectionStatusText2.setText(R.string.transfer_status_connected);
|
||||||
|
|
||||||
|
vConnectionStatusView1.setConnected(true);
|
||||||
|
vConnectionStatusView2.setConnected(true);
|
||||||
|
|
||||||
|
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showWifiError(String wifiSsid) {
|
||||||
|
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_wifi_error);
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(wifiSsid)) {
|
||||||
|
vWifiErrorInstructions
|
||||||
|
.setText(getResources().getString(R.string.transfer_error_wifi_text_instructions_ssid, wifiSsid));
|
||||||
|
} else {
|
||||||
|
vWifiErrorInstructions.setText(R.string.transfer_error_wifi_text_instructions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showReceivingKeys() {
|
||||||
|
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_passive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showViewDisconnected() {
|
||||||
|
vConnectionStatusText1.setText(R.string.transfer_status_disconnected);
|
||||||
|
vConnectionStatusText2.setText(R.string.transfer_status_disconnected);
|
||||||
|
|
||||||
|
vConnectionStatusView1.setConnected(false);
|
||||||
|
vConnectionStatusView2.setConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setQrImage(final Bitmap qrCode) {
|
||||||
|
vQrCodeImage.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||||
|
new OnGlobalLayoutListener() {
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
int viewSize = vQrCodeImage.getWidth();
|
||||||
|
if (viewSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create actual bitmap in display dimensions
|
||||||
|
Bitmap scaled = Bitmap.createScaledBitmap(qrCode, viewSize, viewSize, false);
|
||||||
|
vQrCodeImage.setImageBitmap(scaled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vQrCodeImage.requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scanQrCode() {
|
||||||
|
Intent intent = new Intent(getActivity(), QrCodeCaptureActivity.class);
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_SCAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setShowDoneIcon(boolean showDoneIcon) {
|
||||||
|
this.showDoneIcon = showDoneIcon;
|
||||||
|
FragmentActivity activity = getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
activity.invalidateOptionsMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSecretKeyAdapter(Adapter adapter) {
|
||||||
|
vTransferKeyList.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setShowSecretKeyEmptyView(boolean isEmpty) {
|
||||||
|
vTransferKeyListEmptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReceivedKeyAdapter(Adapter adapter) {
|
||||||
|
vReceivedKeyList.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Parcelable, S extends OperationResult> CryptoOperationHelper<T,S> createCryptoOperationHelper(Callback<T, S> callback) {
|
||||||
|
CryptoOperationHelper<T,S> cryptoOperationHelper = new CryptoOperationHelper<>(1, this, callback, null);
|
||||||
|
currentCryptoOperationHelper = cryptoOperationHelper;
|
||||||
|
return cryptoOperationHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseCryptoOperationHelper() {
|
||||||
|
currentCryptoOperationHelper = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showErrorBadKey() {
|
||||||
|
Notify.create(getActivity(), R.string.transfer_error_read_incoming, Style.ERROR).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showErrorConnectionFailed() {
|
||||||
|
Notify.create(getActivity(), R.string.transfer_error_connect, Style.ERROR).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showErrorListenFailed() {
|
||||||
|
Notify.create(getActivity(), R.string.transfer_error_listen, Style.ERROR).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showErrorConnectionError(String errorMessage) {
|
||||||
|
if (errorMessage != null) {
|
||||||
|
String text = getString(R.string.transfer_error_generic_msg, errorMessage);
|
||||||
|
Notify.create(getActivity(), text, Style.ERROR).show();
|
||||||
|
} else {
|
||||||
|
Notify.create(getActivity(), R.string.transfer_error_generic, Style.ERROR).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showResultNotification(ImportKeyResult result) {
|
||||||
|
result.createNotify(getActivity()).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addFakeBackStackItem(final String tag) {
|
||||||
|
FragmentManager fragmentManager = getFragmentManager();
|
||||||
|
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.addToBackStack(tag)
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
fragmentManager.executePendingTransactions();
|
||||||
|
|
||||||
|
fragmentManager.addOnBackStackChangedListener(new OnBackStackChangedListener() {
|
||||||
|
@Override
|
||||||
|
public void onBackStackChanged() {
|
||||||
|
FragmentManager fragMan = getFragmentManager();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <look@my.amazin.horse>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <look@my.amazin.horse>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.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<TransferKeyViewHolder> {
|
||||||
|
private final Context context;
|
||||||
|
private final LayoutInflater layoutInflater;
|
||||||
|
private final OnClickTransferKeyListener onClickTransferKeyListener;
|
||||||
|
|
||||||
|
private Long focusedMasterKeyId;
|
||||||
|
private List<SecretKeyItem> data;
|
||||||
|
private ArrayList<Long> finishedItems = new ArrayList<>();
|
||||||
|
private boolean allItemsDisabled;
|
||||||
|
|
||||||
|
|
||||||
|
public TransferKeyAdapter(Context context, LayoutInflater layoutInflater,
|
||||||
|
OnClickTransferKeyListener onClickTransferKeyListener) {
|
||||||
|
this.context = context;
|
||||||
|
this.layoutInflater = layoutInflater;
|
||||||
|
this.onClickTransferKeyListener = onClickTransferKeyListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<SecretKeyItem> 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<List<SecretKeyItem>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,10 @@ import java.util.Locale;
|
|||||||
*/
|
*/
|
||||||
public class QrCodeUtils {
|
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) {
|
public static Bitmap getQRCodeBitmap(final Uri uri, final int size) {
|
||||||
// for URIs we want alphanumeric encoding to save space, thus make everything upper case!
|
// for URIs we want alphanumeric encoding to save space, thus make everything upper case!
|
||||||
// zxing will then select Mode.ALPHANUMERIC internally
|
// zxing will then select Mode.ALPHANUMERIC internally
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
|||||||
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
|
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
|
||||||
|
|
||||||
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
|
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
|
||||||
|
private final boolean showAfterLast;
|
||||||
|
|
||||||
private Drawable mDivider;
|
private Drawable mDivider;
|
||||||
|
|
||||||
private int mOrientation;
|
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);
|
final TypedArray a = context.obtainStyledAttributes(ATTRS);
|
||||||
mDivider = a.getDrawable(0);
|
mDivider = a.getDrawable(0);
|
||||||
a.recycle();
|
a.recycle();
|
||||||
@@ -66,8 +68,11 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
|||||||
final int left = parent.getPaddingLeft();
|
final int left = parent.getPaddingLeft();
|
||||||
final int right = parent.getWidth() - parent.getPaddingRight();
|
final int right = parent.getWidth() - parent.getPaddingRight();
|
||||||
|
|
||||||
final int childCount = parent.getChildCount();
|
int childCount = parent.getChildCount();
|
||||||
for (int i = 0; i < childCount -1; i++) {
|
if (!showAfterLast) {
|
||||||
|
childCount -= 1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
final View child = parent.getChildAt(i);
|
final View child = parent.getChildAt(i);
|
||||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
|
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
|
||||||
.getLayoutParams();
|
.getLayoutParams();
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Vincent Breitmoser <look@my.amazin.horse>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,4 +99,21 @@ public class ToolableViewAnimator extends ViewAnimator {
|
|||||||
setOutAnimation(savedOutAnim);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png
Normal file
|
After Width: | Height: | Size: 386 B |
BIN
OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png
Normal file
|
After Width: | Height: | Size: 614 B |
BIN
OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png
Normal file
|
After Width: | Height: | Size: 317 B |
BIN
OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png
Normal file
|
After Width: | Height: | Size: 445 B |
BIN
OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
BIN
OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png
Normal file
|
After Width: | Height: | Size: 596 B |
BIN
OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png
Normal file
|
After Width: | Height: | Size: 464 B |
BIN
OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png
Normal file
|
After Width: | Height: | Size: 708 B |
BIN
OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
BIN
OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png
Normal file
|
After Width: | Height: | Size: 905 B |
BIN
OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png
Normal file
|
After Width: | Height: | Size: 593 B |
BIN
OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
BIN
OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
@@ -115,6 +115,24 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
style="?android:attr/borderlessButtonStyle" />
|
style="?android:attr/borderlessButtonStyle" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/create_key_secure_device_setup"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/first_time_secure_wifi_transfer"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||||
|
android:drawableRight="@drawable/ic_chevron_right_grey_24dp"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:gravity="right|center_vertical"
|
||||||
|
android:clickable="true"
|
||||||
|
style="?android:attr/borderlessButtonStyle" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/create_key_cancel"
|
android:id="@+id/create_key_cancel"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
|
|||||||
102
OpenKeychain/src/main/res/layout/key_transfer_item.xml
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:custom="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="?attr/listPreferredItemHeight"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingLeft="4dp"
|
||||||
|
android:paddingRight="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dip"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:focusable="true"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingRight="4dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/key_list_item_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="@string/label_main_user_id"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/key_list_item_email"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="user@example.com"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/key_list_item_creation"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
tools:text="Created on 10/10/2010 10:00" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:outAnimation="@anim/fade_out"
|
||||||
|
android:inAnimation="@anim/fade_in"
|
||||||
|
android:id="@+id/transfer_state"
|
||||||
|
custom:initialView="01">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:id="@+id/button_transfer"
|
||||||
|
android:src="@drawable/ic_send_24dp"
|
||||||
|
android:tint="@color/md_grey_600"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_check_black_24dp"
|
||||||
|
android:tint="@color/android_green_light"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:id="@+id/button_import"
|
||||||
|
android:text="@string/button_import"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
374
OpenKeychain/src/main/res/layout/transfer_fragment.xml
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:custom="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_animator"
|
||||||
|
android:inAnimation="@anim/fade_in_delayed"
|
||||||
|
android:outAnimation="@anim/fade_out"
|
||||||
|
custom:initialView="0">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_layout_waiting"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/transfer_scan_explanation"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:weightSum="1">
|
||||||
|
|
||||||
|
<android.support.v7.widget.CardView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0.75"
|
||||||
|
custom:cardUseCompatPadding="true"
|
||||||
|
custom:cardCornerRadius="4dp"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/qr_code_image"
|
||||||
|
tools:layout_height="240dp"
|
||||||
|
tools:src="@drawable/ic_qrcode_white_24dp"
|
||||||
|
tools:tint="@color/md_black_1000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</android.support.v7.widget.CardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/transfer_scan_warning"
|
||||||
|
android:textColor="@color/android_red_dark"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
tools:ignore="ButtonStyle" >
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:id="@+id/button_help"
|
||||||
|
android:text="@string/button_more_info"
|
||||||
|
android:drawableLeft="@drawable/ic_live_help_24dp"
|
||||||
|
android:drawableTint="@color/md_black_1000"
|
||||||
|
android:drawablePadding="8dp"/>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:id="@+id/button_scan"
|
||||||
|
android:text="@string/button_scan"
|
||||||
|
android:drawableLeft="@drawable/ic_qrcode_white_24dp"
|
||||||
|
android:drawableTint="@color/md_black_1000"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_layout_connecting"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<org.sufficientlysecure.keychain.ui.widget.ConnectionStatusView
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:text="@string/transfer_connecting"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_layout_connected"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<org.sufficientlysecure.keychain.ui.widget.ConnectionStatusView
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:id="@+id/connection_status_icon_1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_toRightOf="@+id/connection_status_icon_1"
|
||||||
|
android:layout_toEndOf="@+id/connection_status_icon_1"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:id="@+id/connection_title_1"
|
||||||
|
android:text="@string/connection_status"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_toRightOf="@+id/connection_status_icon_1"
|
||||||
|
android:layout_toEndOf="@+id/connection_status_icon_1"
|
||||||
|
android:layout_below="@+id/connection_title_1"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:id="@+id/connection_status_1"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
tools:text="Connected"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/section_transfer_keys"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
style="@style/SectionHeader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/transfer_key_list"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:id="@+id/transfer_key_list_empty"
|
||||||
|
android:text="@string/transfer_list_empty"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_layout_passive"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<org.sufficientlysecure.keychain.ui.widget.ConnectionStatusView
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:id="@+id/connection_status_icon_2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_toRightOf="@+id/connection_status_icon_2"
|
||||||
|
android:layout_toEndOf="@+id/connection_status_icon_2"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:id="@+id/connection_title_2"
|
||||||
|
android:text="@string/connection_status"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_toRightOf="@+id/connection_status_icon_2"
|
||||||
|
android:layout_toEndOf="@+id/connection_status_icon_2"
|
||||||
|
android:layout_below="@+id/connection_title_2"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:id="@+id/connection_status_2"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
tools:text="Connected"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/section_received_keys"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
style="@style/SectionHeader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/received_key_list"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_layout_no_wifi"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:src="@drawable/ic_wifi_off_96dp"
|
||||||
|
android:tint="@color/md_grey_600"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:text="@string/transfer_scan_only_wifi"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/transfer_layout_wifi_error"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:src="@drawable/ic_wifi_question_96dp"
|
||||||
|
android:tint="@color/md_grey_600"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/transfer_error_wifi_text"
|
||||||
|
tools:text="Connection failed!"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:id="@+id/transfer_wifi_error_instructions"
|
||||||
|
android:text="@string/transfer_error_wifi_text_instructions"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:id="@+id/button_scan_again"
|
||||||
|
android:text="@string/button_scan_again"
|
||||||
|
android:drawableLeft="@drawable/ic_qrcode_white_24dp"
|
||||||
|
android:drawableTint="@color/md_black_1000"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="48dp"
|
||||||
|
android:src="@drawable/ic_android_96dp"
|
||||||
|
android:tint="@color/md_grey_400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:text="@string/transfer_not_available"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -18,6 +18,11 @@
|
|||||||
android:title="@string/menu_export_key"
|
android:title="@string/menu_export_key"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_key_view_skt"
|
||||||
|
android:title="@string/menu_secure_wifi_transfer"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_key_view_delete"
|
android:id="@+id/menu_key_view_delete"
|
||||||
android:icon="@drawable/ic_delete_grey_24dp"
|
android:icon="@drawable/ic_delete_grey_24dp"
|
||||||
|
|||||||
11
OpenKeychain/src/main/res/menu/transfer_menu.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:title="Done"
|
||||||
|
android:id="@+id/menu_done"
|
||||||
|
app:showAsAction="ifRoom"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -848,6 +848,7 @@
|
|||||||
<string name="drawer_close">"Close navigation drawer"</string>
|
<string name="drawer_close">"Close navigation drawer"</string>
|
||||||
<string name="my_keys">"My Keys"</string>
|
<string name="my_keys">"My Keys"</string>
|
||||||
<string name="nav_backup">"Backup/Restore"</string>
|
<string name="nav_backup">"Backup/Restore"</string>
|
||||||
|
<string name="nav_transfer">"Secure Wifi Transfer"</string>
|
||||||
|
|
||||||
<!-- hints -->
|
<!-- hints -->
|
||||||
<string name="encrypt_content_edit_text_hint">"Type text"</string>
|
<string name="encrypt_content_edit_text_hint">"Type text"</string>
|
||||||
@@ -1888,4 +1889,34 @@
|
|||||||
|
|
||||||
<string name="identity_context_forget">Forget</string>
|
<string name="identity_context_forget">Forget</string>
|
||||||
|
|
||||||
|
<string name="button_scan">"Scan"</string>
|
||||||
|
<string name="button_more_info">"More Info"</string>
|
||||||
|
<string name="transfer_scan_explanation">"To share or import your key setup, establish a secure connection by scanning with either device."</string>
|
||||||
|
<string name="transfer_scan_warning">Use with your own devices only!</string>
|
||||||
|
<string name="connection_status">"Connection status:"</string>
|
||||||
|
<string name="section_transfer_keys">"Available keys for transfer"</string>
|
||||||
|
<string name="section_received_keys">"Received keys"</string>
|
||||||
|
<string name="transfer_scan_only_wifi">"This feature can only be used on Wifi."</string>
|
||||||
|
<string name="button_import">"Import"</string>
|
||||||
|
<string name="transfer_status_connected">"Connected"</string>
|
||||||
|
<string name="transfer_status_disconnected">"Disconnected!"</string>
|
||||||
|
<string name="transfer_error_read_incoming">"Failed reading incoming key!"</string>
|
||||||
|
<string name="transfer_error_connect">"Connection failed!"</string>
|
||||||
|
<string name="transfer_error_listen">"Error waiting for connection!"</string>
|
||||||
|
<string name="transfer_error_generic">"Unknown communication error!"</string>
|
||||||
|
<string name="transfer_error_generic_msg">"Communication error: %s"</string>
|
||||||
|
<string name="transfer_not_available">"Sorry, this feature can only be used on Android 5 or newer :("</string>
|
||||||
|
<string name="transfer_list_empty">"No keys on this device that could be sent. Waiting for incoming keys…"</string>
|
||||||
|
<string name="transfer_connecting">"Connecting…"</string>
|
||||||
|
<string name="first_time_secure_wifi_transfer">"Secure Wifi Transfer"</string>
|
||||||
|
<string name="menu_secure_wifi_transfer">"Secure Wifi Transfer"</string>
|
||||||
|
<string name="transfer_confirm_cancel">"Cancel"</string>
|
||||||
|
<string name="transfer_confirm_ok">"Send"</string>
|
||||||
|
<string name="transfer_confirm_title">"Send your key?"</string>
|
||||||
|
<string name="transfer_confirm_text">"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!"</string>
|
||||||
|
<string name="button_scan_again">"Scan Again"</string>
|
||||||
|
<string name="transfer_error_wifi_text">"Connection failed!"</string>
|
||||||
|
<string name="transfer_error_wifi_text_instructions">"Make sure you are on the same network, then scan again."</string>
|
||||||
|
<string name="transfer_error_wifi_text_instructions_ssid">"Make sure you are on the "%s" network, then scan again."</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
4
graphics/drawables/ic_android.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M6 18c0 .55.45 1 1 1h1v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h2v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h1c.55 0 1-.45 1-1V8H6v10zM3.5 8C2.67 8 2 8.67 2 9.5v7c0 .83.67 1.5 1.5 1.5S5 17.33 5 16.5v-7C5 8.67 4.33 8 3.5 8zm17 0c-.83 0-1.5.67-1.5 1.5v7c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5v-7c0-.83-.67-1.5-1.5-1.5zm-4.97-5.84l1.3-1.3c.2-.2.2-.51 0-.71-.2-.2-.51-.2-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.15c-.2-.2-.51-.2-.71 0-.2.2-.2.51 0 .71l1.31 1.31C6.97 3.26 6 5.01 6 7h12c0-1.99-.97-3.75-2.47-4.84zM10 5H9V4h1v1zm5 0h-1V4h1v1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 730 B |
4
graphics/drawables/ic_live_help.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M19 2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-6 16h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 11.9 13 12.5 13 14h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 454 B |
4
graphics/drawables/ic_send.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 201 B |
4
graphics/drawables/ic_wifi_lock.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||||
|
<path d="M23 16v-1.5c0-1.4-1.1-2.5-2.5-2.5S18 13.1 18 14.5V16c-.5 0-1 .5-1 1v4c0 .5.5 1 1 1h5c.5 0 1-.5 1-1v-4c0-.5-.5-1-1-1zm-1 0h-3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V16zm-6.5-1.5c0-2.8 2.2-5 5-5 .4 0 .7 0 1 .1L23.6 7c-.4-.3-4.9-4-11.6-4C5.3 3 .8 6.7.4 7L12 21.5l3.5-4.4v-2.6z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 438 B |
4
graphics/drawables/ic_wifi_off.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M23.64 7c-.45-.34-4.93-4-11.64-4-1.5 0-2.89.19-4.15.48L18.18 13.8 23.64 7zm-6.6 8.22L3.27 1.44 2 2.72l2.05 2.06C1.91 5.76.59 6.82.36 7l11.63 14.49.01.01.01-.01 3.9-4.86 3.32 3.32 1.27-1.27-3.46-3.46z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 363 B |
80
graphics/drawables/ic_wifi_question.svg
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
fill="#000000"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
version="1.1"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="ic_wifi_question.svg"
|
||||||
|
inkscape:version="0.92.1 r15371">
|
||||||
|
<metadata
|
||||||
|
id="metadata12">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs10" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1056"
|
||||||
|
id="namedview8"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:measure-start="15.5,6.9"
|
||||||
|
inkscape:measure-end="21.5,14.4"
|
||||||
|
inkscape:zoom="39.333333"
|
||||||
|
inkscape:cx="12.121876"
|
||||||
|
inkscape:cy="9.0333709"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="24"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0V0z"
|
||||||
|
fill="none"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
d="m 15.5,14.5 c 0,-2.8 2.2,-5 5,-5 0.4,0 0.7,0 1,0.1 L 23.6,7 C 23.2,6.7 18.7,3 12,3 5.3,3 0.8,6.7 0.4,7 L 12,21.5 15.5,17.1 Z"
|
||||||
|
id="path4"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ssccscccs" />
|
||||||
|
<g
|
||||||
|
style="fill:none;fill-rule:evenodd;stroke:none;stroke-width:1"
|
||||||
|
id="Page-1"
|
||||||
|
sketch:type="MSPage"
|
||||||
|
transform="matrix(0.14184741,0,0,0.14184741,13.309409,9.1313496)">
|
||||||
|
<g
|
||||||
|
style="fill:#000000"
|
||||||
|
id="signature-unverified"
|
||||||
|
sketch:type="MSArtboardGroup">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="M 56.359,80.095 H 42.883 V 66.877 H 56.359 Z M 70.222,40.709 c -0.717,1.773 -1.602,3.289 -2.66,4.547 -1.061,1.259 -2.232,2.332 -3.52,3.218 -1.287,0.888 -2.504,1.772 -3.648,2.659 -1.145,0.889 -2.16,1.903 -3.047,3.047 -0.889,1.145 -1.445,2.573 -1.674,4.289 v 3.26 H 44.086 v -3.861 c 0.172,-2.457 0.643,-4.518 1.416,-6.176 0.771,-1.658 1.674,-3.075 2.703,-4.248 1.031,-1.172 2.117,-2.188 3.261,-3.046 1.145,-0.856 2.203,-1.716 3.176,-2.573 0.973,-0.858 1.76,-1.801 2.361,-2.831 0.601,-1.03 0.871,-2.316 0.814,-3.86 0,-2.631 -0.643,-4.576 -1.93,-5.836 -1.289,-1.256 -3.078,-1.887 -5.365,-1.887 -1.545,0 -2.875,0.301 -3.992,0.901 -1.115,0.601 -2.031,1.401 -2.746,2.402 -0.717,1.001 -1.244,2.174 -1.588,3.517 -0.344,1.346 -0.516,2.79 -0.516,4.334 H 29.065 c 0.055,-3.09 0.586,-5.921 1.588,-8.494 1,-2.574 2.402,-4.805 4.205,-6.691 1.803,-1.889 3.977,-3.361 6.523,-4.422 2.547,-1.059 5.395,-1.588 8.541,-1.588 4.062,0 7.453,0.559 10.171,1.674 2.717,1.116 4.906,2.504 6.566,4.164 1.66,1.659 2.846,3.446 3.562,5.362 0.715,1.917 1.072,3.703 1.072,5.362 0.002,2.745 -0.356,5.006 -1.071,6.777 z"
|
||||||
|
sketch:type="MSShapeGroup"
|
||||||
|
id="path8"
|
||||||
|
sodipodi:nodetypes="ccccccccccccccccccsccsccccccccsccccc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -22,7 +22,7 @@ SRC_DIR=./drawables/
|
|||||||
#inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg
|
#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
|
do
|
||||||
echo $NAME
|
echo $NAME
|
||||||
inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
|
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"
|
inkscape -w 96 -h 96 -e "$XXXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
|
||||||
done
|
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
|
do
|
||||||
echo $NAME
|
echo $NAME
|
||||||
inkscape -w 96 -h 96 -e "$MDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"
|
inkscape -w 96 -h 96 -e "$MDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"
|
||||||
|
|||||||