diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 7b4f3ee28..d3bf9c3d5 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -82,6 +82,7 @@ + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 70b29141e..a8e2c99f8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -74,6 +74,9 @@ public final class Constants { // used by QR Codes (Guardian Project, Monkeysphere compatibility) 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; // prefix packagename for exported Intents diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferInteractor.java new file mode 100644 index 000000000..6a421673e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferInteractor.java @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.network; + + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.NoRouteToHostException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.net.PskKeyManager; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class KeyTransferInteractor { + private static final String[] ALLOWED_CIPHERSUITES = new String[] { + // only allow ephemeral diffie-hellman based PSK ciphers! + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256" + }; + + private static final int CONNECTION_LISTENING = 1; + private static final int CONNECTION_ESTABLISHED = 2; + private static final int CONNECTION_SEND_OK = 3; + private static final int CONNECTION_RECEIVE_OK = 4; + private static final int CONNECTION_LOST = 5; + private static final int CONNECTION_ERROR_NO_ROUTE_TO_HOST = 6; + private static final int CONNECTION_ERROR_CONNECT = 7; + private static final int CONNECTION_ERROR_WHILE_CONNECTED = 8; + private static final int CONNECTION_ERROR_LISTEN = 0; + + private static final int TIMEOUT_CONNECTING = 1500; + private static final int TIMEOUT_RECEIVING = 2000; + private static final int TIMEOUT_WAITING = 500; + private static final int PSK_BYTE_LENGTH = 16; + + + private final String delimiterStart; + private final String delimiterEnd; + + private TransferThread transferThread; + + + public KeyTransferInteractor(String delimiterStart, String delimiterEnd) { + this.delimiterStart = delimiterStart; + this.delimiterEnd = delimiterEnd; + } + + public void connectToServer(String qrCodeContent, KeyTransferCallback callback) throws URISyntaxException { + SktUri sktUri = SktUri.parse(qrCodeContent); + + transferThread = TransferThread.createClientTransferThread(delimiterStart, delimiterEnd, callback, + sktUri.getPresharedKey(), sktUri.getHost(), sktUri.getPort(), sktUri.getWifiSsid()); + transferThread.start(); + } + + public void startServer(KeyTransferCallback callback, String wifiSsid) { + byte[] presharedKey = generatePresharedKey(); + + transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey, wifiSsid); + transferThread.start(); + } + + private static class TransferThread extends Thread { + private final String delimiterStart; + private final String delimiterEnd; + + private final Handler handler; + private final byte[] presharedKey; + private final boolean isServer; + private final String clientHost; + private final Integer clientPort; + private final String wifiSsid; + + private KeyTransferCallback callback; + private SSLServerSocket serverSocket; + private byte[] dataToSend; + private String sendPassthrough; + + static TransferThread createClientTransferThread(String delimiterStart, String delimiterEnd, + KeyTransferCallback callback, byte[] presharedKey, String host, int port, String wifiSsid) { + return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, false, host, port, wifiSsid); + } + + static TransferThread createServerTransferThread(String delimiterStart, String delimiterEnd, + KeyTransferCallback callback, byte[] presharedKey, String wifiSsid) { + return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, true, null, null, wifiSsid); + } + + private TransferThread(String delimiterStart, String delimiterEnd, + KeyTransferCallback callback, byte[] presharedKey, boolean isServer, + String clientHost, Integer clientPort, String wifiSsid) { + super("TLS-PSK Key Transfer Thread"); + + this.delimiterStart = delimiterStart; + this.delimiterEnd = delimiterEnd; + + this.callback = callback; + this.presharedKey = presharedKey; + this.clientHost = clientHost; + this.clientPort = clientPort; + this.wifiSsid = wifiSsid; + this.isServer = isServer; + + handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void run() { + SSLContext sslContext = createTlsPskSslContext(presharedKey); + + Socket socket = null; + try { + socket = getSocketListenOrConnect(sslContext); + if (socket == null) { + return; + } + + try { + handleOpenConnection(socket); + Log.d(Constants.TAG, "connection closed ok!"); + } catch (SSLHandshakeException e) { + Log.d(Constants.TAG, "ssl handshake error!", e); + invokeListener(CONNECTION_ERROR_CONNECT, null); + } catch (IOException e) { + Log.e(Constants.TAG, "communication error!", e); + invokeListener(CONNECTION_ERROR_WHILE_CONNECTED, e.getLocalizedMessage()); + } + } finally { + closeQuietly(socket); + closeQuietly(serverSocket); + } + } + + @Nullable + private Socket getSocketListenOrConnect(SSLContext sslContext) { + Socket socket; + if (isServer) { + try { + serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(0); + String[] supportedCipherSuites = serverSocket.getSupportedCipherSuites(); + String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES); + serverSocket.setEnabledCipherSuites(enabledCipherSuites); + + SktUri sktUri = SktUri.create(getIPAddress(true), serverSocket.getLocalPort(), presharedKey, wifiSsid); + invokeListener(CONNECTION_LISTENING, sktUri.toUriString()); + + socket = serverSocket.accept(); + } catch (IOException e) { + Log.e(Constants.TAG, "error while listening!", e); + invokeListener(CONNECTION_ERROR_LISTEN, null); + return null; + } + } else { + try { + SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(); + String[] supportedCipherSuites = sslSocket.getSupportedCipherSuites(); + String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES); + sslSocket.setEnabledCipherSuites(enabledCipherSuites); + + socket = sslSocket; + socket.connect(new InetSocketAddress(InetAddress.getByName(clientHost), clientPort), TIMEOUT_CONNECTING); + } catch (IOException e) { + Log.e(Constants.TAG, "error while connecting!", e); + if (e instanceof NoRouteToHostException) { + invokeListener(CONNECTION_ERROR_NO_ROUTE_TO_HOST, wifiSsid); + } else { + invokeListener(CONNECTION_ERROR_CONNECT, null); + } + return null; + } + } + return socket; + } + + private static SSLContext createTlsPskSslContext(byte[] presharedKey) { + try { + PresharedKeyManager pskKeyManager = new PresharedKeyManager(presharedKey); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null); + + return sslContext; + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + private void handleOpenConnection(Socket socket) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + OutputStream outputStream = new BufferedOutputStream(socket.getOutputStream()); + + invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); + + socket.setSoTimeout(TIMEOUT_WAITING); + while (!isInterrupted() && socket.isConnected() && !socket.isClosed()) { + sendDataIfAvailable(socket, outputStream); + boolean connectionTerminated = receiveDataIfAvailable(socket, bufferedReader); + if (connectionTerminated) { + break; + } + } + Log.d(Constants.TAG, "disconnected"); + invokeListener(CONNECTION_LOST, null); + } + + private boolean receiveDataIfAvailable(Socket socket, BufferedReader bufferedReader) throws IOException { + String firstLine; + try { + firstLine = bufferedReader.readLine(); + } catch (SocketTimeoutException e) { + return false; + } + + if (firstLine == null) { + return true; + } + + boolean lineIsDelimiter = delimiterStart.equals(firstLine); + if (!lineIsDelimiter) { + Log.d(Constants.TAG, "bad beginning of key block?"); + return false; + } + + socket.setSoTimeout(TIMEOUT_RECEIVING); + String receivedData = receiveLinesUntilEndDelimiter(bufferedReader, firstLine); + socket.setSoTimeout(TIMEOUT_WAITING); + + invokeListener(CONNECTION_RECEIVE_OK, receivedData); + return false; + } + + private boolean sendDataIfAvailable(Socket socket, OutputStream outputStream) throws IOException { + if (dataToSend != null) { + byte[] data = dataToSend; + dataToSend = null; + + socket.setSoTimeout(TIMEOUT_RECEIVING); + outputStream.write(data); + outputStream.flush(); + socket.setSoTimeout(TIMEOUT_WAITING); + + invokeListener(CONNECTION_SEND_OK, sendPassthrough); + sendPassthrough = null; + return true; + } + return false; + } + + private String receiveLinesUntilEndDelimiter(BufferedReader bufferedReader, String line) throws IOException { + StringBuilder builder = new StringBuilder(); + do { + boolean lineIsDelimiter = delimiterEnd.equals(line); + if (lineIsDelimiter) { + break; + } + + builder.append(line).append('\n'); + + line = bufferedReader.readLine(); + } while (line != null); + + return builder.toString(); + } + + private void invokeListener(final int method, final String arg) { + if (handler == null) { + return; + } + + Runnable runnable = new Runnable() { + @Override + public void run() { + if (callback == null) { + return; + } + switch (method) { + case CONNECTION_LISTENING: + callback.onServerStarted(arg); + break; + case CONNECTION_ESTABLISHED: + callback.onConnectionEstablished(arg); + break; + case CONNECTION_RECEIVE_OK: + callback.onDataReceivedOk(arg); + break; + case CONNECTION_SEND_OK: + callback.onDataSentOk(arg); + break; + case CONNECTION_LOST: + callback.onConnectionLost(); + break; + case CONNECTION_ERROR_WHILE_CONNECTED: + callback.onConnectionError(arg); + break; + case CONNECTION_ERROR_NO_ROUTE_TO_HOST: + callback.onConnectionErrorNoRouteToHost(wifiSsid); + break; + case CONNECTION_ERROR_CONNECT: + callback.onConnectionErrorConnect(); + break; + case CONNECTION_ERROR_LISTEN: + callback.onConnectionErrorListen(); + break; + } + } + }; + + handler.post(runnable); + } + + synchronized void sendData(byte[] dataToSend, String passthrough) { + this.dataToSend = dataToSend; + this.sendPassthrough = passthrough; + } + + @Override + public void interrupt() { + callback = null; + super.interrupt(); + closeQuietly(serverSocket); + } + } + + private static byte[] generatePresharedKey() { + byte[] presharedKey = new byte[PSK_BYTE_LENGTH]; + new SecureRandom().nextBytes(presharedKey); + return presharedKey; + } + + public void closeConnection() { + if (transferThread != null) { + transferThread.interrupt(); + } + + transferThread = null; + } + + public void sendData(byte[] dataToSend, String passthrough) { + transferThread.sendData(dataToSend, passthrough); + } + + public interface KeyTransferCallback { + void onServerStarted(String qrCodeData); + void onConnectionEstablished(String otherName); + void onConnectionLost(); + + void onDataReceivedOk(String receivedData); + void onDataSentOk(String passthrough); + + void onConnectionErrorConnect(); + void onConnectionErrorNoRouteToHost(String wifiSsid); + void onConnectionErrorListen(); + void onConnectionError(String arg); + } + + /** + * from: http://stackoverflow.com/a/13007325 + *

+ * Get IP address from first non-localhost interface + * + * @param useIPv4 true=return ipv4, false=return ipv6 + * @return address or empty string + */ + private static String getIPAddress(boolean useIPv4) { + try { + List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface intf : interfaces) { + List addrs = Collections.list(intf.getInetAddresses()); + for (InetAddress addr : addrs) { + if (addr.isLoopbackAddress()) { + continue; + } + String sAddr = addr.getHostAddress(); + boolean isIPv4 = sAddr.indexOf(':') < 0; + if (useIPv4) { + if (isIPv4) { + return sAddr; + } + } else { + int delimIndex = sAddr.indexOf('%'); // drop ip6 zone suffix + if (delimIndex >= 0) { + sAddr = sAddr.substring(0, delimIndex); + } + return sAddr.toUpperCase(); + } + } + } + } catch (Exception ex) { + // ignore + } + return ""; + } + + private static class PresharedKeyManager extends PskKeyManager implements KeyManager { + byte[] presharedKey; + + private PresharedKeyManager(byte[] presharedKey) { + this.presharedKey = presharedKey; + } + + @Override + public String chooseClientKeyIdentity(String identityHint, Socket socket) { + return identityHint; + } + + @Override + public String chooseClientKeyIdentity(String identityHint, SSLEngine engine) { + return identityHint; + } + + @Override + public SecretKey getKey(String identityHint, String identity, Socket socket) { + return new SecretKeySpec(presharedKey, "AES"); + } + + @Override + public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { + return new SecretKeySpec(presharedKey, "AES"); + } + } + + private static void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException e) { + // ignore + } + } + + private static String[] intersectArrays(String[] array1, String[] array2) { + Set s1 = new HashSet<>(Arrays.asList(array1)); + Set s2 = new HashSet<>(Arrays.asList(array2)); + s1.retainAll(s2); + + return s1.toArray(new String[0]); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/SktUri.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/SktUri.java new file mode 100644 index 000000000..e73f756d6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/SktUri.java @@ -0,0 +1,90 @@ +package org.sufficientlysecure.keychain.network; + + +import java.net.URISyntaxException; +import java.nio.charset.Charset; + +import android.annotation.SuppressLint; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.auto.value.AutoValue; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + + +@AutoValue +abstract class SktUri { + private static final String QRCODE_URI_FORMAT = Constants.SKT_SCHEME + ":%s/%d/%s"; + private static final String QRCODE_URI_FORMAT_SSID = Constants.SKT_SCHEME + ":%s/%d/%s/SSID:%s"; + + + public abstract String getHost(); + public abstract int getPort(); + public abstract byte[] getPresharedKey(); + + @Nullable + public abstract String getWifiSsid(); + + @NonNull + public static SktUri parse(String input) throws URISyntaxException { + if (!input.startsWith(Constants.SKT_SCHEME + ":")) { + throw new URISyntaxException(input, "invalid scheme"); + } + + String[] pieces = input.substring(input.indexOf(":") +1).split("/"); + if (pieces.length < 3) { + throw new URISyntaxException(input, "invalid syntax"); + } + + String address = pieces[0]; + int port; + try { + port = Integer.parseInt(pieces[1]); + } catch (NumberFormatException e) { + throw new URISyntaxException(input, "error parsing port"); + } + byte[] psk; + try { + psk = Hex.decode(pieces[2]); + } catch (DecoderException e) { + throw new URISyntaxException(input, "error parsing hex psk"); + } + + String wifiSsid = null; + for (int i = 3; i < pieces.length; i++) { + String[] optarg = pieces[i].split(":", 2); + if (optarg.length == 2 && "SSID".equals(optarg[0])) { + try { + wifiSsid = new String(Hex.decode(optarg[1])); + } catch (DecoderException e) { + Log.d(Constants.TAG, "error parsing ssid in skt uri, ignoring: " + input); + } + } + } + + return new AutoValue_SktUri(address, port, psk, wifiSsid); + } + + @SuppressLint("DefaultLocale") + String toUriString() { + String sktHex = Hex.toHexString(getPresharedKey()); + String wifiSsid = getWifiSsid(); + + String result; + if (wifiSsid != null) { + String encodedWifiSsid = Hex.toHexString(getWifiSsid().getBytes(Charset.defaultCharset())); + result = String.format(QRCODE_URI_FORMAT_SSID, getHost(), getPort(), sktHex, encodedWifiSsid); + } else { + result = String.format(QRCODE_URI_FORMAT, getHost(), getPort(), sktHex); + } + + return result.toUpperCase(); + } + + static SktUri create(String host, int port, byte[] presharedKey, @Nullable String wifiSsid) { + return new AutoValue_SktUri(host, port, presharedKey, wifiSsid); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java index 4d584331a..a6bc0233b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -22,6 +22,7 @@ package org.sufficientlysecure.keychain.operations; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; @@ -477,7 +478,7 @@ public class ImportOperation extends BaseReadWriteOperation @NonNull @Override public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) { - ArrayList keyList = importInput.getKeyList(); + List keyList = importInput.getKeyList(); HkpKeyserverAddress keyServer = importInput.getKeyserver(); boolean skipSave = importInput.isSkipSave(); @@ -510,7 +511,7 @@ public class ImportOperation extends BaseReadWriteOperation } @NonNull - private ImportKeyResult multiThreadedKeyImport(ArrayList keyList, + private ImportKeyResult multiThreadedKeyImport(List keyList, final HkpKeyserverAddress keyServer, final ParcelableProxy proxy, final boolean skipSave) { Log.d(Constants.TAG, "Multi-threaded key import starting"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java index acdbb93fc..850c24640 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -103,6 +103,10 @@ public class UncachedKeyRing { return mRing.getPublicKey().getKeyID(); } + public long getCreationTime() { + return mRing.getPublicKey().getCreationTime().getTime(); + } + public UncachedPublicKey getPublicKey() { return new UncachedPublicKey(mRing.getPublicKey()); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java index 5277cd393..f2626bcf2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java @@ -13,12 +13,12 @@ import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; +import org.bouncycastle.bcpg.ArmoredOutputStream; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; @@ -236,19 +236,27 @@ public class KeyRepository { } } - private String getKeyRingAsArmoredString(byte[] data) throws IOException, PgpGeneralException { - UncachedKeyRing keyRing = UncachedKeyRing.decodeFromData(data); - + private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException, PgpGeneralException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); - keyRing.encodeArmored(bos, null); + ArmoredOutputStream aos = new ArmoredOutputStream(bos); - return bos.toString("UTF-8"); + aos.write(data); + aos.close(); + + return bos.toByteArray(); } public String getPublicKeyRingAsArmoredString(long masterKeyId) throws NotFoundException, IOException, PgpGeneralException { byte[] data = loadPublicKeyRingData(masterKeyId); - return getKeyRingAsArmoredString(data); + byte[] armoredData = getKeyRingAsArmoredData(data); + return new String(armoredData); + } + + public byte[] getSecretKeyRingAsArmoredData(long masterKeyId) + throws NotFoundException, IOException, PgpGeneralException { + byte[] data = loadSecretKeyRingData(masterKeyId); + return getKeyRingAsArmoredData(data); } public ContentResolver getContentResolver() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java index 6597e91e1..6aee2a219 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java @@ -122,7 +122,8 @@ public class RemoteDeduplicateActivity extends FragmentActivity { keyChoiceList = (RecyclerView) view.findViewById(R.id.duplicate_key_list); keyChoiceList.setLayoutManager(new LinearLayoutManager(activity)); - keyChoiceList.addItemDecoration(new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST)); + keyChoiceList.addItemDecoration( + new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true)); setupListenersForPresenter(); mvpView = createMvpView(view, layoutInflater); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java index 7b9c4f60d..e7a0e2d51 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java @@ -20,6 +20,8 @@ package org.sufficientlysecure.keychain.service; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -31,21 +33,25 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; @AutoValue public abstract class ImportKeyringParcel implements Parcelable { @Nullable // If null, keys are expected to be read from a cache file in ImportExportOperations - public abstract ArrayList getKeyList(); + public abstract List getKeyList(); @Nullable // must be set if keys are to be imported from a keyserver public abstract HkpKeyserverAddress getKeyserver(); public abstract boolean isSkipSave(); - public static ImportKeyringParcel createImportKeyringParcel(ArrayList keyList, + public static ImportKeyringParcel createImportKeyringParcel(List keyList, HkpKeyserverAddress keyserver) { return new AutoValue_ImportKeyringParcel(keyList, keyserver, false); } - public static ImportKeyringParcel createWithSkipSave(ArrayList keyList, + public static ImportKeyringParcel createWithSkipSave(List keyList, HkpKeyserverAddress keyserver) { return new AutoValue_ImportKeyringParcel(keyList, keyserver, true); } + public static ImportKeyringParcel createImportKeyringParcel(ParcelableKeyRing key) { + return new AutoValue_ImportKeyringParcel(Collections.singletonList(key), null, false); + } + public static ImportKeyringParcel createFromFileCacheWithSkipSave() { return new AutoValue_ImportKeyringParcel(null, null, true); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java index 007049e15..e3e1719a2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java @@ -17,23 +17,31 @@ package org.sufficientlysecure.keychain.ui; + +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; public class CreateKeyStartFragment extends Fragment { + public static final int REQUEST_CODE_IMPORT_KEY = 0x00007012; + CreateKeyActivity mCreateKeyActivity; @@ -41,7 +49,8 @@ public class CreateKeyStartFragment extends Fragment { View mImportKey; View mSecurityToken; TextView mSkipOrCancel; - public static final int REQUEST_CODE_IMPORT_KEY = 0x00007012; + View mSecureDeviceSetup; + /** * Creates new instance of this fragment @@ -64,6 +73,7 @@ public class CreateKeyStartFragment extends Fragment { mImportKey = view.findViewById(R.id.create_key_import_button); mSecurityToken = view.findViewById(R.id.create_key_security_token_button); mSkipOrCancel = (TextView) view.findViewById(R.id.create_key_cancel); + mSecureDeviceSetup = view.findViewById(R.id.create_key_secure_device_setup); if (mCreateKeyActivity.mFirstTime) { mSkipOrCancel.setText(R.string.first_time_skip); @@ -96,6 +106,19 @@ public class CreateKeyStartFragment extends Fragment { } }); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + mSecureDeviceSetup.setOnClickListener(new OnClickListener() { + @TargetApi(VERSION_CODES.LOLLIPOP) + @Override + public void onClick(View v) { + TransferFragment frag = new TransferFragment(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } + }); + } else { + mSecureDeviceSetup.setVisibility(View.GONE); + } + mSkipOrCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java index b7260d34f..0eb74f8a5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java @@ -40,6 +40,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp import org.sufficientlysecure.keychain.operations.results.SingletonResult; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4; import org.sufficientlysecure.keychain.util.Log; @@ -143,6 +144,17 @@ public class ImportKeysProxyActivity extends FragmentActivity Log.d(Constants.TAG, "scanned: " + uri); + // example: pgp+transfer: + if (uri != null && uri.getScheme() != null && uri.getScheme().equalsIgnoreCase(Constants.SKT_SCHEME)) { + Intent intent = new Intent(this, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_TRANSFER); + intent.putExtra(TransferFragment.EXTRA_OPENPGP_SKT_INFO, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + finish(); + return; + } + // example: openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282 if (uri == null || uri.getScheme() == null || !uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) { @@ -153,6 +165,7 @@ public class ImportKeysProxyActivity extends FragmentActivity returnResult(intent); return; } + final String fingerprintHex = uri.getEncodedSchemeSpecificPart().toLowerCase(Locale.ENGLISH); if (!fingerprintHex.matches("[a-fA-F0-9]{40}")) { SingletonResult result = new SingletonResult( diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index 13df0b539..015161c9e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -20,6 +20,8 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; +import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -41,6 +43,8 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.remote.ui.AppsListFragment; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferNotAvailableFragment; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Preferences; @@ -50,8 +54,9 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai static final int ID_ENCRYPT_DECRYPT = 2; static final int ID_APPS = 3; static final int ID_BACKUP = 4; - static final int ID_SETTINGS = 5; - static final int ID_HELP = 6; + public static final int ID_TRANSFER = 5; + static final int ID_SETTINGS = 6; + static final int ID_HELP = 7; // both of these are used for instrumentation testing only public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time"; @@ -82,6 +87,11 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai .withIdentifier(ID_APPS).withSelectable(false), new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore) .withIdentifier(ID_BACKUP).withSelectable(false), + new PrimaryDrawerItem().withName(R.string.nav_transfer) + .withIcon(R.drawable.ic_wifi_lock_24dp) + .withIconColorRes(R.color.md_grey_600) + .withIconTintingEnabled(true) + .withIdentifier(ID_TRANSFER).withSelectable(false), new DividerDrawerItem(), new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withSelectable(false), new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withSelectable(false) @@ -105,6 +115,9 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai case ID_BACKUP: onBackupSelected(); break; + case ID_TRANSFER: + onTransferSelected(); + break; case ID_SETTINGS: intent = new Intent(MainActivity.this, SettingsActivity.class); break; @@ -160,13 +173,36 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai case ID_APPS: onAppsSelected(); break; + case ID_TRANSFER: + onTransferSelected(); + break; } } } - private void setFragment(Fragment fragment, boolean addToBackStack) { + @Override + public void onNewIntent(Intent data) { + super.onNewIntent(data); + setIntent(data); + if (data != null && data.hasExtra(EXTRA_INIT_FRAG)) { + // initialize FragmentLayout with KeyListFragment at first + switch (data.getIntExtra(EXTRA_INIT_FRAG, -1)) { + case ID_ENCRYPT_DECRYPT: + onEnDecryptSelected(); + break; + case ID_APPS: + onAppsSelected(); + break; + case ID_TRANSFER: + onTransferSelected(); + break; + } + } + } + + private void setFragment(Fragment fragment, boolean addToBackStack) { FragmentManager fragmentManager = getSupportFragmentManager(); fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); @@ -175,11 +211,12 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai if (addToBackStack) { ft.addToBackStack(null); } + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } - private void onKeysSelected() { + public void onKeysSelected() { mToolbar.setTitle(R.string.app_name); mDrawer.setSelection(ID_KEYS, false); Fragment frag = new KeyListFragment(); @@ -207,6 +244,18 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai setFragment(frag, true); } + private void onTransferSelected() { + mToolbar.setTitle(R.string.nav_transfer); + mDrawer.setSelection(ID_TRANSFER, false); + if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) { + Fragment frag = new TransferNotAvailableFragment(); + setFragment(frag, true); + } else { + Fragment frag = new TransferFragment(); + setFragment(frag, true); + } + } + @Override protected void onSaveInstanceState(Bundle outState) { // add the values which need to be saved from the drawer to the bundle diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java index b811d51b5..26f5dbee7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java @@ -75,7 +75,8 @@ public class SettingsCacheTTLFragment extends Fragment { recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); recyclerView.setAdapter(mAdapter); - recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST)); + recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST, + true)); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java index f7b08944a..670439481 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java @@ -371,6 +371,13 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements startPassphraseActivity(REQUEST_BACKUP); return true; } + case R.id.menu_key_view_skt: { + Intent intent = new Intent(this, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_TRANSFER); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + } case R.id.menu_key_view_delete: { deleteKey(); return true; @@ -405,6 +412,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements public boolean onPrepareOptionsMenu(Menu menu) { MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup); backupKey.setVisible(mIsSecret); + menu.findItem(R.id.menu_key_view_skt).setVisible(mIsSecret); MenuItem changePassword = menu.findItem(R.id.menu_key_change_password); changePassword.setVisible(mIsSecret); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java index 0ddc6b160..b587eb9d3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java @@ -46,7 +46,7 @@ public class IdentitiesCardView extends CardView implements IdentitiesMvpView { vIdentities = (RecyclerView) view.findViewById(R.id.view_key_user_ids); vIdentities.setLayoutManager(new LinearLayoutManager(context)); - vIdentities.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST)); + vIdentities.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, false)); Button userIdsEditButton = (Button) view.findViewById(R.id.view_key_card_user_ids_edit); userIdsEditButton.setOnClickListener(new OnClickListener() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java new file mode 100644 index 000000000..b4b8e4824 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.transfer.loader; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; + + +public class SecretKeyLoader extends AsyncTaskLoader> { + public static final String[] PROJECTION = new String[] { + KeyRings.MASTER_KEY_ID, + KeyRings.CREATION, + KeyRings.NAME, + KeyRings.EMAIL, + KeyRings.HAS_ANY_SECRET + }; + private static final int INDEX_KEY_ID = 0; + private static final int INDEX_CREATION = 1; + private static final int INDEX_NAME = 2; + private static final int INDEX_EMAIL = 3; + + + private final ContentResolver contentResolver; + + private List cachedResult; + + + public SecretKeyLoader(Context context, ContentResolver contentResolver) { + super(context); + + this.contentResolver = contentResolver; + } + + @Override + public List loadInBackground() { + String where = KeyRings.HAS_ANY_SECRET + " = 1"; + Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, where, null, null); + if (cursor == null) { + Log.e(Constants.TAG, "Error loading key items!"); + return null; + } + + try { + ArrayList secretKeyItems = new ArrayList<>(); + while (cursor.moveToNext()) { + SecretKeyItem secretKeyItem = new SecretKeyItem(cursor); + secretKeyItems.add(secretKeyItem); + } + + return Collections.unmodifiableList(secretKeyItems); + } finally { + cursor.close(); + } + } + + @Override + public void deliverResult(List keySubkeyStatus) { + cachedResult = keySubkeyStatus; + + if (isStarted()) { + super.deliverResult(keySubkeyStatus); + } + } + + @Override + protected void onStartLoading() { + if (cachedResult != null) { + deliverResult(cachedResult); + } + + if (takeContentChanged() || cachedResult == null) { + forceLoad(); + } + } + + public static class SecretKeyItem { + final int position; + public final long masterKeyId; + public final long creationMillis; + public final String name; + public final String email; + + SecretKeyItem(Cursor cursor) { + position = cursor.getPosition(); + + masterKeyId = cursor.getLong(INDEX_KEY_ID); + creationMillis = cursor.getLong(INDEX_CREATION) * 1000; + + name = cursor.getString(INDEX_NAME); + email = cursor.getString(INDEX_EMAIL); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java new file mode 100644 index 000000000..40e674a09 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.transfer.presenter; + + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.support.annotation.RequiresApi; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; +import android.support.v7.widget.RecyclerView.Adapter; +import android.view.LayoutInflater; + +import org.openintents.openpgp.util.OpenPgpUtils; +import org.openintents.openpgp.util.OpenPgpUtils.UserId; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.network.KeyTransferInteractor; +import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; +import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.OnClickImportKeyListener; +import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyAdapter; +import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyItem; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.OnClickTransferKeyListener; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.TransferKeyAdapter; +import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; +import org.sufficientlysecure.keychain.util.Log; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks>, + OnClickTransferKeyListener, OnClickImportKeyListener { + private static final String DELIMITER_START = "-----BEGIN PGP PRIVATE KEY BLOCK-----"; + private static final String DELIMITER_END = "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String BACKSTACK_TAG_TRANSFER = "transfer"; + + private final Context context; + private final TransferMvpView view; + private final LoaderManager loaderManager; + private final int loaderId; + private final KeyRepository databaseInteractor; + + private final TransferKeyAdapter secretKeyAdapter; + private final ReceivedKeyAdapter receivedKeyAdapter; + + + private KeyTransferInteractor keyTransferClientInteractor; + private KeyTransferInteractor keyTransferServerInteractor; + + private boolean wasConnected = false; + private boolean sentData = false; + private boolean waitingForWifi = false; + private Long confirmingMasterKeyId; + + + public TransferPresenter(Context context, LoaderManager loaderManager, int loaderId, TransferMvpView view) { + this.context = context; + this.view = view; + this.loaderManager = loaderManager; + this.loaderId = loaderId; + this.databaseInteractor = KeyRepository.createDatabaseInteractor(context); + + secretKeyAdapter = new TransferKeyAdapter(context, LayoutInflater.from(context), this); + view.setSecretKeyAdapter(secretKeyAdapter); + + receivedKeyAdapter = new ReceivedKeyAdapter(context, LayoutInflater.from(context), this); + view.setReceivedKeyAdapter(receivedKeyAdapter); + } + + + public void onUiInitFromIntentUri(final Uri initUri) { + connectionStartConnect(initUri.toString()); + } + + public void onUiStart() { + loaderManager.restartLoader(loaderId, null, this); + + if (keyTransferServerInteractor == null && keyTransferClientInteractor == null && !wasConnected) { + checkWifiResetAndStartListen(); + } + } + + public void onUiStop() { + connectionClear(); + + if (wasConnected) { + view.showViewDisconnected(); + view.dismissConfirmationIfExists(); + secretKeyAdapter.setAllDisabled(true); + } + } + + public void onUiClickScan() { + connectionClear(); + + view.scanQrCode(); + } + + public void onUiClickScanAgain() { + onUiClickScan(); + } + + public void onUiClickDone() { + view.finishFragmentOrActivity(); + } + + public void onUiQrCodeScanned(String qrCodeContent) { + connectionStartConnect(qrCodeContent); + } + + public void onUiBackStackPop() { + if (wasConnected) { + checkWifiResetAndStartListen(); + } + } + + @Override + public void onUiClickTransferKey(long masterKeyId) { + if (sentData) { + prepareAndSendKey(masterKeyId); + } else { + confirmingMasterKeyId = masterKeyId; + view.showConfirmSendDialog(); + } + } + + public void onUiClickConfirmSend() { + if (confirmingMasterKeyId == null) { + return; + } + long masterKeyId = confirmingMasterKeyId; + confirmingMasterKeyId = null; + + prepareAndSendKey(masterKeyId); + } + + @Override + public void onUiClickImportKey(final long masterKeyId, String keyData) { + receivedKeyAdapter.focusItem(masterKeyId); + + final ImportKeyringParcel importKeyringParcel = ImportKeyringParcel.createImportKeyringParcel( + ParcelableKeyRing.createFromEncodedBytes(keyData.getBytes())); + + CryptoOperationHelper op = + view.createCryptoOperationHelper(new Callback() { + @Override + public ImportKeyringParcel createOperationInput() { + return importKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + receivedKeyAdapter.focusItem(null); + receivedKeyAdapter.addToFinishedItems(masterKeyId); + view.releaseCryptoOperationHelper(); + view.showResultNotification(result); + } + + @Override + public void onCryptoOperationCancelled() { + view.releaseCryptoOperationHelper(); + receivedKeyAdapter.focusItem(null); + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + receivedKeyAdapter.focusItem(null); + view.releaseCryptoOperationHelper(); + view.showResultNotification(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }); + + op.cryptoOperation(); + } + + public void onWifiConnected() { + if (waitingForWifi) { + resetAndStartListen(); + } + } + + @Override + public void onServerStarted(String qrCodeData) { + Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse(qrCodeData)); + view.setQrImage(qrCodeBitmap); + } + + @Override + public void onConnectionEstablished(String otherName) { + wasConnected = true; + + secretKeyAdapter.clearFinishedItems(); + secretKeyAdapter.focusItem(null); + secretKeyAdapter.setAllDisabled(false); + receivedKeyAdapter.clear(); + + view.showConnectionEstablished(otherName); + view.setShowDoneIcon(true); + view.addFakeBackStackItem(BACKSTACK_TAG_TRANSFER); + } + + @Override + public void onConnectionLost() { + if (!wasConnected) { + checkWifiResetAndStartListen(); + + view.showErrorConnectionFailed(); + } else { + connectionClear(); + + view.dismissConfirmationIfExists(); + view.showViewDisconnected(); + secretKeyAdapter.setAllDisabled(true); + } + } + + @Override + public void onDataReceivedOk(String receivedData) { + if (sentData) { + Log.d(Constants.TAG, "received data, but we already sent a key! race condition, or other side misbehaving?"); + return; + } + + Log.d(Constants.TAG, "received data"); + UncachedKeyRing uncachedKeyRing; + try { + uncachedKeyRing = UncachedKeyRing.decodeFromData(receivedData.getBytes()); + } catch (PgpGeneralException | IOException | RuntimeException e) { + Log.e(Constants.TAG, "error parsing incoming key", e); + view.showErrorBadKey(); + return; + } + + String primaryUserId = uncachedKeyRing.getPublicKey().getPrimaryUserIdWithFallback(); + UserId userId = OpenPgpUtils.splitUserId(primaryUserId); + + ReceivedKeyItem receivedKeyItem = new ReceivedKeyItem(receivedData, uncachedKeyRing.getMasterKeyId(), + uncachedKeyRing.getCreationTime(), userId.name, userId.email); + receivedKeyAdapter.addItem(receivedKeyItem); + + view.showReceivingKeys(); + } + + @Override + public void onDataSentOk(String passthrough) { + Log.d(Constants.TAG, "data sent ok!"); + final long masterKeyId = Long.parseLong(passthrough); + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + secretKeyAdapter.focusItem(null); + secretKeyAdapter.addToFinishedItems(masterKeyId); + } + }, 750); + } + + @Override + public void onConnectionErrorConnect() { + view.showWaitingForConnection(); + view.showErrorConnectionFailed(); + + resetAndStartListen(); + } + + @Override + public void onConnectionErrorNoRouteToHost(String wifiSsid) { + connectionClear(); + + String ownWifiSsid = getConnectedWifiSsid(); + if (!wifiSsid.equalsIgnoreCase(ownWifiSsid)) { + view.showWifiError(wifiSsid); + } else { + view.showWaitingForConnection(); + view.showErrorConnectionFailed(); + + resetAndStartListen(); + } + } + + @Override + public void onConnectionErrorListen() { + view.showErrorListenFailed(); + } + + @Override + public void onConnectionError(String errorMessage) { + view.showErrorConnectionError(errorMessage); + + connectionClear(); + if (wasConnected) { + view.showViewDisconnected(); + secretKeyAdapter.setAllDisabled(true); + } + } + + private void connectionStartConnect(String qrCodeContent) { + connectionClear(); + + view.showEstablishingConnection(); + + keyTransferClientInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END); + try { + keyTransferClientInteractor.connectToServer(qrCodeContent, TransferPresenter.this); + } catch (URISyntaxException e) { + view.showErrorConnectionFailed(); + } + } + + private void checkWifiResetAndStartListen() { + if (!isWifiConnected()) { + waitingForWifi = true; + view.showNotOnWifi(); + return; + } + + resetAndStartListen(); + } + + private void resetAndStartListen() { + waitingForWifi = false; + wasConnected = false; + sentData = false; + connectionClear(); + + String wifiSsid = getConnectedWifiSsid(); + + keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END); + keyTransferServerInteractor.startServer(this, wifiSsid); + + view.showWaitingForConnection(); + view.setShowDoneIcon(false); + } + + private boolean isWifiConnected() { + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo wifiNetwork = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + + return wifiNetwork.isConnected(); + } + + private String getConnectedWifiSsid() { + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiManager == null) { + return null; + } + WifiInfo info = wifiManager.getConnectionInfo(); + if (info == null) { + return null; + } + // getSSID will return the ssid in quotes if it is valid utf-8. we only return it in that case. + String ssid = info.getSSID(); + if (ssid.charAt(0) != '"') { + return null; + } + return ssid.substring(1, ssid.length() -1); + } + + private void connectionClear() { + if (keyTransferServerInteractor != null) { + keyTransferServerInteractor.closeConnection(); + keyTransferServerInteractor = null; + } + if (keyTransferClientInteractor != null) { + keyTransferClientInteractor.closeConnection(); + keyTransferClientInteractor = null; + } + } + + private void prepareAndSendKey(long masterKeyId) { + try { + byte[] armoredSecretKey = databaseInteractor.getSecretKeyRingAsArmoredData(masterKeyId); + secretKeyAdapter.focusItem(masterKeyId); + connectionSend(armoredSecretKey, Long.toString(masterKeyId)); + } catch (IOException | NotFoundException | PgpGeneralException e) { + // TODO + e.printStackTrace(); + } + } + + private void connectionSend(byte[] armoredSecretKey, String passthrough) { + sentData = true; + if (keyTransferClientInteractor != null) { + keyTransferClientInteractor.sendData(armoredSecretKey, passthrough); + } else if (keyTransferServerInteractor != null) { + keyTransferServerInteractor.sendData(armoredSecretKey, passthrough); + } + } + + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return secretKeyAdapter.createLoader(context); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + secretKeyAdapter.setData(data); + view.setShowSecretKeyEmptyView(data.isEmpty()); + } + + @Override + public void onLoaderReset(Loader> loader) { + secretKeyAdapter.setData(null); + } + + + public interface TransferMvpView { + void showNotOnWifi(); + void showWaitingForConnection(); + void showEstablishingConnection(); + void showConnectionEstablished(String hostname); + + void showWifiError(String wifiSsid); + + void showReceivingKeys(); + + void showViewDisconnected(); + + void scanQrCode(); + void setQrImage(Bitmap qrCode); + + void releaseCryptoOperationHelper(); + + void showErrorBadKey(); + void showErrorConnectionFailed(); + void showErrorListenFailed(); + void showErrorConnectionError(String errorMessage); + void showResultNotification(ImportKeyResult result); + + void setShowDoneIcon(boolean showDoneIcon); + + void setSecretKeyAdapter(Adapter adapter); + void setShowSecretKeyEmptyView(boolean isEmpty); + void setReceivedKeyAdapter(Adapter secretKeyAdapter); + + CryptoOperationHelper createCryptoOperationHelper(Callback callback); + + void addFakeBackStackItem(String tag); + + void finishFragmentOrActivity(); + + void showConfirmSendDialog(); + void dismissConfirmationIfExists(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/ReceivedSecretKeyList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/ReceivedSecretKeyList.java new file mode 100644 index 000000000..5ba1f3517 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/ReceivedSecretKeyList.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.transfer.view; + + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; + + +public class ReceivedSecretKeyList extends RecyclerView { + private static final int STATE_INVISIBLE = 0; + // private static final int STATE_BUTTON = 1; // used in TransferSecretKeyList + private static final int STATE_PROGRESS = 2; + private static final int STATE_TRANSFERRED = 3; + private static final int STATE_IMPORT_BUTTON = 4; + + + public ReceivedSecretKeyList(Context context) { + super(context); + init(context); + } + + public ReceivedSecretKeyList(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ReceivedSecretKeyList(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setLayoutManager(new LinearLayoutManager(context)); + addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, true)); + } + + public static class ReceivedKeyAdapter extends Adapter { + private final Context context; + private final LayoutInflater layoutInflater; + private final OnClickImportKeyListener onClickImportKeyListener; + + private Long focusedMasterKeyId; + private List data = new ArrayList<>(); + private ArrayList finishedItems = new ArrayList<>(); + + + public ReceivedKeyAdapter(Context context, LayoutInflater layoutInflater, + OnClickImportKeyListener onClickImportKeyListener) { + this.context = context; + this.layoutInflater = layoutInflater; + this.onClickImportKeyListener = onClickImportKeyListener; + } + + @Override + public ReceivedKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ReceivedKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false)); + } + + @Override + public void onBindViewHolder(ReceivedKeyViewHolder holder, int position) { + ReceivedKeyItem item = data.get(position); + boolean isFinished = finishedItems.contains(item.masterKeyId); + holder.bind(context, item, onClickImportKeyListener, focusedMasterKeyId, isFinished); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + @Override + public long getItemId(int position) { + return data.get(position).masterKeyId; + } + + public void addToFinishedItems(long masterKeyId) { + finishedItems.add(masterKeyId); + // doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things! + } + + public void focusItem(Long masterKeyId) { + focusedMasterKeyId = masterKeyId; + notifyItemRangeChanged(0, getItemCount()); + } + + public void addItem(ReceivedKeyItem receivedKeyItem) { + data.add(receivedKeyItem); + notifyItemInserted(data.size() -1); + } + + public void clear() { + data.clear(); + finishedItems.clear(); + focusedMasterKeyId = null; + notifyDataSetChanged(); + } + } + + static class ReceivedKeyViewHolder extends ViewHolder { + private final TextView vName; + private final TextView vEmail; + private final TextView vCreation; + private final View vImportButton; + private final ViewAnimator vState; + + ReceivedKeyViewHolder(View itemView) { + super(itemView); + + vName = (TextView) itemView.findViewById(R.id.key_list_item_name); + vEmail = (TextView) itemView.findViewById(R.id.key_list_item_email); + vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + + vImportButton = itemView.findViewById(R.id.button_import); + vState = (ViewAnimator) itemView.findViewById(R.id.transfer_state); + } + + private void bind(Context context, final ReceivedKeyItem item, + final OnClickImportKeyListener onClickReceiveKeyListener, Long focusedMasterKeyId, + boolean isFinished) { + if (item.name != null) { + vName.setText(item.name); + vName.setVisibility(View.VISIBLE); + } else { + vName.setVisibility(View.GONE); + } + if (item.email != null) { + vEmail.setText(item.email); + vEmail.setVisibility(View.VISIBLE); + } else { + vEmail.setVisibility(View.GONE); + } + + String dateTime = DateUtils.formatDateTime(context, item.creationMillis, + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); + vCreation.setText(context.getString(R.string.label_key_created, dateTime)); + + if (focusedMasterKeyId != null) { + if (focusedMasterKeyId != item.masterKeyId) { + itemView.animate().alpha(0.2f).start(); + vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE); + } else { + itemView.setAlpha(1.0f); + vState.setDisplayedChild(STATE_PROGRESS); + } + } else { + itemView.animate().alpha(1.0f).start(); + vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_IMPORT_BUTTON); + } + + if (focusedMasterKeyId == null && onClickReceiveKeyListener != null) { + vImportButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onClickReceiveKeyListener.onUiClickImportKey(item.masterKeyId, item.keyData); + } + }); + } else { + vImportButton.setOnClickListener(null); + } + } + } + + public interface OnClickImportKeyListener { + void onUiClickImportKey(long masterKeyId, String keyData); + } + + public static class ReceivedKeyItem { + private final String keyData; + + private final long masterKeyId; + private final long creationMillis; + private final String name; + private final String email; + + public ReceivedKeyItem(String keyData, long masterKeyId, long creationMillis, String name, String email) { + this.keyData = keyData; + this.masterKeyId = masterKeyId; + this.creationMillis = creationMillis; + this.name = name; + this.email = email; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java new file mode 100644 index 000000000..aa16ea353 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.transfer.view; + + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.OnBackStackChangedListener; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AlertDialog.Builder; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.zxing.client.android.Intents; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.ui.MainActivity; +import org.sufficientlysecure.keychain.ui.QrCodeCaptureActivity; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback; +import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter; +import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.widget.ConnectionStatusView; +import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class TransferFragment extends Fragment implements TransferMvpView { + public static final int REQUEST_CODE_SCAN = 1; + public static final int LOADER_ID = 1; + + public static final String EXTRA_OPENPGP_SKT_INFO = "openpgp_skt_info"; + + + private ImageView vQrCodeImage; + private TransferPresenter presenter; + private ToolableViewAnimator vTransferAnimator; + private TextView vConnectionStatusText1; + private TextView vConnectionStatusText2; + private ConnectionStatusView vConnectionStatusView1; + private ConnectionStatusView vConnectionStatusView2; + private RecyclerView vTransferKeyList; + private View vTransferKeyListEmptyView; + private RecyclerView vReceivedKeyList; + + private CryptoOperationHelper currentCryptoOperationHelper; + private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) { + NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + if (networkInfo != null && networkInfo.isConnected()) { + presenter.onWifiConnected(); + } + } + } + }; + private boolean showDoneIcon; + private AlertDialog confirmationDialog; + private TextView vWifiErrorInstructions; + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.transfer_fragment, container, false); + + vTransferAnimator = (ToolableViewAnimator) view.findViewById(R.id.transfer_animator); + + vConnectionStatusText1 = (TextView) view.findViewById(R.id.connection_status_1); + vConnectionStatusText2 = (TextView) view.findViewById(R.id.connection_status_2); + vConnectionStatusView1 = (ConnectionStatusView) view.findViewById(R.id.connection_status_icon_1); + vConnectionStatusView2 = (ConnectionStatusView) view.findViewById(R.id.connection_status_icon_2); + vTransferKeyList = (RecyclerView) view.findViewById(R.id.transfer_key_list); + vTransferKeyListEmptyView = view.findViewById(R.id.transfer_key_list_empty); + vReceivedKeyList = (RecyclerView) view.findViewById(R.id.received_key_list); + vWifiErrorInstructions = (TextView) view.findViewById(R.id.transfer_wifi_error_instructions); + + vQrCodeImage = (ImageView) view.findViewById(R.id.qr_code_image); + + view.findViewById(R.id.button_scan).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (presenter != null) { + presenter.onUiClickScan(); + } + } + }); + + view.findViewById(R.id.button_scan_again).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (presenter != null) { + presenter.onUiClickScanAgain(); + } + } + }); + + presenter = new TransferPresenter(getContext(), getLoaderManager(), LOADER_ID, this); + + setHasOptionsMenu(true); + + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null) { + return; + } + + Intent activityIntent = getActivity().getIntent(); + if (activityIntent != null && activityIntent.hasExtra(EXTRA_OPENPGP_SKT_INFO)) { + presenter.onUiInitFromIntentUri(activityIntent.getParcelableExtra(EXTRA_OPENPGP_SKT_INFO)); + } + } + + @Override + public void onStart() { + super.onStart(); + + presenter.onUiStart(); + } + + @Override + public void onResume() { + super.onResume(); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + getContext().registerReceiver(broadcastReceiver, intentFilter); + } + + @Override + public void onPause() { + super.onPause(); + + getContext().unregisterReceiver(broadcastReceiver); + } + + @Override + public void onStop() { + super.onStop(); + + presenter.onUiStop(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (showDoneIcon) { + inflater.inflate(R.menu.transfer_menu, menu); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_done) { + presenter.onUiClickDone(); + return true; + } + return false; + } + + @Override + public void showNotOnWifi() { + vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_no_wifi); + } + + @Override + public void showWaitingForConnection() { + vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_waiting); + } + + @Override + public void showEstablishingConnection() { + vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connecting); + } + + @Override + public void showConnectionEstablished(String hostname) { + // String statusText = getString(R.string.transfer_status_connected, hostname); + + vConnectionStatusText1.setText(R.string.transfer_status_connected); + vConnectionStatusText2.setText(R.string.transfer_status_connected); + + vConnectionStatusView1.setConnected(true); + vConnectionStatusView2.setConnected(true); + + vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connected); + } + + @Override + public void showWifiError(String wifiSsid) { + vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_wifi_error); + + if (!TextUtils.isEmpty(wifiSsid)) { + vWifiErrorInstructions + .setText(getResources().getString(R.string.transfer_error_wifi_text_instructions_ssid, wifiSsid)); + } else { + vWifiErrorInstructions.setText(R.string.transfer_error_wifi_text_instructions); + } + } + + @Override + public void showReceivingKeys() { + vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_passive); + } + + @Override + public void showViewDisconnected() { + vConnectionStatusText1.setText(R.string.transfer_status_disconnected); + vConnectionStatusText2.setText(R.string.transfer_status_disconnected); + + vConnectionStatusView1.setConnected(false); + vConnectionStatusView2.setConnected(false); + } + + @Override + public void setQrImage(final Bitmap qrCode) { + vQrCodeImage.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int viewSize = vQrCodeImage.getWidth(); + if (viewSize == 0) { + return; + } + // create actual bitmap in display dimensions + Bitmap scaled = Bitmap.createScaledBitmap(qrCode, viewSize, viewSize, false); + vQrCodeImage.setImageBitmap(scaled); + } + }); + vQrCodeImage.requestLayout(); + } + + @Override + public void scanQrCode() { + Intent intent = new Intent(getActivity(), QrCodeCaptureActivity.class); + startActivityForResult(intent, REQUEST_CODE_SCAN); + } + + @Override + public void setShowDoneIcon(boolean showDoneIcon) { + this.showDoneIcon = showDoneIcon; + FragmentActivity activity = getActivity(); + if (activity != null) { + activity.invalidateOptionsMenu(); + } + } + + @Override + public void setSecretKeyAdapter(Adapter adapter) { + vTransferKeyList.setAdapter(adapter); + } + + @Override + public void setShowSecretKeyEmptyView(boolean isEmpty) { + vTransferKeyListEmptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); + } + + @Override + public void setReceivedKeyAdapter(Adapter adapter) { + vReceivedKeyList.setAdapter(adapter); + } + + @Override + public CryptoOperationHelper createCryptoOperationHelper(Callback callback) { + CryptoOperationHelper cryptoOperationHelper = new CryptoOperationHelper<>(1, this, callback, null); + currentCryptoOperationHelper = cryptoOperationHelper; + return cryptoOperationHelper; + } + + @Override + public void releaseCryptoOperationHelper() { + currentCryptoOperationHelper = null; + } + + @Override + public void showErrorBadKey() { + Notify.create(getActivity(), R.string.transfer_error_read_incoming, Style.ERROR).show(); + } + + @Override + public void showErrorConnectionFailed() { + Notify.create(getActivity(), R.string.transfer_error_connect, Style.ERROR).show(); + } + + @Override + public void showErrorListenFailed() { + Notify.create(getActivity(), R.string.transfer_error_listen, Style.ERROR).show(); + } + + @Override + public void showErrorConnectionError(String errorMessage) { + if (errorMessage != null) { + String text = getString(R.string.transfer_error_generic_msg, errorMessage); + Notify.create(getActivity(), text, Style.ERROR).show(); + } else { + Notify.create(getActivity(), R.string.transfer_error_generic, Style.ERROR).show(); + } + } + + @Override + public void showResultNotification(ImportKeyResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public void addFakeBackStackItem(final String tag) { + FragmentManager fragmentManager = getFragmentManager(); + + fragmentManager.beginTransaction() + .addToBackStack(tag) + .commitAllowingStateLoss(); + fragmentManager.executePendingTransactions(); + + fragmentManager.addOnBackStackChangedListener(new OnBackStackChangedListener() { + @Override + public void onBackStackChanged() { + FragmentManager fragMan = getFragmentManager(); + fragMan.popBackStack(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE); + fragMan.removeOnBackStackChangedListener(this); + + presenter.onUiBackStackPop(); + } + }); + } + + @Override + public void finishFragmentOrActivity() { + FragmentActivity activity = getActivity(); + if (activity != null) { + if (activity instanceof MainActivity) { + ((MainActivity) activity).onKeysSelected(); + } else { + activity.finish(); + } + } + } + + @Override + public void showConfirmSendDialog() { + if (confirmationDialog != null) { + return; + } + confirmationDialog = new Builder(getContext()) + .setTitle(R.string.transfer_confirm_title) + .setMessage(R.string.transfer_confirm_text) + .setPositiveButton(R.string.transfer_confirm_ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + if (whichButton == DialogInterface.BUTTON_POSITIVE) { + presenter.onUiClickConfirmSend(); + } else { + dialog.dismiss(); + } + } + }) + .setNegativeButton(R.string.transfer_confirm_cancel, null) + .setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + confirmationDialog = null; + } + }) + .create(); + confirmationDialog.show(); + } + + @Override + public void dismissConfirmationIfExists() { + if (confirmationDialog != null && confirmationDialog.isShowing()) { + confirmationDialog.dismiss(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (currentCryptoOperationHelper != null && + currentCryptoOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + return; + } + + switch (requestCode) { + case REQUEST_CODE_SCAN: + if (resultCode == Activity.RESULT_OK) { + String qrCodeData = data.getStringExtra(Intents.Scan.RESULT); + presenter.onUiQrCodeScanned(qrCodeData); + } + break; + default: + super.onActivityResult(requestCode, resultCode, data); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferNotAvailableFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferNotAvailableFragment.java new file mode 100644 index 000000000..25e0058fd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferNotAvailableFragment.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.transfer.view; + + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.keychain.R; + + +public class TransferNotAvailableFragment extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.transfer_not_available_fragment, container, false); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java new file mode 100644 index 000000000..1f8407309 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.transfer.view; + + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v4.content.Loader; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; + + +public class TransferSecretKeyList extends RecyclerView { + private static final int STATE_INVISIBLE = 0; + private static final int STATE_BUTTON = 1; + private static final int STATE_PROGRESS = 2; + private static final int STATE_TRANSFERRED = 3; + // private static final int STATE_IMPORT_BUTTON = 4; // used in ReceivedSecretKeyList + + + public TransferSecretKeyList(Context context) { + super(context); + init(context); + } + + public TransferSecretKeyList(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public TransferSecretKeyList(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setLayoutManager(new LinearLayoutManager(context)); + addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, true)); + setItemAnimator(null); + } + + public static class TransferKeyAdapter extends RecyclerView.Adapter { + private final Context context; + private final LayoutInflater layoutInflater; + private final OnClickTransferKeyListener onClickTransferKeyListener; + + private Long focusedMasterKeyId; + private List data; + private ArrayList finishedItems = new ArrayList<>(); + private boolean allItemsDisabled; + + + public TransferKeyAdapter(Context context, LayoutInflater layoutInflater, + OnClickTransferKeyListener onClickTransferKeyListener) { + this.context = context; + this.layoutInflater = layoutInflater; + this.onClickTransferKeyListener = onClickTransferKeyListener; + } + + @Override + public TransferKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new TransferKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false)); + } + + @Override + public void onBindViewHolder(TransferKeyViewHolder holder, int position) { + SecretKeyItem item = data.get(position); + boolean isFinished = finishedItems.contains(item.masterKeyId); + holder.bind(context, item, onClickTransferKeyListener, focusedMasterKeyId, isFinished, allItemsDisabled); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + @Override + public long getItemId(int position) { + return data.get(position).masterKeyId; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + public void clearFinishedItems() { + finishedItems.clear(); + notifyItemRangeChanged(0, getItemCount()); + } + + public void addToFinishedItems(long masterKeyId) { + finishedItems.add(masterKeyId); + // doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things! + } + + public void focusItem(Long masterKeyId) { + focusedMasterKeyId = masterKeyId; + notifyItemRangeChanged(0, getItemCount()); + } + + public Loader> createLoader(Context context) { + return new SecretKeyLoader(context, context.getContentResolver()); + } + + public void setAllDisabled(boolean allItemsdisablde) { + allItemsDisabled = allItemsdisablde; + notifyItemRangeChanged(0, getItemCount()); + } + } + + static class TransferKeyViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vEmail; + private final TextView vCreation; + private final View vSendButton; + private final ViewAnimator vState; + + TransferKeyViewHolder(View itemView) { + super(itemView); + + vName = (TextView) itemView.findViewById(R.id.key_list_item_name); + vEmail = (TextView) itemView.findViewById(R.id.key_list_item_email); + vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + + vSendButton = itemView.findViewById(R.id.button_transfer); + vState = (ViewAnimator) itemView.findViewById(R.id.transfer_state); + } + + private void bind(Context context, final SecretKeyItem item, + final OnClickTransferKeyListener onClickTransferKeyListener, Long focusedMasterKeyId, + boolean isFinished, boolean disableAll) { + if (item.name != null) { + vName.setText(item.name); + vName.setVisibility(View.VISIBLE); + } else { + vName.setVisibility(View.GONE); + } + if (item.email != null) { + vEmail.setText(item.email); + vEmail.setVisibility(View.VISIBLE); + } else { + vEmail.setVisibility(View.GONE); + } + + String dateTime = DateUtils.formatDateTime(context, item.creationMillis, + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); + vCreation.setText(context.getString(R.string.label_key_created, dateTime)); + + if (disableAll) { + itemView.setAlpha(0.2f); + vState.setDisplayedChild(STATE_INVISIBLE); + vSendButton.setOnClickListener(null); + return; + } + + if (focusedMasterKeyId != null) { + if (focusedMasterKeyId != item.masterKeyId) { + itemView.animate().alpha(0.2f).start(); + vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE); + } else { + itemView.setAlpha(1.0f); + vState.setDisplayedChild(STATE_PROGRESS); + } + } else { + itemView.animate().alpha(1.0f).start(); + vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_BUTTON); + } + + if (focusedMasterKeyId == null && onClickTransferKeyListener != null) { + vSendButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onClickTransferKeyListener.onUiClickTransferKey(item.masterKeyId); + } + }); + } else { + vSendButton.setOnClickListener(null); + } + } + } + + public interface OnClickTransferKeyListener { + void onUiClickTransferKey(long masterKeyId); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java index a6394a3fb..dad6fe2c2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java @@ -41,6 +41,10 @@ import java.util.Locale; */ public class QrCodeUtils { + public static Bitmap getQRCodeBitmap(final Uri uri) { + return getQRCodeBitmap(uri.toString(), 0); + } + public static Bitmap getQRCodeBitmap(final Uri uri, final int size) { // for URIs we want alphanumeric encoding to save space, thus make everything upper case! // zxing will then select Mode.ALPHANUMERIC internally diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java index 2af4e062d..d31243c40 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java @@ -34,12 +34,14 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; + private final boolean showAfterLast; private Drawable mDivider; private int mOrientation; - public DividerItemDecoration(Context context, int orientation) { + public DividerItemDecoration(Context context, int orientation, boolean showAfterLast) { + this.showAfterLast = showAfterLast; final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); @@ -66,8 +68,11 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); - final int childCount = parent.getChildCount(); - for (int i = 0; i < childCount -1; i++) { + int childCount = parent.getChildCount(); + if (!showAfterLast) { + childCount -= 1; + } + for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ConnectionStatusView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ConnectionStatusView.java new file mode 100644 index 000000000..9bceea5bf --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ConnectionStatusView.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + + +public class ConnectionStatusView extends View { + private static final int ARC_COUNT = 3; + public static final int COLOR_CONNECTED = 0xff394baf; + public static final int COLOR_DISCONNECTED = 0xffcccccc; + + + private Arc[] arcs; + private ValueAnimator[] animators; + private boolean isConnected = false; + + + public ConnectionStatusView(Context context) { + super(context); + } + + public ConnectionStatusView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ConnectionStatusView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + initializeObjects(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int measuredWidth = resolveSize(150, widthMeasureSpec); + final int measuredHeight = resolveSize(150, heightMeasureSpec); + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + public void onDraw(Canvas canvas) { + for (int i = 0; i < ARC_COUNT; i++) { + Arc arc = arcs[i]; + canvas.drawArc(arc.oval, 225, 90, false, arc.paint); + } + + if (isConnected != isAnimationInitiated()) { + resetAnimations(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + cancelAnimations(); + } + + public void setConnected(boolean isConnected) { + this.isConnected = isConnected; + + if (arcs != null) { + for (int i = 0; i < ARC_COUNT; i++) { + arcs[i].paint.setColor(isConnected ? COLOR_CONNECTED : COLOR_DISCONNECTED); + } + } + + invalidate(); + } + + private void resetAnimations() { + if (isConnected != isAnimationInitiated()) { + post(new Runnable() { + @Override + public void run() { + if (isConnected) { + setupAnimations(); + } else { + cancelAnimations(); + } + } + }); + } + } + + private boolean isAnimationInitiated() { + return animators != null; + } + + private void setupAnimations() { + if (isAnimationInitiated()) { + return; + } + + animators = new ValueAnimator[ARC_COUNT]; + for (int i = 0; i < ARC_COUNT; i++) { + final int index = i; + ValueAnimator animator = ValueAnimator.ofInt(100, 255, 100); + animator.setRepeatCount(ValueAnimator.INFINITE); + animator.setDuration(2000); + animator.setStartDelay(i * 300); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + arcs[index].paint.setAlpha((int) animation.getAnimatedValue()); + invalidate(); + } + }); + animator.start(); + + animators[i] = animator; + } + } + + private void cancelAnimations() { + if (!isAnimationInitiated()) { + return; + } + + for (int i = 0; i < ARC_COUNT; i++) { + animators[i].cancel(); + } + animators = null; + } + + private void initializeObjects() { + int width = getWidth(); + int height = getHeight(); + float centerX = width / 2.0f; + float centerY = height / 2.0f; + float r = Math.min(width, height) / 2f; + + arcs = new Arc[ARC_COUNT]; + for (int i = 0; i < ARC_COUNT; i++) { + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(r / 10f); + paint.setColor(isConnected ? COLOR_CONNECTED : COLOR_DISCONNECTED); + + float d = r / 4 + i * r / 4; + RectF oval = new RectF(centerX - d, centerY - d + r / 3, centerX + d, centerY + d + r / 3); + + arcs[i] = new Arc(paint, oval); + } + } + + private static class Arc { + private final Paint paint; + private final RectF oval; + + Arc(Paint paint, RectF oval) { + this.paint = paint; + this.oval = oval; + } + } +} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java index a8274e45a..172009d24 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java @@ -99,4 +99,21 @@ public class ToolableViewAnimator extends ViewAnimator { setOutAnimation(savedOutAnim); } + public void setDisplayedChildId(int id) { + if (getDisplayedChildId() == id) { + return; + } + for (int i = 0, count = getChildCount(); i < count; i++) { + if (getChildAt(i).getId() == id) { + setDisplayedChild(i); + return; + } + } + String name = getResources().getResourceEntryName(id); + throw new IllegalArgumentException("No view with ID " + name); + } + + public int getDisplayedChildId() { + return getChildAt(getDisplayedChild()).getId(); + } } diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png new file mode 100644 index 000000000..e5c94abed Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_android_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png new file mode 100644 index 000000000..7bb1015d4 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_live_help_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png new file mode 100644 index 000000000..efb0c3432 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_send_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png new file mode 100644 index 000000000..3881c3f1b Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_lock_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png new file mode 100644 index 000000000..fae3d36c9 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_off_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_question_96dp.png new file mode 100644 index 000000000..36dbfcfcc Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_wifi_question_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png new file mode 100644 index 000000000..376485ca1 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_android_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png new file mode 100644 index 000000000..1fe391018 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_live_help_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png new file mode 100644 index 000000000..e352a5495 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_send_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png new file mode 100644 index 000000000..5d4b657d3 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_lock_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png new file mode 100644 index 000000000..76a879e75 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_off_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_question_96dp.png new file mode 100644 index 000000000..335547b9c Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_wifi_question_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png new file mode 100644 index 000000000..d423e1ed1 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_android_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png new file mode 100644 index 000000000..db50954cb Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_live_help_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png new file mode 100644 index 000000000..eb7220029 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_send_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png new file mode 100644 index 000000000..621506209 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_lock_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png new file mode 100644 index 000000000..bd0adcd05 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_off_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_question_96dp.png new file mode 100644 index 000000000..7e200fdd9 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_wifi_question_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png new file mode 100644 index 000000000..a7071a8aa Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_android_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png new file mode 100644 index 000000000..ac256838a Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_live_help_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png new file mode 100644 index 000000000..65fdc289d Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_send_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png new file mode 100644 index 000000000..e80019793 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_lock_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png new file mode 100644 index 000000000..e4e09119f Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_off_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_question_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_question_96dp.png new file mode 100644 index 000000000..5300ff766 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_wifi_question_96dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png new file mode 100644 index 000000000..5d4b0f3fc Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_live_help_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png new file mode 100644 index 000000000..43a1b4899 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_send_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png new file mode 100644 index 000000000..447c5d155 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_wifi_lock_24dp.png differ diff --git a/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml b/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml index e6e92c451..6b8d4a027 100644 --- a/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml +++ b/OpenKeychain/src/main/res/layout/create_key_start_fragment.xml @@ -115,6 +115,24 @@ android:clickable="true" style="?android:attr/borderlessButtonStyle" /> + + + + + + + + + + + + + + + + + + + + + + + + +