org.connectbot.ConsoleActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.connectbot.ConsoleActivity.java

Source

/*
 * 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 org.connectbot;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

import org.connectbot.bean.HostBean;
import org.connectbot.service.BridgeDisconnectedListener;
import org.connectbot.service.PromptHelper;
import org.connectbot.service.TerminalBridge;
import org.connectbot.service.TerminalKeyListener;
import org.connectbot.service.TerminalManager;
import org.connectbot.util.PreferenceConstants;
import org.connectbot.util.TerminalViewPager;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.ClipboardManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import de.mud.terminal.vt320;

public class ConsoleActivity extends AppCompatActivity implements BridgeDisconnectedListener {
    public final static String TAG = "CB.ConsoleActivity";

    protected static final int REQUEST_EDIT = 1;

    private static final int KEYBOARD_DISPLAY_TIME = 3000;
    private static final int KEYBOARD_REPEAT_INITIAL = 500;
    private static final int KEYBOARD_REPEAT = 100;
    private static final String STATE_SELECTED_URI = "selectedUri";

    protected TerminalViewPager pager = null;
    protected TabLayout tabs = null;
    protected Toolbar toolbar = null;
    @Nullable
    protected TerminalManager bound = null;
    protected TerminalPagerAdapter adapter = null;
    protected LayoutInflater inflater = null;

    private SharedPreferences prefs = null;

    // determines whether or not menuitem accelerators are bound
    // otherwise they collide with an external keyboard's CTRL-char
    private boolean hardKeyboard = false;

    protected Uri requested;

    protected ClipboardManager clipboard;
    private RelativeLayout stringPromptGroup;
    protected EditText stringPrompt;
    private TextView stringPromptInstructions;

    private RelativeLayout booleanPromptGroup;
    private TextView booleanPrompt;
    private Button booleanYes;

    private LinearLayout keyboardGroup;
    private Runnable keyboardGroupHider;

    private TextView empty;

    private Animation fade_out_delayed;

    private Animation keyboard_fade_in, keyboard_fade_out;

    private MenuItem disconnect, copy, paste, portForward, resize, urlscan;

    private boolean forcedOrientation;

    private Handler handler = new Handler();

    private View contentView;

    private ImageView mKeyboardButton;

    @Nullable
    private ActionBar actionBar;
    private boolean inActionBarMenu = false;
    private boolean titleBarHide;
    private boolean keyboardAlwaysVisible = false;

    private ServiceConnection connection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            bound = ((TerminalManager.TerminalBinder) service).getService();

            // let manager know about our event handling services
            bound.disconnectListener = ConsoleActivity.this;
            bound.setResizeAllowed(true);

            final String requestedNickname = (requested != null) ? requested.getFragment() : null;
            TerminalBridge requestedBridge = bound.getConnectedBridge(requestedNickname);

            // If we didn't find the requested connection, try opening it
            if (requestedNickname != null && requestedBridge == null) {
                try {
                    Log.d(TAG, String.format(
                            "We couldnt find an existing bridge with URI=%s (nickname=%s), so creating one now",
                            requested.toString(), requestedNickname));
                    requestedBridge = bound.openConnection(requested);
                } catch (Exception e) {
                    Log.e(TAG, "Problem while trying to create new requested bridge from URI", e);
                }
            }

            // create views for all bridges on this service
            adapter.notifyDataSetChanged();
            final int requestedIndex = bound.getBridges().indexOf(requestedBridge);

            if (requestedBridge != null)
                requestedBridge.promptHelper.setHandler(promptHandler);

            if (requestedIndex != -1) {
                pager.post(new Runnable() {
                    @Override
                    public void run() {
                        setDisplayedTerminal(requestedIndex);
                    }
                });
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            bound = null;
            adapter.notifyDataSetChanged();
            updateEmptyVisible();
        }
    };

    protected Handler promptHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // someone below us requested to display a prompt
            updatePromptVisible();
        }
    };

    public void onDisconnected(TerminalBridge bridge) {
        synchronized (adapter) {
            adapter.notifyDataSetChanged();
            Log.d(TAG, "Someone sending HANDLE_DISCONNECT to parentHandler");

            if (bridge.isAwaitingClose()) {
                closeBridge(bridge);
            }
        }
    }

    protected OnClickListener emulatedKeysListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            onEmulatedKeyClicked(v);
        }
    };

    protected Handler keyRepeatHandler = new Handler();

    /**
     * Handle repeatable virtual keys and touch events
     */
    public class KeyRepeater implements Runnable, OnTouchListener, OnClickListener {
        private View mView;
        private Handler mHandler;
        private boolean mDown;

        public KeyRepeater(Handler handler, View view) {
            mView = view;
            mHandler = handler;
            mDown = false;
        }

        @Override
        public void run() {
            mDown = true;
            mHandler.removeCallbacks(this);
            mHandler.postDelayed(this, KEYBOARD_REPEAT);
            mView.performClick();
        }

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "KeyRepeater.onTouch(" + v.getId() + ", " + event.getAction() + ", "
                        + event.getActionIndex() + ", " + event.getActionMasked() + ");");
            }
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDown = false;
                mHandler.postDelayed(this, KEYBOARD_REPEAT_INITIAL);
                mView.setPressed(true);
                return (true);

            case MotionEvent.ACTION_CANCEL:
                mHandler.removeCallbacks(this);
                mView.setPressed(false);
                return (true);

            case MotionEvent.ACTION_UP:
                mHandler.removeCallbacks(this);
                mView.setPressed(false);

                if (!mDown) {
                    mView.performClick();
                }
                return (true);
            }
            return false;
        }

        @Override
        public void onClick(View view) {
            onEmulatedKeyClicked(view);
        }
    }

    private void onEmulatedKeyClicked(View v) {
        TerminalView terminal = adapter.getCurrentTerminalView();
        if (terminal == null)
            return;

        if (BuildConfig.DEBUG) {
            Log.d(TAG, "onEmulatedKeyClicked(" + v.getId() + ")");
        }
        TerminalKeyListener handler = terminal.bridge.getKeyHandler();
        boolean hideKeys = false;

        switch (v.getId()) {
        case R.id.button_ctrl:
            handler.metaPress(TerminalKeyListener.OUR_CTRL_ON, true);
            hideKeys = true;
            break;
        case R.id.button_esc:
            handler.sendEscape();
            hideKeys = true;
            break;
        case R.id.button_tab:
            handler.sendTab();
            hideKeys = true;
            break;

        case R.id.button_up:
            handler.sendPressedKey(vt320.KEY_UP);
            break;
        case R.id.button_down:
            handler.sendPressedKey(vt320.KEY_DOWN);
            break;
        case R.id.button_left:
            handler.sendPressedKey(vt320.KEY_LEFT);
            break;
        case R.id.button_right:
            handler.sendPressedKey(vt320.KEY_RIGHT);
            break;

        case R.id.button_home:
            handler.sendPressedKey(vt320.KEY_HOME);
            break;
        case R.id.button_end:
            handler.sendPressedKey(vt320.KEY_END);
            break;
        case R.id.button_pgup:
            handler.sendPressedKey(vt320.KEY_PAGE_UP);
            break;
        case R.id.button_pgdn:
            handler.sendPressedKey(vt320.KEY_PAGE_DOWN);
            break;

        case R.id.button_f1:
            handler.sendPressedKey(vt320.KEY_F1);
            break;
        case R.id.button_f2:
            handler.sendPressedKey(vt320.KEY_F2);
            break;
        case R.id.button_f3:
            handler.sendPressedKey(vt320.KEY_F3);
            break;
        case R.id.button_f4:
            handler.sendPressedKey(vt320.KEY_F4);
            break;
        case R.id.button_f5:
            handler.sendPressedKey(vt320.KEY_F5);
            break;
        case R.id.button_f6:
            handler.sendPressedKey(vt320.KEY_F6);
            break;
        case R.id.button_f7:
            handler.sendPressedKey(vt320.KEY_F7);
            break;
        case R.id.button_f8:
            handler.sendPressedKey(vt320.KEY_F8);
            break;
        case R.id.button_f9:
            handler.sendPressedKey(vt320.KEY_F9);
            break;
        case R.id.button_f10:
            handler.sendPressedKey(vt320.KEY_F10);
            break;
        case R.id.button_f11:
            handler.sendPressedKey(vt320.KEY_F11);
            break;
        case R.id.button_f12:
            handler.sendPressedKey(vt320.KEY_F12);
            break;
        default:
            Log.e(TAG, "Unknown emulated key clicked: " + v.getId());
            break;
        }

        if (hideKeys) {
            hideEmulatedKeys();
        } else {
            autoHideEmulatedKeys();
        }

        terminal.bridge.tryKeyVibrate();
        hideActionBarIfRequested();
    }

    private void hideActionBarIfRequested() {
        if (titleBarHide && actionBar != null) {
            actionBar.hide();
        }
    }

    /**
     * @param bridge
     */
    private void closeBridge(final TerminalBridge bridge) {
        updateEmptyVisible();
        updatePromptVisible();

        // If we just closed the last bridge, go back to the previous activity.
        if (pager.getChildCount() == 0) {
            finish();
        }
    }

    protected View findCurrentView(int id) {
        View view = pager.findViewWithTag(adapter.getBridgeAtPosition(pager.getCurrentItem()));
        if (view == null) {
            return null;
        }
        return view.findViewById(id);
    }

    protected PromptHelper getCurrentPromptHelper() {
        TerminalView view = adapter.getCurrentTerminalView();
        if (view == null)
            return null;
        return view.bridge.promptHelper;
    }

    protected void hideAllPrompts() {
        stringPromptGroup.setVisibility(View.GONE);
        booleanPromptGroup.setVisibility(View.GONE);
    }

    private void showEmulatedKeys(boolean showActionBar) {
        if (keyboardGroup.getVisibility() == View.GONE) {
            keyboardGroup.startAnimation(keyboard_fade_in);
            keyboardGroup.setVisibility(View.VISIBLE);
        }
        if (showActionBar) {
            actionBar.show();
        }
        autoHideEmulatedKeys();
    }

    private void autoHideEmulatedKeys() {
        if (keyboardGroupHider != null) {
            handler.removeCallbacks(keyboardGroupHider);
        }

        keyboardGroupHider = new Runnable() {
            public void run() {
                if (keyboardGroup.getVisibility() == View.GONE || inActionBarMenu) {
                    return;
                }

                if (!keyboardAlwaysVisible) {
                    keyboardGroup.startAnimation(keyboard_fade_out);
                    keyboardGroup.setVisibility(View.GONE);
                }
                hideActionBarIfRequested();
                keyboardGroupHider = null;
            }
        };
        handler.postDelayed(keyboardGroupHider, KEYBOARD_DISPLAY_TIME);
    }

    private void hideEmulatedKeys() {
        if (!keyboardAlwaysVisible) {
            if (keyboardGroupHider != null)
                handler.removeCallbacks(keyboardGroupHider);
            keyboardGroup.setVisibility(View.GONE);
        }
        hideActionBarIfRequested();
    }

    @TargetApi(11)
    private void requestActionBar() {
        supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            StrictModeSetup.run();
        }

        hardKeyboard = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;

        clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
        prefs = PreferenceManager.getDefaultSharedPreferences(this);

        titleBarHide = prefs.getBoolean(PreferenceConstants.TITLEBARHIDE, false);
        if (titleBarHide && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // This is a separate method because Gradle does not uniformly respect the conditional
            // Build check. See: https://code.google.com/p/android/issues/detail?id=137195
            requestActionBar();
        }

        this.setContentView(R.layout.act_console);

        // hide status bar if requested by user
        if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) {
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }

        // TODO find proper way to disable volume key beep if it exists.
        setVolumeControlStream(AudioManager.STREAM_MUSIC);

        // handle requested console from incoming intent
        if (icicle == null) {
            requested = getIntent().getData();
        } else {
            String uri = icicle.getString(STATE_SELECTED_URI);
            if (uri != null) {
                requested = Uri.parse(uri);
            }
        }

        inflater = LayoutInflater.from(this);

        toolbar = (Toolbar) findViewById(R.id.toolbar);

        pager = (TerminalViewPager) findViewById(R.id.console_flip);
        pager.addOnPageChangeListener(new TerminalViewPager.SimpleOnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                setTitle(adapter.getPageTitle(position));
                onTerminalChanged();
            }
        });
        adapter = new TerminalPagerAdapter();
        pager.setAdapter(adapter);

        empty = (TextView) findViewById(android.R.id.empty);

        stringPromptGroup = (RelativeLayout) findViewById(R.id.console_password_group);
        stringPromptInstructions = (TextView) findViewById(R.id.console_password_instructions);
        stringPrompt = (EditText) findViewById(R.id.console_password);
        stringPrompt.setOnKeyListener(new OnKeyListener() {
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_UP)
                    return false;
                if (keyCode != KeyEvent.KEYCODE_ENTER)
                    return false;

                // pass collected password down to current terminal
                String value = stringPrompt.getText().toString();

                PromptHelper helper = getCurrentPromptHelper();
                if (helper == null)
                    return false;
                helper.setResponse(value);

                // finally clear password for next user
                stringPrompt.setText("");
                updatePromptVisible();

                return true;
            }
        });

        booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group);
        booleanPrompt = (TextView) findViewById(R.id.console_prompt);

        booleanYes = (Button) findViewById(R.id.console_prompt_yes);
        booleanYes.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                PromptHelper helper = getCurrentPromptHelper();
                if (helper == null)
                    return;
                helper.setResponse(Boolean.TRUE);
                updatePromptVisible();
            }
        });

        Button booleanNo = (Button) findViewById(R.id.console_prompt_no);
        booleanNo.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                PromptHelper helper = getCurrentPromptHelper();
                if (helper == null)
                    return;
                helper.setResponse(Boolean.FALSE);
                updatePromptVisible();
            }
        });

        fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed);

        // Preload animation for keyboard button
        keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in);
        keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out);

        keyboardGroup = (LinearLayout) findViewById(R.id.keyboard_group);

        keyboardAlwaysVisible = prefs.getBoolean(PreferenceConstants.KEY_ALWAYS_VISIVLE, false);
        if (keyboardAlwaysVisible) {
            // equivalent to android:layout_above=keyboard_group
            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            layoutParams.addRule(RelativeLayout.ABOVE, R.id.keyboard_group);
            pager.setLayoutParams(layoutParams);

            // Show virtual keyboard
            keyboardGroup.setVisibility(View.VISIBLE);
        }

        mKeyboardButton = (ImageView) findViewById(R.id.button_keyboard);
        mKeyboardButton.setOnClickListener(new OnClickListener() {
            public void onClick(View view) {
                View terminal = adapter.getCurrentTerminalView();
                if (terminal == null)
                    return;
                InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(
                        Context.INPUT_METHOD_SERVICE);
                inputMethodManager.toggleSoftInputFromWindow(terminal.getApplicationWindowToken(),
                        InputMethodManager.SHOW_FORCED, 0);
                terminal.requestFocus();
                hideEmulatedKeys();
            }
        });

        findViewById(R.id.button_ctrl).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_esc).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_tab).setOnClickListener(emulatedKeysListener);

        addKeyRepeater(findViewById(R.id.button_up));
        addKeyRepeater(findViewById(R.id.button_up));
        addKeyRepeater(findViewById(R.id.button_down));
        addKeyRepeater(findViewById(R.id.button_left));
        addKeyRepeater(findViewById(R.id.button_right));

        findViewById(R.id.button_home).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_end).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_pgup).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_pgdn).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f1).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f2).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f3).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f4).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f5).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f6).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f7).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f8).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f9).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f10).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f11).setOnClickListener(emulatedKeysListener);
        findViewById(R.id.button_f12).setOnClickListener(emulatedKeysListener);

        actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
            if (titleBarHide) {
                actionBar.hide();
            }
            actionBar.addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
                public void onMenuVisibilityChanged(boolean isVisible) {
                    inActionBarMenu = isVisible;
                    if (!isVisible) {
                        hideEmulatedKeys();
                    }
                }
            });
        }

        final HorizontalScrollView keyboardScroll = (HorizontalScrollView) findViewById(R.id.keyboard_hscroll);
        if (!hardKeyboard) {
            // Show virtual keyboard and scroll back and forth
            showEmulatedKeys(false);
            keyboardScroll.postDelayed(new Runnable() {
                @Override
                public void run() {
                    final int xscroll = findViewById(R.id.button_f12).getRight();
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "smoothScrollBy(toEnd[" + xscroll + "])");
                    }
                    keyboardScroll.smoothScrollBy(xscroll, 0);
                    keyboardScroll.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            if (BuildConfig.DEBUG) {
                                Log.d(TAG, "smoothScrollBy(toStart[" + (-xscroll) + "])");
                            }
                            keyboardScroll.smoothScrollBy(-xscroll, 0);
                        }
                    }, 500);
                }
            }, 500);
        }

        // Reset keyboard auto-hide timer when scrolling
        keyboardScroll.setOnTouchListener(new OnTouchListener() {
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    autoHideEmulatedKeys();
                    break;
                case MotionEvent.ACTION_UP:
                    v.performClick();
                    return (true);
                }
                return (false);
            }
        });

        tabs = (TabLayout) findViewById(R.id.tabs);
        if (tabs != null)
            setupTabLayoutWithViewPager();

        pager.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                showEmulatedKeys(true);
            }
        });

        // Change keyboard button image according to soft keyboard visibility
        // How to detect keyboard visibility: http://stackoverflow.com/q/4745988
        contentView = findViewById(android.R.id.content);
        contentView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                Rect r = new Rect();
                contentView.getWindowVisibleDisplayFrame(r);
                int screenHeight = contentView.getRootView().getHeight();
                int keypadHeight = screenHeight - r.bottom;

                if (keypadHeight > screenHeight * 0.15) {
                    // keyboard is opened
                    mKeyboardButton.setImageResource(R.drawable.ic_keyboard_hide);
                    mKeyboardButton.setContentDescription(
                            getResources().getText(R.string.image_description_hide_keyboard));
                } else {
                    // keyboard is closed
                    mKeyboardButton.setImageResource(R.drawable.ic_keyboard);
                    mKeyboardButton.setContentDescription(
                            getResources().getText(R.string.image_description_show_keyboard));
                }
            }
        });
    }

    private void addKeyRepeater(View view) {
        KeyRepeater keyRepeater = new KeyRepeater(keyRepeatHandler, view);
        view.setOnClickListener(keyRepeater);
        view.setOnTouchListener(keyRepeater);
    }

    /**
     * Ties the {@link TabLayout} to the {@link TerminalViewPager}.
     *
     * <p>This method will:
     * <ul>
     *     <li>Add a {@link TerminalViewPager.OnPageChangeListener} that will forward events to
     *     this TabLayout.</li>
     *     <li>Populate the TabLayout's tabs from the ViewPager's {@link PagerAdapter}.</li>
     *     <li>Set our {@link TabLayout.OnTabSelectedListener} which will forward
     *     selected events to the ViewPager</li>
     * </ul>
     * </p>
     */
    public void setupTabLayoutWithViewPager() {
        tabs.setTabsFromPagerAdapter(adapter);
        pager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabs));
        tabs.setOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(pager));

        if (adapter.getCount() > 0) {
            final int curItem = pager.getCurrentItem();
            if (tabs.getSelectedTabPosition() != curItem) {
                tabs.getTabAt(curItem).select();
            }
        }
    }

    /**
     *
     */
    private void configureOrientation() {
        String rotateDefault;
        if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS)
            rotateDefault = PreferenceConstants.ROTATION_PORTRAIT;
        else
            rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE;

        String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault);
        if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate))
            rotate = rotateDefault;

        // request a forced orientation if requested by user
        if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            forcedOrientation = true;
        } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            forcedOrientation = true;
        } else {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
            forcedOrientation = false;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        TerminalView view = adapter.getCurrentTerminalView();
        final boolean activeTerminal = view != null;
        boolean sessionOpen = false;
        boolean disconnected = false;
        boolean canForwardPorts = false;

        if (activeTerminal) {
            TerminalBridge bridge = view.bridge;
            sessionOpen = bridge.isSessionOpen();
            disconnected = bridge.isDisconnected();
            canForwardPorts = bridge.canFowardPorts();
        }

        menu.setQwertyMode(true);

        disconnect = menu.add(R.string.list_host_disconnect);
        if (hardKeyboard)
            disconnect.setAlphabeticShortcut('w');
        if (!sessionOpen && disconnected)
            disconnect.setTitle(R.string.console_menu_close);
        disconnect.setEnabled(activeTerminal);
        disconnect.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
        disconnect.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                // disconnect or close the currently visible session
                TerminalView terminalView = adapter.getCurrentTerminalView();
                TerminalBridge bridge = terminalView.bridge;

                bridge.dispatchDisconnect(true);
                return true;
            }
        });

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            copy = menu.add(R.string.console_menu_copy);
            if (hardKeyboard)
                copy.setAlphabeticShortcut('c');
            MenuItemCompat.setShowAsAction(copy, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
            copy.setIcon(R.drawable.ic_action_copy);
            copy.setEnabled(activeTerminal);
            copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
                public boolean onMenuItemClick(MenuItem item) {
                    adapter.getCurrentTerminalView().startPreHoneycombCopyMode();
                    Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG)
                            .show();
                    return true;
                }
            });
        }

        paste = menu.add(R.string.console_menu_paste);
        if (hardKeyboard)
            paste.setAlphabeticShortcut('v');
        MenuItemCompat.setShowAsAction(paste, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        paste.setIcon(R.drawable.ic_action_paste);
        paste.setEnabled(activeTerminal);
        paste.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                pasteIntoTerminal();
                return true;
            }
        });

        portForward = menu.add(R.string.console_menu_portforwards);
        if (hardKeyboard)
            portForward.setAlphabeticShortcut('f');
        portForward.setIcon(android.R.drawable.ic_menu_manage);
        portForward.setEnabled(sessionOpen && canForwardPorts);
        portForward.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                TerminalView terminalView = adapter.getCurrentTerminalView();
                TerminalBridge bridge = terminalView.bridge;

                Intent intent = new Intent(ConsoleActivity.this, PortForwardListActivity.class);
                intent.putExtra(Intent.EXTRA_TITLE, bridge.host.getId());
                ConsoleActivity.this.startActivityForResult(intent, REQUEST_EDIT);
                return true;
            }
        });

        urlscan = menu.add(R.string.console_menu_urlscan);
        if (hardKeyboard)
            urlscan.setAlphabeticShortcut('u');
        urlscan.setIcon(android.R.drawable.ic_menu_search);
        urlscan.setEnabled(activeTerminal);
        urlscan.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                final TerminalView terminalView = adapter.getCurrentTerminalView();

                List<String> urls = terminalView.bridge.scanForURLs();

                Dialog urlDialog = new Dialog(ConsoleActivity.this);
                urlDialog.setTitle(R.string.console_menu_urlscan);

                ListView urlListView = new ListView(ConsoleActivity.this);
                URLItemListener urlListener = new URLItemListener(ConsoleActivity.this);
                urlListView.setOnItemClickListener(urlListener);

                urlListView.setAdapter(
                        new ArrayAdapter<String>(ConsoleActivity.this, android.R.layout.simple_list_item_1, urls));
                urlDialog.setContentView(urlListView);
                urlDialog.show();

                return true;
            }
        });

        resize = menu.add(R.string.console_menu_resize);
        if (hardKeyboard)
            resize.setAlphabeticShortcut('s');
        resize.setIcon(android.R.drawable.ic_menu_crop);
        resize.setEnabled(sessionOpen);
        resize.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                final TerminalView terminalView = adapter.getCurrentTerminalView();

                @SuppressLint("InflateParams") // Dialogs do not have a parent view.
                final View resizeView = inflater.inflate(R.layout.dia_resize, null, false);
                new android.support.v7.app.AlertDialog.Builder(ConsoleActivity.this, R.style.AlertDialogTheme)
                        .setView(resizeView)
                        .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                int width, height;
                                try {
                                    width = Integer.parseInt(
                                            ((EditText) resizeView.findViewById(R.id.width)).getText().toString());
                                    height = Integer.parseInt(
                                            ((EditText) resizeView.findViewById(R.id.height)).getText().toString());
                                } catch (NumberFormatException nfe) {
                                    // TODO change this to a real dialog where we can
                                    // make the input boxes turn red to indicate an error.
                                    return;
                                }

                                terminalView.forceSize(width, height);
                            }
                        }).setNegativeButton(android.R.string.cancel, null).create().show();

                return true;
            }
        });

        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);

        final TerminalView view = adapter.getCurrentTerminalView();
        boolean activeTerminal = view != null;
        boolean sessionOpen = false;
        boolean disconnected = false;
        boolean canForwardPorts = false;

        if (activeTerminal) {
            TerminalBridge bridge = view.bridge;
            sessionOpen = bridge.isSessionOpen();
            disconnected = bridge.isDisconnected();
            canForwardPorts = bridge.canFowardPorts();
        }

        disconnect.setEnabled(activeTerminal);
        if (sessionOpen || !disconnected)
            disconnect.setTitle(R.string.list_host_disconnect);
        else
            disconnect.setTitle(R.string.console_menu_close);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            copy.setEnabled(activeTerminal);
        }
        paste.setEnabled(activeTerminal);
        portForward.setEnabled(sessionOpen && canForwardPorts);
        urlscan.setEnabled(activeTerminal);
        resize.setEnabled(sessionOpen);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
            Intent intent = new Intent(this, HostListActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onOptionsMenuClosed(Menu menu) {
        super.onOptionsMenuClosed(menu);

        setVolumeControlStream(AudioManager.STREAM_MUSIC);
    }

    @Override
    public void onStart() {
        super.onStart();

        // connect with manager service to find all bridges
        // when connected it will insert all views
        bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, "onPause called");

        if (forcedOrientation && bound != null) {
            bound.setResizeAllowed(false);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, "onResume called");

        // Make sure we don't let the screen fall asleep.
        // This also keeps the Wi-Fi chipset from disconnecting us.
        if (prefs.getBoolean(PreferenceConstants.KEEP_ALIVE, true)) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        } else {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }

        configureOrientation();

        if (forcedOrientation && bound != null) {
            bound.setResizeAllowed(true);
        }
    }

    /* (non-Javadoc)
     * @see android.app.Activity#onNewIntent(android.content.Intent)
     */
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        Log.d(TAG, "onNewIntent called");

        requested = intent.getData();

        if (requested == null) {
            Log.e(TAG, "Got null intent data in onNewIntent()");
            return;
        }

        if (bound == null) {
            Log.e(TAG, "We're not bound in onNewIntent()");
            return;
        }

        TerminalBridge requestedBridge = bound.getConnectedBridge(requested.getFragment());
        int requestedIndex = 0;

        synchronized (pager) {
            if (requestedBridge == null) {
                // If we didn't find the requested connection, try opening it

                try {
                    Log.d(TAG,
                            String.format(
                                    "We couldnt find an existing bridge with URI=%s (nickname=%s),"
                                            + "so creating one now",
                                    requested.toString(), requested.getFragment()));
                    bound.openConnection(requested);
                } catch (Exception e) {
                    Log.e(TAG, "Problem while trying to create new requested bridge from URI", e);
                    // TODO: We should display an error dialog here.
                    return;
                }

                adapter.notifyDataSetChanged();
                requestedIndex = adapter.getCount();
            } else {
                final int flipIndex = bound.getBridges().indexOf(requestedBridge);
                if (flipIndex > requestedIndex) {
                    requestedIndex = flipIndex;
                }
            }

            setDisplayedTerminal(requestedIndex);
        }
    }

    @Override
    public void onStop() {
        super.onStop();

        unbindService(connection);
    }

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        // Maintain selected host if connected.
        TerminalView currentTerminalView = adapter.getCurrentTerminalView();
        if (currentTerminalView != null && !currentTerminalView.bridge.isDisconnected()) {
            requested = currentTerminalView.bridge.host.getUri();
            savedInstanceState.putString(STATE_SELECTED_URI, requested.toString());
        }

        super.onSaveInstanceState(savedInstanceState);
    }

    /**
     * Save the currently shown {@link TerminalView} as the default. This is
     * saved back down into {@link TerminalManager} where we can read it again
     * later.
     */
    private void updateDefault() {
        // update the current default terminal
        TerminalView view = adapter.getCurrentTerminalView();
        if (view == null || bound == null) {
            return;
        }
        bound.defaultBridge = view.bridge;
    }

    protected void updateEmptyVisible() {
        // update visibility of empty status message
        empty.setVisibility((pager.getChildCount() == 0) ? View.VISIBLE : View.GONE);
    }

    /**
     * Show any prompts requested by the currently visible {@link TerminalView}.
     */
    protected void updatePromptVisible() {
        // check if our currently-visible terminalbridge is requesting any prompt services
        TerminalView view = adapter.getCurrentTerminalView();

        // Hide all the prompts in case a prompt request was canceled
        hideAllPrompts();

        if (view == null) {
            // we dont have an active view, so hide any prompts
            return;
        }

        PromptHelper prompt = view.bridge.promptHelper;
        if (String.class.equals(prompt.promptRequested)) {
            stringPromptGroup.setVisibility(View.VISIBLE);

            String instructions = prompt.promptInstructions;
            if (instructions != null && instructions.length() > 0) {
                stringPromptInstructions.setVisibility(View.VISIBLE);
                stringPromptInstructions.setText(instructions);
            } else
                stringPromptInstructions.setVisibility(View.GONE);
            stringPrompt.setText("");
            stringPrompt.setHint(prompt.promptHint);
            stringPrompt.requestFocus();

        } else if (Boolean.class.equals(prompt.promptRequested)) {
            booleanPromptGroup.setVisibility(View.VISIBLE);
            booleanPrompt.setText(prompt.promptHint);
            booleanYes.requestFocus();

        } else {
            hideAllPrompts();
            view.requestFocus();
        }
    }

    private class URLItemListener implements OnItemClickListener {
        private WeakReference<Context> contextRef;

        URLItemListener(Context context) {
            this.contextRef = new WeakReference<Context>(context);
        }

        public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
            Context context = contextRef.get();

            if (context == null)
                return;

            try {
                TextView urlView = (TextView) view;

                String url = urlView.getText().toString();
                if (url.contains("://"))
                    url = "http://" + url;

                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                context.startActivity(intent);
            } catch (Exception e) {
                Log.e(TAG, "couldn't open URL", e);
                // We should probably tell the user that we couldn't find a handler...
            }
        }

    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        Log.d(TAG, String.format("onConfigurationChanged; requestedOrientation=%d, newConfig.orientation=%d",
                getRequestedOrientation(), newConfig.orientation));
        if (bound != null) {
            if (forcedOrientation
                    && (newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE
                            && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
                    || (newConfig.orientation != Configuration.ORIENTATION_PORTRAIT
                            && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT))
                bound.setResizeAllowed(false);
            else
                bound.setResizeAllowed(true);

            bound.hardKeyboardHidden = (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);

            mKeyboardButton.setVisibility(bound.hardKeyboardHidden ? View.VISIBLE : View.GONE);
        }
    }

    /**
     * Called whenever the displayed terminal is changed.
     */
    private void onTerminalChanged() {
        View terminalNameOverlay = findCurrentView(R.id.terminal_name_overlay);
        if (terminalNameOverlay != null)
            terminalNameOverlay.startAnimation(fade_out_delayed);
        updateDefault();
        updatePromptVisible();
        ActivityCompat.invalidateOptionsMenu(ConsoleActivity.this);
    }

    /**
     * Displays the child in the ViewPager at the requestedIndex and updates the prompts.
     *
     * @param requestedIndex the index of the terminal view to display
     */
    private void setDisplayedTerminal(int requestedIndex) {
        pager.setCurrentItem(requestedIndex);
        // set activity title
        setTitle(adapter.getPageTitle(requestedIndex));
        onTerminalChanged();
    }

    private void pasteIntoTerminal() {
        // force insert of clipboard text into current console
        TerminalView terminalView = adapter.getCurrentTerminalView();
        TerminalBridge bridge = terminalView.bridge;

        // pull string from clipboard and generate all events to force down
        String clip = "";
        if (clipboard.hasText()) {
            clip = clipboard.getText().toString();
        }
        bridge.injectString(clip);
    }

    public class TerminalPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            if (bound != null) {
                return bound.getBridges().size();
            } else {
                return 0;
            }
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            if (bound == null || bound.getBridges().size() <= position) {
                Log.w(TAG, "Activity not bound when creating TerminalView.");
            }
            TerminalBridge bridge = bound.getBridges().get(position);
            bridge.promptHelper.setHandler(promptHandler);

            // inflate each terminal view
            RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.item_terminal, container, false);

            // set the terminal name overlay text
            TextView terminalNameOverlay = (TextView) view.findViewById(R.id.terminal_name_overlay);
            terminalNameOverlay.setText(bridge.host.getNickname());

            // and add our terminal view control, using index to place behind overlay
            final TerminalView terminal = new TerminalView(container.getContext(), bridge, pager);
            terminal.setId(R.id.terminal_view);
            view.addView(terminal, 0);

            // Tag the view with its bridge so it can be retrieved later.
            view.setTag(bridge);

            container.addView(view);
            terminalNameOverlay.startAnimation(fade_out_delayed);
            return view;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            final View view = (View) object;

            container.removeView(view);
        }

        @Override
        public int getItemPosition(Object object) {
            if (bound == null) {
                return POSITION_NONE;
            }

            View view = (View) object;
            TerminalView terminal = (TerminalView) view.findViewById(R.id.terminal_view);
            HostBean host = terminal.bridge.host;

            int itemIndex = POSITION_NONE;
            int i = 0;
            for (TerminalBridge bridge : bound.getBridges()) {
                if (bridge.host.equals(host)) {
                    itemIndex = i;
                    break;
                }
                i++;
            }
            return itemIndex;
        }

        public TerminalBridge getBridgeAtPosition(int position) {
            if (bound == null) {
                return null;
            }

            ArrayList<TerminalBridge> bridges = bound.getBridges();
            if (position < 0 || position >= bridges.size()) {
                return null;
            }
            return bridges.get(position);
        }

        @Override
        public void notifyDataSetChanged() {
            super.notifyDataSetChanged();
            if (tabs != null) {
                toolbar.setVisibility(this.getCount() > 1 ? View.VISIBLE : View.GONE);
                tabs.setTabsFromPagerAdapter(this);
            }
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        @Override
        public CharSequence getPageTitle(int position) {
            TerminalBridge bridge = getBridgeAtPosition(position);
            if (bridge == null) {
                return "???";
            }
            return bridge.host.getNickname();
        }

        public TerminalView getCurrentTerminalView() {
            View currentView = pager.findViewWithTag(getBridgeAtPosition(pager.getCurrentItem()));
            if (currentView == null) {
                return null;
            }
            return (TerminalView) currentView.findViewById(R.id.terminal_view);
        }
    }
}