comparison 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
comparison
equal deleted inserted replaced
437:208b31032318 438:d29cce60f393
1 /*
2 * ConnectBot: simple, powerful, open-source SSH client for Android
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package com.five_ten_sg.connectbot.service;
19
20 import java.io.IOException;
21 import java.lang.ref.WeakReference;
22 import java.security.KeyPair;
23 import java.security.PrivateKey;
24 import java.security.PublicKey;
25 import java.util.Arrays;
26 import java.util.HashMap;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import java.util.Timer;
32 import java.util.TimerTask;
33
34 import com.five_ten_sg.connectbot.R;
35 import com.five_ten_sg.connectbot.bean.HostBean;
36 import com.five_ten_sg.connectbot.bean.PubkeyBean;
37 import com.five_ten_sg.connectbot.transport.TransportFactory;
38 import com.five_ten_sg.connectbot.util.HostDatabase;
39 import com.five_ten_sg.connectbot.util.PreferenceConstants;
40 import com.five_ten_sg.connectbot.util.PubkeyDatabase;
41 import com.five_ten_sg.connectbot.util.PubkeyUtils;
42 import android.app.Service;
43 import android.content.Context;
44 import android.content.Intent;
45 import android.content.SharedPreferences;
46 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
47 import android.content.res.AssetFileDescriptor;
48 import android.content.res.Configuration;
49 import android.content.res.Resources;
50 import android.media.AudioManager;
51 import android.media.MediaPlayer;
52 import android.media.MediaPlayer.OnCompletionListener;
53 import android.net.Uri;
54 import android.os.Binder;
55 import android.os.Handler;
56 import android.os.IBinder;
57 import android.os.Message;
58 import android.os.Vibrator;
59 import android.preference.PreferenceManager;
60 import android.util.Log;
61
62 /**
63 * Manager for SSH connections that runs as a service. This service holds a list
64 * of currently connected SSH bridges that are ready for connection up to a GUI
65 * if needed.
66 *
67 * @author jsharkey
68 */
69 public class TerminalManager extends Service implements BridgeDisconnectedListener, OnSharedPreferenceChangeListener {
70 public final static String TAG = "ConnectBot.TerminalManager";
71
72 public List<TerminalBridge> bridges = new LinkedList<TerminalBridge>();
73 public Map<HostBean, WeakReference<TerminalBridge>> mHostBridgeMap =
74 new HashMap<HostBean, WeakReference<TerminalBridge>>();
75 public Map<String, WeakReference<TerminalBridge>> mNicknameBridgeMap =
76 new HashMap<String, WeakReference<TerminalBridge>>();
77
78 public TerminalBridge defaultBridge = null;
79
80 public List<HostBean> disconnected = new LinkedList<HostBean>();
81
82 public Handler disconnectHandler = null;
83
84 public Map<String, KeyHolder> loadedKeypairs = new HashMap<String, KeyHolder>();
85
86 public Resources res;
87
88 public HostDatabase hostdb;
89 public PubkeyDatabase pubkeydb;
90
91 protected SharedPreferences prefs;
92
93 final private IBinder binder = new TerminalBinder();
94
95 private ConnectivityReceiver connectivityManager;
96 private ConnectionNotifier connectionNotifier = new ConnectionNotifier();
97
98 private MediaPlayer mediaPlayer;
99
100 private Timer pubkeyTimer;
101
102 private Timer idleTimer;
103 private final long IDLE_TIMEOUT = 300000; // 5 minutes
104
105 private Vibrator vibrator;
106 private volatile boolean wantKeyVibration;
107 public static final long VIBRATE_DURATION = 30;
108
109 private boolean wantBellVibration;
110
111 private boolean resizeAllowed = true;
112
113 private int fullScreen = 0;
114
115 private boolean savingKeys;
116
117 protected List<WeakReference<TerminalBridge>> mPendingReconnect
118 = new LinkedList<WeakReference<TerminalBridge>>();
119
120 public boolean hardKeyboardHidden;
121
122 @Override
123 public void onCreate() {
124 Log.i(TAG, "Starting service");
125 prefs = PreferenceManager.getDefaultSharedPreferences(this);
126 prefs.registerOnSharedPreferenceChangeListener(this);
127 res = getResources();
128 pubkeyTimer = new Timer("pubkeyTimer", true);
129 hostdb = new HostDatabase(this);
130 pubkeydb = new PubkeyDatabase(this);
131 // load all marked pubkeys into memory
132 updateSavingKeys();
133 List<PubkeyBean> pubkeys = pubkeydb.getAllStartPubkeys();
134
135 for (PubkeyBean pubkey : pubkeys) {
136 try {
137 PrivateKey privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType());
138 PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
139 KeyPair pair = new KeyPair(pubKey, privKey);
140 addKey(pubkey, pair);
141 }
142 catch (Exception e) {
143 Log.d(TAG, String.format("Problem adding key '%s' to in-memory cache", pubkey.getNickname()), e);
144 }
145 }
146
147 vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
148 wantKeyVibration = prefs.getBoolean(PreferenceConstants.BUMPY_ARROWS, true);
149 wantBellVibration = prefs.getBoolean(PreferenceConstants.BELL_VIBRATE, true);
150 enableMediaPlayer();
151 hardKeyboardHidden = (res.getConfiguration().hardKeyboardHidden ==
152 Configuration.HARDKEYBOARDHIDDEN_YES);
153 final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true);
154 connectivityManager = new ConnectivityReceiver(this, lockingWifi);
155 }
156
157 private void updateSavingKeys() {
158 savingKeys = prefs.getBoolean(PreferenceConstants.MEMKEYS, true);
159 }
160
161 @Override
162 public void onDestroy() {
163 Log.i(TAG, "Destroying service");
164 disconnectAll(true);
165
166 if (hostdb != null) {
167 hostdb.close();
168 hostdb = null;
169 }
170
171 if (pubkeydb != null) {
172 pubkeydb.close();
173 pubkeydb = null;
174 }
175
176 synchronized (this) {
177 if (idleTimer != null)
178 idleTimer.cancel();
179
180 if (pubkeyTimer != null)
181 pubkeyTimer.cancel();
182 }
183
184 connectivityManager.cleanup();
185 connectionNotifier.hideRunningNotification(this);
186 disableMediaPlayer();
187 }
188
189 /**
190 * Disconnect all currently connected bridges.
191 */
192 private void disconnectAll(final boolean immediate) {
193 disconnectAll(immediate, false);
194 }
195
196 /**
197 * Disconnect all currently connected bridges.
198 */
199 private void disconnectAll(final boolean immediate, boolean onlyRemote) {
200 TerminalBridge[] tmpBridges = null;
201
202 synchronized (bridges) {
203 if (bridges.size() > 0) {
204 tmpBridges = bridges.toArray(new TerminalBridge[bridges.size()]);
205 }
206 }
207
208 if (tmpBridges != null) {
209 // disconnect and dispose of any existing bridges
210 for (int i = 0; i < tmpBridges.length; i++) {
211 if (!onlyRemote || !(tmpBridges[i].transport instanceof com.five_ten_sg.connectbot.transport.Local))
212 tmpBridges[i].dispatchDisconnect(immediate);
213 }
214 }
215 }
216
217 /**
218 * Open a new SSH session using the given parameters.
219 */
220 private TerminalBridge openConnection(HostBean host) throws IllegalArgumentException, IOException {
221 // throw exception if terminal already open
222 if (getConnectedBridge(host) != null) {
223 throw new IllegalArgumentException("Connection already open for that nickname");
224 }
225
226 TerminalBridge bridge = new TerminalBridge(this, host, getApplicationInfo().dataDir);
227 bridge.setOnDisconnectedListener(this);
228 bridge.startConnection();
229
230 synchronized (bridges) {
231 bridges.add(bridge);
232 WeakReference<TerminalBridge> wr = new WeakReference<TerminalBridge> (bridge);
233 mHostBridgeMap.put(bridge.host, wr);
234 mNicknameBridgeMap.put(bridge.host.getNickname(), wr);
235 }
236
237 synchronized (disconnected) {
238 disconnected.remove(bridge.host);
239 }
240
241 if (bridge.isUsingNetwork()) {
242 connectivityManager.incRef();
243 }
244
245 if (prefs.getBoolean(PreferenceConstants.CONNECTION_PERSIST, true)) {
246 connectionNotifier.showRunningNotification(this);
247 }
248
249 // also update database with new connected time
250 touchHost(host);
251 return bridge;
252 }
253
254 public String getEmulation() {
255 return prefs.getString(PreferenceConstants.EMULATION, "xterm-256color");
256 }
257
258 public int getScrollback() {
259 int scrollback = 140;
260
261 try {
262 scrollback = Integer.parseInt(prefs.getString(PreferenceConstants.SCROLLBACK, "140"));
263 }
264 catch (Exception e) {
265 }
266
267 return scrollback;
268 }
269
270 /**
271 * Open a new connection by reading parameters from the given URI. Follows
272 * format specified by an individual transport.
273 */
274 public TerminalBridge openConnection(Uri uri) throws Exception {
275 HostBean host = TransportFactory.findHost(hostdb, uri);
276
277 if (host == null)
278 host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
279
280 return openConnection(host);
281 }
282
283 /**
284 * Update the last-connected value for the given nickname by passing through
285 * to {@link HostDatabase}.
286 */
287 private void touchHost(HostBean host) {
288 hostdb.touchHost(host);
289 }
290
291 /**
292 * Find a connected {@link TerminalBridge} with the given HostBean.
293 *
294 * @param host the HostBean to search for
295 * @return TerminalBridge that uses the HostBean
296 */
297 public TerminalBridge getConnectedBridge(HostBean host) {
298 WeakReference<TerminalBridge> wr = mHostBridgeMap.get(host);
299
300 if (wr != null) {
301 return wr.get();
302 }
303 else {
304 return null;
305 }
306 }
307
308 /**
309 * Find a connected {@link TerminalBridge} using its nickname.
310 *
311 * @param nickname
312 * @return TerminalBridge that matches nickname
313 */
314 public TerminalBridge getConnectedBridge(final String nickname) {
315 if (nickname == null) {
316 return null;
317 }
318
319 WeakReference<TerminalBridge> wr = mNicknameBridgeMap.get(nickname);
320
321 if (wr != null) {
322 return wr.get();
323 }
324 else {
325 return null;
326 }
327 }
328
329 /**
330 * Called by child bridge when somehow it's been disconnected.
331 */
332 public void onDisconnected(TerminalBridge bridge) {
333 boolean shouldHideRunningNotification = false;
334
335 synchronized (bridges) {
336 // remove this bridge from our list
337 bridges.remove(bridge);
338 mHostBridgeMap.remove(bridge.host);
339 mNicknameBridgeMap.remove(bridge.host.getNickname());
340
341 if (bridge.isUsingNetwork()) {
342 connectivityManager.decRef();
343 }
344
345 if (bridges.size() == 0 &&
346 mPendingReconnect.size() == 0) {
347 shouldHideRunningNotification = true;
348 }
349 }
350
351 synchronized (disconnected) {
352 disconnected.add(bridge.host);
353 }
354
355 if (shouldHideRunningNotification) {
356 connectionNotifier.hideRunningNotification(this);
357 }
358
359 // pass notification back up to gui
360 if (disconnectHandler != null)
361 Message.obtain(disconnectHandler, -1, bridge).sendToTarget();
362 }
363
364 public boolean isKeyLoaded(String nickname) {
365 return loadedKeypairs.containsKey(nickname);
366 }
367
368 public void addKey(PubkeyBean pubkey, KeyPair pair) {
369 addKey(pubkey, pair, false);
370 }
371
372 public void addKey(PubkeyBean pubkey, KeyPair pair, boolean force) {
373 if (!savingKeys && !force)
374 return;
375
376 removeKey(pubkey.getNickname());
377 byte[] sshPubKey = PubkeyUtils.extractOpenSSHPublic(pair);
378 KeyHolder keyHolder = new KeyHolder();
379 keyHolder.bean = pubkey;
380 keyHolder.pair = pair;
381 keyHolder.openSSHPubkey = sshPubKey;
382 loadedKeypairs.put(pubkey.getNickname(), keyHolder);
383
384 if (pubkey.getLifetime() > 0) {
385 final String nickname = pubkey.getNickname();
386 pubkeyTimer.schedule(new TimerTask() {
387 @Override
388 public void run() {
389 Log.d(TAG, "Unloading from memory key: " + nickname);
390 removeKey(nickname);
391 }
392 }, pubkey.getLifetime() * 1000);
393 }
394
395 Log.d(TAG, String.format("Added key '%s' to in-memory cache", pubkey.getNickname()));
396 }
397
398 public boolean removeKey(String nickname) {
399 Log.d(TAG, String.format("Removed key '%s' from in-memory cache", nickname));
400 return loadedKeypairs.remove(nickname) != null;
401 }
402
403 public boolean removeKey(byte[] publicKey) {
404 String nickname = null;
405
406 for (Entry<String, KeyHolder> entry : loadedKeypairs.entrySet()) {
407 if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) {
408 nickname = entry.getKey();
409 break;
410 }
411 }
412
413 if (nickname != null) {
414 Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname));
415 return removeKey(nickname);
416 }
417 else
418 return false;
419 }
420
421 public KeyPair getKey(String nickname) {
422 if (loadedKeypairs.containsKey(nickname)) {
423 KeyHolder keyHolder = loadedKeypairs.get(nickname);
424 return keyHolder.pair;
425 }
426 else
427 return null;
428 }
429
430 public KeyPair getKey(byte[] publicKey) {
431 for (KeyHolder keyHolder : loadedKeypairs.values()) {
432 if (Arrays.equals(keyHolder.openSSHPubkey, publicKey))
433 return keyHolder.pair;
434 }
435
436 return null;
437 }
438
439 public String getKeyNickname(byte[] publicKey) {
440 for (Entry<String, KeyHolder> entry : loadedKeypairs.entrySet()) {
441 if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey))
442 return entry.getKey();
443 }
444
445 return null;
446 }
447
448 private void stopWithDelay() {
449 // TODO add in a way to check whether keys loaded are encrypted and only
450 // set timer when we have an encrypted key loaded
451 if (loadedKeypairs.size() > 0) {
452 synchronized (this) {
453 if (idleTimer == null)
454 idleTimer = new Timer("idleTimer", true);
455
456 idleTimer.schedule(new IdleTask(), IDLE_TIMEOUT);
457 }
458 }
459 else {
460 Log.d(TAG, "Stopping service immediately");
461 stopSelf();
462 }
463 }
464
465 protected void stopNow() {
466 if (bridges.size() == 0) {
467 stopSelf();
468 }
469 }
470
471 private synchronized void stopIdleTimer() {
472 if (idleTimer != null) {
473 idleTimer.cancel();
474 idleTimer = null;
475 }
476 }
477
478 public class TerminalBinder extends Binder {
479 public TerminalManager getService() {
480 return TerminalManager.this;
481 }
482 }
483
484 @Override
485 public IBinder onBind(Intent intent) {
486 Log.i(TAG, "Someone bound to TerminalManager");
487 setResizeAllowed(true);
488 stopIdleTimer();
489 // Make sure we stay running to maintain the bridges
490 startService(new Intent(this, TerminalManager.class));
491 return binder;
492 }
493
494 @Override
495 public int onStartCommand(Intent intent, int flags, int startId) {
496 /*
497 * We want this service to continue running until it is explicitly
498 * stopped, so return sticky.
499 */
500 return START_STICKY;
501 }
502
503 @Override
504 public void onRebind(Intent intent) {
505 super.onRebind(intent);
506 setResizeAllowed(true);
507 Log.i(TAG, "Someone rebound to TerminalManager");
508 stopIdleTimer();
509 }
510
511 @Override
512 public boolean onUnbind(Intent intent) {
513 Log.i(TAG, "Someone unbound from TerminalManager");
514 setResizeAllowed(true);
515
516 if (bridges.size() == 0) {
517 stopWithDelay();
518 }
519
520 return true;
521 }
522
523 private class IdleTask extends TimerTask {
524 /* (non-Javadoc)
525 * @see java.util.TimerTask#run()
526 */
527 @Override
528 public void run() {
529 Log.d(TAG, String.format("Stopping service after timeout of ~%d seconds", IDLE_TIMEOUT / 1000));
530 TerminalManager.this.stopNow();
531 }
532 }
533
534 public void tryKeyVibrate() {
535 if (wantKeyVibration)
536 vibrate();
537 }
538
539 private void vibrate() {
540 if (vibrator != null)
541 vibrator.vibrate(VIBRATE_DURATION);
542 }
543
544 private void enableMediaPlayer() {
545 mediaPlayer = new MediaPlayer();
546 float volume = prefs.getFloat(PreferenceConstants.BELL_VOLUME,
547 PreferenceConstants.DEFAULT_BELL_VOLUME);
548 mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
549 mediaPlayer.setOnCompletionListener(new BeepListener());
550 AssetFileDescriptor file = res.openRawResourceFd(R.raw.bell);
551
552 try {
553 mediaPlayer.setDataSource(file.getFileDescriptor(), file
554 .getStartOffset(), file.getLength());
555 file.close();
556 mediaPlayer.setVolume(volume, volume);
557 mediaPlayer.prepare();
558 }
559 catch (IOException e) {
560 Log.e(TAG, "Error setting up bell media player", e);
561 }
562 }
563
564 private void disableMediaPlayer() {
565 if (mediaPlayer != null) {
566 mediaPlayer.release();
567 mediaPlayer = null;
568 }
569 }
570
571 public void playBeep() {
572 if (mediaPlayer != null)
573 mediaPlayer.start();
574
575 if (wantBellVibration)
576 vibrate();
577 }
578
579 private static class BeepListener implements OnCompletionListener {
580 public void onCompletion(MediaPlayer mp) {
581 mp.seekTo(0);
582 }
583 }
584
585 /**
586 * Send system notification to user for a certain host. When user selects
587 * the notification, it will bring them directly to the ConsoleActivity
588 * displaying the host.
589 *
590 * @param host
591 */
592 public void sendActivityNotification(HostBean host) {
593 if (!prefs.getBoolean(PreferenceConstants.BELL_NOTIFICATION, false))
594 return;
595
596 connectionNotifier.showActivityNotification(this, host);
597 }
598
599 /* (non-Javadoc)
600 * @see android.content.SharedPreferences.OnSharedPreferenceChangeListener#onSharedPreferenceChanged(android.content.SharedPreferences, java.lang.String)
601 */
602 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
603 String key) {
604 if (PreferenceConstants.BELL.equals(key)) {
605 boolean wantAudible = sharedPreferences.getBoolean(
606 PreferenceConstants.BELL, true);
607
608 if (wantAudible && mediaPlayer == null)
609 enableMediaPlayer();
610 else if (!wantAudible && mediaPlayer != null)
611 disableMediaPlayer();
612 }
613 else if (PreferenceConstants.BELL_VOLUME.equals(key)) {
614 if (mediaPlayer != null) {
615 float volume = sharedPreferences.getFloat(
616 PreferenceConstants.BELL_VOLUME,
617 PreferenceConstants.DEFAULT_BELL_VOLUME);
618 mediaPlayer.setVolume(volume, volume);
619 }
620 }
621 else if (PreferenceConstants.BELL_VIBRATE.equals(key)) {
622 wantBellVibration = sharedPreferences.getBoolean(
623 PreferenceConstants.BELL_VIBRATE, true);
624 }
625 else if (PreferenceConstants.BUMPY_ARROWS.equals(key)) {
626 wantKeyVibration = sharedPreferences.getBoolean(
627 PreferenceConstants.BUMPY_ARROWS, true);
628 }
629 else if (PreferenceConstants.WIFI_LOCK.equals(key)) {
630 final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true);
631 connectivityManager.setWantWifiLock(lockingWifi);
632 }
633 else if (PreferenceConstants.MEMKEYS.equals(key)) {
634 updateSavingKeys();
635 }
636 }
637
638 /**
639 * Allow {@link TerminalBridge} to resize when the parent has changed.
640 * @param resizeAllowed
641 */
642 public void setResizeAllowed(boolean resizeAllowed) {
643 this.resizeAllowed = resizeAllowed;
644 }
645
646 public boolean isResizeAllowed() {
647 return resizeAllowed;
648 }
649
650 public void setFullScreen(int fullScreen) {
651 this.fullScreen = fullScreen;
652 }
653
654 public int getFullScreen() {
655 return this.fullScreen;
656 }
657
658 public static class KeyHolder {
659 public PubkeyBean bean;
660 public KeyPair pair;
661 public byte[] openSSHPubkey;
662 }
663
664 /**
665 * Called when connectivity to the network is lost and it doesn't appear
666 * we'll be getting a different connection any time soon.
667 */
668 public void onConnectivityLost() {
669 final Thread t = new Thread() {
670 @Override
671 public void run() {
672 disconnectAll(false, true);
673 }
674 };
675 t.setName("Disconnector");
676 t.start();
677 }
678
679 /**
680 * Called when connectivity to the network is restored.
681 */
682 public void onConnectivityRestored() {
683 final Thread t = new Thread() {
684 @Override
685 public void run() {
686 reconnectPending();
687 }
688 };
689 t.setName("Reconnector");
690 t.start();
691 }
692
693 /**
694 * Insert request into reconnect queue to be executed either immediately
695 * or later when connectivity is restored depending on whether we're
696 * currently connected.
697 *
698 * @param bridge the TerminalBridge to reconnect when possible
699 */
700 public void requestReconnect(TerminalBridge bridge) {
701 synchronized (mPendingReconnect) {
702 mPendingReconnect.add(new WeakReference<TerminalBridge> (bridge));
703
704 if (!bridge.isUsingNetwork() ||
705 connectivityManager.isConnected()) {
706 reconnectPending();
707 }
708 }
709 }
710
711 /**
712 * Reconnect all bridges that were pending a reconnect when connectivity
713 * was lost.
714 */
715 private void reconnectPending() {
716 synchronized (mPendingReconnect) {
717 for (WeakReference<TerminalBridge> ref : mPendingReconnect) {
718 TerminalBridge bridge = ref.get();
719
720 if (bridge == null) {
721 continue;
722 }
723
724 bridge.startConnection();
725 }
726
727 mPendingReconnect.clear();
728 }
729 }
730 }