Mercurial > 510Connectbot
diff app/src/main/java/com/five_ten_sg/connectbot/service/TerminalManager.java @ 438:d29cce60f393
migrate from Eclipse to Android Studio
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Thu, 03 Dec 2015 11:23:55 -0800 |
parents | src/com/five_ten_sg/connectbot/service/TerminalManager.java@ebcb4aea03ec |
children | 2cc170e3fc9b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/src/main/java/com/five_ten_sg/connectbot/service/TerminalManager.java Thu Dec 03 11:23:55 2015 -0800 @@ -0,0 +1,730 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * 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.five_ten_sg.connectbot.service; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Timer; +import java.util.TimerTask; + +import com.five_ten_sg.connectbot.R; +import com.five_ten_sg.connectbot.bean.HostBean; +import com.five_ten_sg.connectbot.bean.PubkeyBean; +import com.five_ten_sg.connectbot.transport.TransportFactory; +import com.five_ten_sg.connectbot.util.HostDatabase; +import com.five_ten_sg.connectbot.util.PreferenceConstants; +import com.five_ten_sg.connectbot.util.PubkeyDatabase; +import com.five_ten_sg.connectbot.util.PubkeyUtils; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.AssetFileDescriptor; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Vibrator; +import android.preference.PreferenceManager; +import android.util.Log; + +/** + * Manager for SSH connections that runs as a service. This service holds a list + * of currently connected SSH bridges that are ready for connection up to a GUI + * if needed. + * + * @author jsharkey + */ +public class TerminalManager extends Service implements BridgeDisconnectedListener, OnSharedPreferenceChangeListener { + public final static String TAG = "ConnectBot.TerminalManager"; + + public List<TerminalBridge> bridges = new LinkedList<TerminalBridge>(); + public Map<HostBean, WeakReference<TerminalBridge>> mHostBridgeMap = + new HashMap<HostBean, WeakReference<TerminalBridge>>(); + public Map<String, WeakReference<TerminalBridge>> mNicknameBridgeMap = + new HashMap<String, WeakReference<TerminalBridge>>(); + + public TerminalBridge defaultBridge = null; + + public List<HostBean> disconnected = new LinkedList<HostBean>(); + + public Handler disconnectHandler = null; + + public Map<String, KeyHolder> loadedKeypairs = new HashMap<String, KeyHolder>(); + + public Resources res; + + public HostDatabase hostdb; + public PubkeyDatabase pubkeydb; + + protected SharedPreferences prefs; + + final private IBinder binder = new TerminalBinder(); + + private ConnectivityReceiver connectivityManager; + private ConnectionNotifier connectionNotifier = new ConnectionNotifier(); + + private MediaPlayer mediaPlayer; + + private Timer pubkeyTimer; + + private Timer idleTimer; + private final long IDLE_TIMEOUT = 300000; // 5 minutes + + private Vibrator vibrator; + private volatile boolean wantKeyVibration; + public static final long VIBRATE_DURATION = 30; + + private boolean wantBellVibration; + + private boolean resizeAllowed = true; + + private int fullScreen = 0; + + private boolean savingKeys; + + protected List<WeakReference<TerminalBridge>> mPendingReconnect + = new LinkedList<WeakReference<TerminalBridge>>(); + + public boolean hardKeyboardHidden; + + @Override + public void onCreate() { + Log.i(TAG, "Starting service"); + prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + res = getResources(); + pubkeyTimer = new Timer("pubkeyTimer", true); + hostdb = new HostDatabase(this); + pubkeydb = new PubkeyDatabase(this); + // load all marked pubkeys into memory + updateSavingKeys(); + List<PubkeyBean> pubkeys = pubkeydb.getAllStartPubkeys(); + + for (PubkeyBean pubkey : pubkeys) { + try { + PrivateKey privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType()); + PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); + KeyPair pair = new KeyPair(pubKey, privKey); + addKey(pubkey, pair); + } + catch (Exception e) { + Log.d(TAG, String.format("Problem adding key '%s' to in-memory cache", pubkey.getNickname()), e); + } + } + + vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + wantKeyVibration = prefs.getBoolean(PreferenceConstants.BUMPY_ARROWS, true); + wantBellVibration = prefs.getBoolean(PreferenceConstants.BELL_VIBRATE, true); + enableMediaPlayer(); + hardKeyboardHidden = (res.getConfiguration().hardKeyboardHidden == + Configuration.HARDKEYBOARDHIDDEN_YES); + final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true); + connectivityManager = new ConnectivityReceiver(this, lockingWifi); + } + + private void updateSavingKeys() { + savingKeys = prefs.getBoolean(PreferenceConstants.MEMKEYS, true); + } + + @Override + public void onDestroy() { + Log.i(TAG, "Destroying service"); + disconnectAll(true); + + if (hostdb != null) { + hostdb.close(); + hostdb = null; + } + + if (pubkeydb != null) { + pubkeydb.close(); + pubkeydb = null; + } + + synchronized (this) { + if (idleTimer != null) + idleTimer.cancel(); + + if (pubkeyTimer != null) + pubkeyTimer.cancel(); + } + + connectivityManager.cleanup(); + connectionNotifier.hideRunningNotification(this); + disableMediaPlayer(); + } + + /** + * Disconnect all currently connected bridges. + */ + private void disconnectAll(final boolean immediate) { + disconnectAll(immediate, false); + } + + /** + * Disconnect all currently connected bridges. + */ + private void disconnectAll(final boolean immediate, boolean onlyRemote) { + TerminalBridge[] tmpBridges = null; + + synchronized (bridges) { + if (bridges.size() > 0) { + tmpBridges = bridges.toArray(new TerminalBridge[bridges.size()]); + } + } + + if (tmpBridges != null) { + // disconnect and dispose of any existing bridges + for (int i = 0; i < tmpBridges.length; i++) { + if (!onlyRemote || !(tmpBridges[i].transport instanceof com.five_ten_sg.connectbot.transport.Local)) + tmpBridges[i].dispatchDisconnect(immediate); + } + } + } + + /** + * Open a new SSH session using the given parameters. + */ + private TerminalBridge openConnection(HostBean host) throws IllegalArgumentException, IOException { + // throw exception if terminal already open + if (getConnectedBridge(host) != null) { + throw new IllegalArgumentException("Connection already open for that nickname"); + } + + TerminalBridge bridge = new TerminalBridge(this, host, getApplicationInfo().dataDir); + bridge.setOnDisconnectedListener(this); + bridge.startConnection(); + + synchronized (bridges) { + bridges.add(bridge); + WeakReference<TerminalBridge> wr = new WeakReference<TerminalBridge> (bridge); + mHostBridgeMap.put(bridge.host, wr); + mNicknameBridgeMap.put(bridge.host.getNickname(), wr); + } + + synchronized (disconnected) { + disconnected.remove(bridge.host); + } + + if (bridge.isUsingNetwork()) { + connectivityManager.incRef(); + } + + if (prefs.getBoolean(PreferenceConstants.CONNECTION_PERSIST, true)) { + connectionNotifier.showRunningNotification(this); + } + + // also update database with new connected time + touchHost(host); + return bridge; + } + + public String getEmulation() { + return prefs.getString(PreferenceConstants.EMULATION, "xterm-256color"); + } + + public int getScrollback() { + int scrollback = 140; + + try { + scrollback = Integer.parseInt(prefs.getString(PreferenceConstants.SCROLLBACK, "140")); + } + catch (Exception e) { + } + + return scrollback; + } + + /** + * Open a new connection by reading parameters from the given URI. Follows + * format specified by an individual transport. + */ + public TerminalBridge openConnection(Uri uri) throws Exception { + HostBean host = TransportFactory.findHost(hostdb, uri); + + if (host == null) + host = TransportFactory.getTransport(uri.getScheme()).createHost(uri); + + return openConnection(host); + } + + /** + * Update the last-connected value for the given nickname by passing through + * to {@link HostDatabase}. + */ + private void touchHost(HostBean host) { + hostdb.touchHost(host); + } + + /** + * Find a connected {@link TerminalBridge} with the given HostBean. + * + * @param host the HostBean to search for + * @return TerminalBridge that uses the HostBean + */ + public TerminalBridge getConnectedBridge(HostBean host) { + WeakReference<TerminalBridge> wr = mHostBridgeMap.get(host); + + if (wr != null) { + return wr.get(); + } + else { + return null; + } + } + + /** + * Find a connected {@link TerminalBridge} using its nickname. + * + * @param nickname + * @return TerminalBridge that matches nickname + */ + public TerminalBridge getConnectedBridge(final String nickname) { + if (nickname == null) { + return null; + } + + WeakReference<TerminalBridge> wr = mNicknameBridgeMap.get(nickname); + + if (wr != null) { + return wr.get(); + } + else { + return null; + } + } + + /** + * Called by child bridge when somehow it's been disconnected. + */ + public void onDisconnected(TerminalBridge bridge) { + boolean shouldHideRunningNotification = false; + + synchronized (bridges) { + // remove this bridge from our list + bridges.remove(bridge); + mHostBridgeMap.remove(bridge.host); + mNicknameBridgeMap.remove(bridge.host.getNickname()); + + if (bridge.isUsingNetwork()) { + connectivityManager.decRef(); + } + + if (bridges.size() == 0 && + mPendingReconnect.size() == 0) { + shouldHideRunningNotification = true; + } + } + + synchronized (disconnected) { + disconnected.add(bridge.host); + } + + if (shouldHideRunningNotification) { + connectionNotifier.hideRunningNotification(this); + } + + // pass notification back up to gui + if (disconnectHandler != null) + Message.obtain(disconnectHandler, -1, bridge).sendToTarget(); + } + + public boolean isKeyLoaded(String nickname) { + return loadedKeypairs.containsKey(nickname); + } + + public void addKey(PubkeyBean pubkey, KeyPair pair) { + addKey(pubkey, pair, false); + } + + public void addKey(PubkeyBean pubkey, KeyPair pair, boolean force) { + if (!savingKeys && !force) + return; + + removeKey(pubkey.getNickname()); + byte[] sshPubKey = PubkeyUtils.extractOpenSSHPublic(pair); + KeyHolder keyHolder = new KeyHolder(); + keyHolder.bean = pubkey; + keyHolder.pair = pair; + keyHolder.openSSHPubkey = sshPubKey; + loadedKeypairs.put(pubkey.getNickname(), keyHolder); + + if (pubkey.getLifetime() > 0) { + final String nickname = pubkey.getNickname(); + pubkeyTimer.schedule(new TimerTask() { + @Override + public void run() { + Log.d(TAG, "Unloading from memory key: " + nickname); + removeKey(nickname); + } + }, pubkey.getLifetime() * 1000); + } + + Log.d(TAG, String.format("Added key '%s' to in-memory cache", pubkey.getNickname())); + } + + public boolean removeKey(String nickname) { + Log.d(TAG, String.format("Removed key '%s' from in-memory cache", nickname)); + return loadedKeypairs.remove(nickname) != null; + } + + public boolean removeKey(byte[] publicKey) { + String nickname = null; + + for (Entry<String, KeyHolder> entry : loadedKeypairs.entrySet()) { + if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) { + nickname = entry.getKey(); + break; + } + } + + if (nickname != null) { + Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname)); + return removeKey(nickname); + } + else + return false; + } + + public KeyPair getKey(String nickname) { + if (loadedKeypairs.containsKey(nickname)) { + KeyHolder keyHolder = loadedKeypairs.get(nickname); + return keyHolder.pair; + } + else + return null; + } + + public KeyPair getKey(byte[] publicKey) { + for (KeyHolder keyHolder : loadedKeypairs.values()) { + if (Arrays.equals(keyHolder.openSSHPubkey, publicKey)) + return keyHolder.pair; + } + + return null; + } + + public String getKeyNickname(byte[] publicKey) { + for (Entry<String, KeyHolder> entry : loadedKeypairs.entrySet()) { + if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) + return entry.getKey(); + } + + return null; + } + + private void stopWithDelay() { + // TODO add in a way to check whether keys loaded are encrypted and only + // set timer when we have an encrypted key loaded + if (loadedKeypairs.size() > 0) { + synchronized (this) { + if (idleTimer == null) + idleTimer = new Timer("idleTimer", true); + + idleTimer.schedule(new IdleTask(), IDLE_TIMEOUT); + } + } + else { + Log.d(TAG, "Stopping service immediately"); + stopSelf(); + } + } + + protected void stopNow() { + if (bridges.size() == 0) { + stopSelf(); + } + } + + private synchronized void stopIdleTimer() { + if (idleTimer != null) { + idleTimer.cancel(); + idleTimer = null; + } + } + + public class TerminalBinder extends Binder { + public TerminalManager getService() { + return TerminalManager.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + Log.i(TAG, "Someone bound to TerminalManager"); + setResizeAllowed(true); + stopIdleTimer(); + // Make sure we stay running to maintain the bridges + startService(new Intent(this, TerminalManager.class)); + return binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + /* + * We want this service to continue running until it is explicitly + * stopped, so return sticky. + */ + return START_STICKY; + } + + @Override + public void onRebind(Intent intent) { + super.onRebind(intent); + setResizeAllowed(true); + Log.i(TAG, "Someone rebound to TerminalManager"); + stopIdleTimer(); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.i(TAG, "Someone unbound from TerminalManager"); + setResizeAllowed(true); + + if (bridges.size() == 0) { + stopWithDelay(); + } + + return true; + } + + private class IdleTask extends TimerTask { + /* (non-Javadoc) + * @see java.util.TimerTask#run() + */ + @Override + public void run() { + Log.d(TAG, String.format("Stopping service after timeout of ~%d seconds", IDLE_TIMEOUT / 1000)); + TerminalManager.this.stopNow(); + } + } + + public void tryKeyVibrate() { + if (wantKeyVibration) + vibrate(); + } + + private void vibrate() { + if (vibrator != null) + vibrator.vibrate(VIBRATE_DURATION); + } + + private void enableMediaPlayer() { + mediaPlayer = new MediaPlayer(); + float volume = prefs.getFloat(PreferenceConstants.BELL_VOLUME, + PreferenceConstants.DEFAULT_BELL_VOLUME); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); + mediaPlayer.setOnCompletionListener(new BeepListener()); + AssetFileDescriptor file = res.openRawResourceFd(R.raw.bell); + + try { + mediaPlayer.setDataSource(file.getFileDescriptor(), file + .getStartOffset(), file.getLength()); + file.close(); + mediaPlayer.setVolume(volume, volume); + mediaPlayer.prepare(); + } + catch (IOException e) { + Log.e(TAG, "Error setting up bell media player", e); + } + } + + private void disableMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } + + public void playBeep() { + if (mediaPlayer != null) + mediaPlayer.start(); + + if (wantBellVibration) + vibrate(); + } + + private static class BeepListener implements OnCompletionListener { + public void onCompletion(MediaPlayer mp) { + mp.seekTo(0); + } + } + + /** + * Send system notification to user for a certain host. When user selects + * the notification, it will bring them directly to the ConsoleActivity + * displaying the host. + * + * @param host + */ + public void sendActivityNotification(HostBean host) { + if (!prefs.getBoolean(PreferenceConstants.BELL_NOTIFICATION, false)) + return; + + connectionNotifier.showActivityNotification(this, host); + } + + /* (non-Javadoc) + * @see android.content.SharedPreferences.OnSharedPreferenceChangeListener#onSharedPreferenceChanged(android.content.SharedPreferences, java.lang.String) + */ + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (PreferenceConstants.BELL.equals(key)) { + boolean wantAudible = sharedPreferences.getBoolean( + PreferenceConstants.BELL, true); + + if (wantAudible && mediaPlayer == null) + enableMediaPlayer(); + else if (!wantAudible && mediaPlayer != null) + disableMediaPlayer(); + } + else if (PreferenceConstants.BELL_VOLUME.equals(key)) { + if (mediaPlayer != null) { + float volume = sharedPreferences.getFloat( + PreferenceConstants.BELL_VOLUME, + PreferenceConstants.DEFAULT_BELL_VOLUME); + mediaPlayer.setVolume(volume, volume); + } + } + else if (PreferenceConstants.BELL_VIBRATE.equals(key)) { + wantBellVibration = sharedPreferences.getBoolean( + PreferenceConstants.BELL_VIBRATE, true); + } + else if (PreferenceConstants.BUMPY_ARROWS.equals(key)) { + wantKeyVibration = sharedPreferences.getBoolean( + PreferenceConstants.BUMPY_ARROWS, true); + } + else if (PreferenceConstants.WIFI_LOCK.equals(key)) { + final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true); + connectivityManager.setWantWifiLock(lockingWifi); + } + else if (PreferenceConstants.MEMKEYS.equals(key)) { + updateSavingKeys(); + } + } + + /** + * Allow {@link TerminalBridge} to resize when the parent has changed. + * @param resizeAllowed + */ + public void setResizeAllowed(boolean resizeAllowed) { + this.resizeAllowed = resizeAllowed; + } + + public boolean isResizeAllowed() { + return resizeAllowed; + } + + public void setFullScreen(int fullScreen) { + this.fullScreen = fullScreen; + } + + public int getFullScreen() { + return this.fullScreen; + } + + public static class KeyHolder { + public PubkeyBean bean; + public KeyPair pair; + public byte[] openSSHPubkey; + } + + /** + * Called when connectivity to the network is lost and it doesn't appear + * we'll be getting a different connection any time soon. + */ + public void onConnectivityLost() { + final Thread t = new Thread() { + @Override + public void run() { + disconnectAll(false, true); + } + }; + t.setName("Disconnector"); + t.start(); + } + + /** + * Called when connectivity to the network is restored. + */ + public void onConnectivityRestored() { + final Thread t = new Thread() { + @Override + public void run() { + reconnectPending(); + } + }; + t.setName("Reconnector"); + t.start(); + } + + /** + * Insert request into reconnect queue to be executed either immediately + * or later when connectivity is restored depending on whether we're + * currently connected. + * + * @param bridge the TerminalBridge to reconnect when possible + */ + public void requestReconnect(TerminalBridge bridge) { + synchronized (mPendingReconnect) { + mPendingReconnect.add(new WeakReference<TerminalBridge> (bridge)); + + if (!bridge.isUsingNetwork() || + connectivityManager.isConnected()) { + reconnectPending(); + } + } + } + + /** + * Reconnect all bridges that were pending a reconnect when connectivity + * was lost. + */ + private void reconnectPending() { + synchronized (mPendingReconnect) { + for (WeakReference<TerminalBridge> ref : mPendingReconnect) { + TerminalBridge bridge = ref.get(); + + if (bridge == null) { + continue; + } + + bridge.startConnection(); + } + + mPendingReconnect.clear(); + } + } +}