/* * Copyright (C) 2012 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.example.google.tv.anymotelibrary.connection; import com.google.polo.exception.PoloException; import com.google.polo.pairing.ClientPairingSession; import com.google.polo.pairing.PairingContext; import com.google.polo.pairing.PairingListener; import com.google.polo.pairing.PairingSession; import com.google.polo.pairing.message.EncodingOption; import com.google.polo.ssl.DummySSLSocketFactory; import com.google.polo.wire.PoloWireInterface; import com.google.polo.wire.WireFormat; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Looper; import android.util.Log; import com.example.google.tv.anymotelibrary.client.AnymoteSender; import com.example.google.tv.anymotelibrary.client.AnymoteClientService.ClientListener; import java.io.IOException; import java.net.ConnectException; import java.net.Socket; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; /** * This task covers entire connection mechanism, including pairing, when * necessary. */ public class ConnectingTask extends Thread { private static final String REMOTE_NAME = Build.MANUFACTURER + " " + Build.MODEL; private static final int RECONNECTION_DELAY_MS = 1000; private static final int MAX_CONNECTION_ATTEMPTS = 3; private static final String LOG_TAG = "ConnectingActivity"; private final Object secretSync; private final AnymoteSender anymoteProxy; private final KeyStoreManager keyStore; private TvDevice target; private ConnectionListener listener; private boolean isCancelled; private String secret; private SSLSocket sslsock; private Context context; /** * Connection status enumeration. */ public enum ConnectionStatus { /** * Connection successful. */ SUCCESS, /** * Error while creating socket or establishing connection. */ ERROR, /** * Error during SSL handshake. */ NEEDS_PAIRING } /** * Device pairing result. */ public enum PairingStatus { /** * Pairing successful */ PAIRING_SUCCESS, /** * Pairing failed due to connection issues */ FAILED_CONNECTION, /** * Pairing failed due to secret mismatch */ FAILED_SECRET, /** * User cancelled pairing */ FAILED_CANCELLED, } /** * Connection listener. This listener receives all calls when connection * state changes. */ public interface ConnectionListener { /** * Connection to target device has been established. * * @param device the device to which we connected. * @param anymoteProxy */ void onConnected(TvDevice device, AnymoteSender anymoteProxy); /** * Connection to target device failed. */ void onConnectionFailed(); /** * Target device requires secret to continue connecting. * * @param pinListener */ void onSecretRequired(PairingPINDialogBuilder.PinListener pinListener); /** * Called when pairing session is started. */ void onConnectionPairing(); /** * Connection to target device disconnected. */ void onConnectionDisconnected(); } /** * Constructor. * * @param device target device to which we want to connect to. * @param keystoreManager key store manager for maintaining server/client * certificates. * @param context context of the foreground Activity which wants to send * events to the server */ public ConnectingTask(TvDevice device, KeyStoreManager keystoreManager, Context context) { this.context = context; target = device; isCancelled = false; secretSync = new Object(); secret = null; keyStore = keystoreManager; anymoteProxy = new AnymoteSender(this); } /** * Initialize background connection; notify the listener about results. */ @Override public void run() { Looper.prepare(); boolean state = connect(); state = anymoteProxy.attemptToConnect(sslsock); if (isCancelled) { disconnect(); } else { if (listener != null) { if (state) { listener.onConnected(target, anymoteProxy); } else { listener.onConnectionFailed(); } } } } /** * Loops to connect to the server until connection is established or max * allowed attempts are made. * * @return true, if connection succeeded. */ protected boolean connect() { PairingStatus pairingStatus = attemptToPair(new PairingListenerImpl()); if (pairingStatus != PairingStatus.PAIRING_SUCCESS) { Log.i(LOG_TAG, "Pairing failed"); return false; } for (int connectionAttempt = 0; connectionAttempt < MAX_CONNECTION_ATTEMPTS;) { /* * wait on every next iteration; placed here so we don't wait after * final one */ try { if (connectionAttempt > 0) { // Give server time to accept connection if we just paired Thread.sleep(RECONNECTION_DELAY_MS); } } catch (InterruptedException e) { return false; } if (isCancelled) { return false; } if (attemptToConnect() == ConnectionStatus.SUCCESS) { Log.i(LOG_TAG, "Connected to " + target.toString()); return true; } connectionAttempt++; } Log.i(LOG_TAG, "Connection failed"); return false; } /** * Attempts to establish pairing with the server. * * @param listener Listener for device pairing state. * @return PairingStatus device pairing result. */ public PairingStatus attemptToPair(final PairingListenerImpl listener) { PairingStatus result = PairingStatus.FAILED_CONNECTION; SSLSocketFactory socketFactory; SSLSocket socket; PairingContext context; try { try { socketFactory = DummySSLSocketFactory.fromKeyManagers(keyStore.getKeyManagers()); } catch (GeneralSecurityException e) { throw new IllegalStateException("Cannot build socket factory", e); } Socket s = new java.net.Socket(target.getAddress().getHostAddress(), target.getPort() + 1); socket = (SSLSocket) socketFactory.createSocket( s, target.getAddress().getHostAddress(), target.getPort() + 1, true); context = PairingContext.fromSslSocket(socket, false); PoloWireInterface protocol = WireFormat.PROTOCOL_BUFFERS.getWireInterface(context); ClientPairingSession pairingSession = new ClientPairingSession(protocol, context, "AnyMote", REMOTE_NAME); EncodingOption hexEnc = new EncodingOption(EncodingOption.EncodingType.ENCODING_HEXADECIMAL, 4); pairingSession.addInputEncoding(hexEnc); pairingSession.addOutputEncoding(hexEnc); boolean ret = pairingSession.doPair(listener); if (ret) { keyStore.storeCertificate(context.getServerCertificate()); result = PairingStatus.PAIRING_SUCCESS; } else { if (listener.isFailedSecret()) { result = PairingStatus.FAILED_SECRET; } else { result = PairingStatus.FAILED_CANCELLED; } } } catch (UnknownHostException e) { Log.e(LOG_TAG, "Unknown host. Failed to connect", e); result = PairingStatus.FAILED_CONNECTION; } catch (PoloException e) { Log.e(LOG_TAG, "Polo exception", e); result = PairingStatus.FAILED_CONNECTION; } catch (IOException e) { Log.e(LOG_TAG, "Failed to connect", e); result = PairingStatus.FAILED_CONNECTION; } return result; } /** * Cancel current connection. */ public void cancel() { disconnect(); // Interrupt thread in case it's pending on pairing code. synchronized (this) { this.interrupt(); } isCancelled = true; } /** * Attach the connection listener to this task. * * @param listener the listener to be called. */ public void setConnectionListener(ConnectionListener listener) { this.listener = listener; } /** * Set secret (PIN, passphrase) which is required for pairing devices. This * method is called when the user enters secret code in the * PairingPinDialog. * * @param secret the secret passphrase provided by user. */ public void setSecret(String secret) { this.secret = secret; synchronized (secretSync) { secretSync.notify(); } } /** * Service lost existing connection. */ public void onConnectionDisconnected() { disconnect(); if (listener != null) { listener.onConnectionDisconnected(); } } /** * Attempts to establish connection the Anymote server. * * @return result of connection attempt. */ public ConnectionStatus attemptToConnect() { ConnectionStatus status = ConnectionStatus.ERROR; try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyStore.getKeyManagers(), keyStore.getTrustManagers(), null); SSLSocketFactory factory = sslContext.getSocketFactory(); sslsock = (SSLSocket) factory.createSocket( target.getAddress().getHostAddress(), target.getPort()); sslsock.setUseClientMode(true); sslsock.setKeepAlive(true); sslsock.setTcpNoDelay(true); sslsock.startHandshake(); if (sslsock.isConnected()) { status = ConnectionStatus.SUCCESS; } } catch (NoSuchAlgorithmException e) { status = ConnectionStatus.ERROR; } catch (KeyManagementException e) { status = ConnectionStatus.ERROR; } catch (SSLException e) { Log.e(LOG_TAG, "(SSL) Could not create socket to " + target.getName(), e); status = ConnectionStatus.NEEDS_PAIRING; } catch (ConnectException e) { Log.e(LOG_TAG, "(IOE) Could not create socket to " + target.getName(), e); status = ConnectionStatus.ERROR; } catch (IOException e) { if (e.getMessage().startsWith("SSL handshake")) { Log.e(LOG_TAG, "(IOE) SSL handshake failed while connecting to " + target.getName(), e); status = ConnectionStatus.NEEDS_PAIRING; } else { Log.e(LOG_TAG, "(IOE) Could not create socket to " + target.getName(), e); status = ConnectionStatus.ERROR; } } if (status != ConnectionStatus.SUCCESS) { if (sslsock != null && sslsock.isConnected()) { try { sslsock.close(); } catch (IOException e) { Log.e(LOG_TAG, "(IOE) Could not close socket", e); } } sslsock = null; } return status; } /** * Disconnect from the Anymote server. */ public void disconnect() { new Thread(new Runnable() { @Override public void run() { if (anymoteProxy != null) { anymoteProxy.destroy(); } try { if (sslsock != null) { sslsock.close(); } } catch (IOException e) { Log.e(LOG_TAG, "(IOE) Failed to close socket", e); } sslsock = null; } }).start(); } /** * Listens for events sent during the pairing session. pairing listener */ private class PairingListenerImpl implements PairingListener, PairingPINDialogBuilder.PinListener { boolean failedSecret; private String secret; private static final int SECRET_WAIT_TIMEOUT_MS = 60 * 1000; private final Object secretSync; public PairingListenerImpl() { secretSync = new Object(); failedSecret = false; } public boolean isFailedSecret() { return failedSecret; } public void onSessionEnded(PairingSession session) { Log.d(LOG_TAG, "onSessionEnded: " + session); } public void onSessionCreated(PairingSession session) { Log.d(LOG_TAG, "onSessionCreated: " + session); } public void onPerformOutputDeviceRole(PairingSession session, byte[] gamma) { Log.d(LOG_TAG, "onPerformOutputDeviceRole: " + session + ", " + session.getEncoder().encodeToString(gamma)); } public void onPerformInputDeviceRole(PairingSession session) { Looper.prepare(); // this listener is implemented by the main Activity which // shows Pairing PIN dialog to the user to enter secret code. listener.onSecretRequired(this); // wait for user to enter secret code. synchronized (secretSync) { try { secretSync.wait(SECRET_WAIT_TIMEOUT_MS); } catch (InterruptedException e) { // secret is already null. } } // check if the secret was entered correctly. Log.d(LOG_TAG, "Got: " + secret); if (secret != null && secret.length() > 0) { try { byte[] secretBytes = session.getEncoder().decodeToBytes(secret); session.setSecret(secretBytes); failedSecret = !session.hasSucceeded(); } catch (IllegalArgumentException exception) { Log.d(LOG_TAG, "Exception while decoding secret: ", exception); session.teardown(); } catch (IllegalStateException exception) { // ISE may be thrown when session is currently terminating Log.d(LOG_TAG, "Exception while setting secret: ", exception); session.teardown(); } } else { session.teardown(); } } public void onLogMessage(LogLevel level, String message) { Log.d(LOG_TAG, "Log: " + message + " (" + level + ")"); } @Override public void onCancel() { } @Override public void onSecretEntered(String secret) { this.secret = secret; synchronized (secretSync) { secretSync.notify(); } } } /** * Returns the version number as defined in Android manifest. * {@code versionCode} * * @return versionCode */ public int getVersionCode() { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { Log.d(LOG_TAG, "cannot retrieve version number, package name not found"); } return -1; } }