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();
+        }
+    }
+}