/* * 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.iiordanov.bssh; import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.app.AlertDialog; 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.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.preference.PreferenceManager; import android.text.ClipboardManager; import android.text.InputType; import android.text.method.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; import android.util.Log; import android.view.GestureDetector; 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.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import android.widget.ViewFlipper; import com.iiordanov.bssh.bean.SelectionArea; import com.iiordanov.bssh.service.PromptHelper; import com.iiordanov.bssh.service.TerminalBridge; import com.iiordanov.bssh.service.TerminalKeyListener; import com.iiordanov.bssh.service.TerminalManager; import com.iiordanov.bssh.util.FileChooser; import com.iiordanov.bssh.util.FileChooserCallback; import com.iiordanov.bssh.util.PreferenceConstants; import com.iiordanov.bssh.util.TransferThread; import de.mud.terminal.vt320; public class ConsoleActivity extends Activity implements FileChooserCallback { public final static String TAG = "ConnectBot.ConsoleActivity"; protected static final int REQUEST_EDIT = 1; private static final int CLICK_TIME = 400; private static final float MAX_CLICK_DISTANCE = 25f; private static final int KEYBOARD_DISPLAY_TIME = 4500; // Direction to shift the ViewFlipper private static final int SHIFT_LEFT = 0; private static final int SHIFT_RIGHT = 1; protected ViewFlipper flip = null; protected TerminalManager bound = 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; // determines whether we are in the fullscreen mode private static final int FULLSCREEN_ON = 1; private static final int FULLSCREEN_OFF = 2; private int fullScreen; protected Uri requested; protected ClipboardManager clipboard; private RelativeLayout stringPromptGroup; protected EditText stringPrompt; private TextView stringPromptInstructions; private RelativeLayout booleanPromptGroup; private TextView booleanPrompt; private Button booleanYes, booleanNo; private TextView empty; private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out, fade_stay_hidden, fade_out_delayed; private Animation keyboard_fade_in, keyboard_fade_out; private float lastX, lastY; private InputMethodManager inputManager; private MenuItem disconnect, copy, paste, portForward, resize, urlscan, screenCapture, download, upload; protected TerminalBridge copySource = null; private int lastTouchRow, lastTouchCol; private boolean forcedOrientation; private Handler handler = new Handler(); private ImageView mKeyboardButton; 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.disconnectHandler = disconnectHandler; Log.d(TAG, String.format("Connected to TerminalManager and found bridges.size=%d", bound.bridges.size())); bound.setResizeAllowed(true); // set fullscreen value if (bound.getFullScreen() == 0) { if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) setFullScreen(FULLSCREEN_ON); else setFullScreen(FULLSCREEN_OFF); } else if (fullScreen != bound.getFullScreen()) setFullScreen(bound.getFullScreen()); // clear out any existing bridges and record requested index flip.removeAllViews(); final String requestedNickname = (requested != null) ? requested.getFragment() : null; int requestedIndex = -1; 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 for (TerminalBridge bridge : bound.bridges) { final int currentIndex = addNewTerminalView(bridge); // check to see if this bridge was requested if (bridge == requestedBridge) { requestedIndex = currentIndex; // store this bridge as default bridge bound.defaultBridge = bridge; } } // if no bridge was requested, try using default bridge if (requestedIndex < 0) { requestedIndex = getFlipIndex(bound.defaultBridge); if (requestedIndex < 0) requestedIndex = 0; } setDisplayedTerminal(requestedIndex); } public void onServiceDisconnected(ComponentName className) { // tell each bridge to forget about our prompt handler synchronized (bound.bridges) { for(TerminalBridge bridge : bound.bridges) bridge.promptHelper.setHandler(null); } flip.removeAllViews(); updateEmptyVisible(); bound = null; } }; protected Handler promptHandler = new Handler() { @Override public void handleMessage(Message msg) { // someone below us requested to display a prompt updatePromptVisible(); } }; protected Handler disconnectHandler = new Handler() { @Override public void handleMessage(Message msg) { Log.d(TAG, "Someone sending HANDLE_DISCONNECT to parentHandler"); // someone below us requested to display a password dialog // they are sending nickname and requested TerminalBridge bridge = (TerminalBridge)msg.obj; if (bridge.isAwaitingClose()) closeBridge(bridge); } }; private Runnable fadeOutOnScreenKeys = new Runnable() { public void run() { final RelativeLayout keyboardGroup = (RelativeLayout) findViewById(R.id.keyboard_group); if (keyboardGroup == null || keyboardGroup.getVisibility() == View.GONE) return; keyboardGroup.startAnimation(keyboard_fade_out); keyboardGroup.setVisibility(View.GONE); } }; /** * @param bridge */ private void closeBridge(final TerminalBridge bridge) { synchronized (flip) { final int flipIndex = getFlipIndex(bridge); if (flipIndex >= 0) { if (flip.getDisplayedChild() == flipIndex) { shiftCurrentTerminal(SHIFT_LEFT); } flip.removeViewAt(flipIndex); /* TODO Remove this workaround when ViewFlipper is fixed to listen * to view removals. Android Issue 1784 */ final int numChildren = flip.getChildCount(); if (flip.getDisplayedChild() >= numChildren && numChildren > 0) { flip.setDisplayedChild(numChildren - 1); } updateEmptyVisible(); } // If we just closed the last bridge, go back to the previous activity. if (flip.getChildCount() == 0) { finish(); } } } protected View findCurrentView(int id) { View view = flip.getCurrentView(); if(view == null) return null; return view.findViewById(id); } protected PromptHelper getCurrentPromptHelper() { View view = findCurrentView(R.id.console_flip); if(!(view instanceof TerminalView)) return null; return ((TerminalView)view).bridge.promptHelper; } protected void hideAllPrompts() { stringPromptGroup.setVisibility(View.GONE); booleanPromptGroup.setVisibility(View.GONE); // adjust window back if size was changed during prompt input View view = findCurrentView(R.id.console_flip); if(!(view instanceof TerminalView)) return; ((TerminalView)view).bridge.parentChanged((TerminalView)view); } // more like configureLaxMode -- enable network IO on UI thread private void configureStrictMode() { try { Class.forName("android.os.StrictMode"); StrictModeSetup.run(); } catch (ClassNotFoundException e) { } } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); configureStrictMode(); hardKeyboard = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY; this.setContentView(R.layout.act_console); clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE); prefs = PreferenceManager.getDefaultSharedPreferences(this); // hide action bar if requested by user // if (!PreferenceConstants.PRE_HONEYCOMB && prefs.getBoolean(PreferenceConstants.HIDE_ACTIONBAR, false)) // this.getActionBar().hide(); // TODO find proper way to disable volume key beep if it exists. setVolumeControlStream(AudioManager.STREAM_MUSIC); // handle requested console from incoming intent requested = getIntent().getData(); inflater = LayoutInflater.from(this); flip = (ViewFlipper)findViewById(R.id.console_flip); 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(); } }); 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(); } }); // preload animations for terminal switching slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in); slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out); slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in); slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out); fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed); fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden); // 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); inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); final RelativeLayout keyboardGroup = (RelativeLayout) findViewById(R.id.keyboard_group); mKeyboardButton = (ImageView) findViewById(R.id.button_keyboard); mKeyboardButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED); keyboardGroup.setVisibility(View.GONE); } }); final ImageView symButton = (ImageView) findViewById(R.id.button_sym); symButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); terminal.bridge.showCharPickerDialog(); keyboardGroup.setVisibility(View.GONE); } }); symButton.setOnLongClickListener(new OnLongClickListener() { public boolean onLongClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return false; TerminalView terminal = (TerminalView)flip; terminal.bridge.showArrowsDialog(); return true; } }); final ImageView mInputButton = (ImageView) findViewById(R.id.button_input); mInputButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; final TerminalView terminal = (TerminalView)flip; Thread promptThread = new Thread(new Runnable() { public void run() { String inj = getCurrentPromptHelper().requestStringPrompt(null, ""); terminal.bridge.injectString(inj); } }); promptThread.setName("Prompt"); promptThread.setDaemon(true); promptThread.start(); keyboardGroup.setVisibility(View.GONE); } }); final ImageView tabButton = (ImageView) findViewById(R.id.button_tab); tabButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.sendTab(); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); final ImageView ctrlButton = (ImageView) findViewById(R.id.button_ctrl); ctrlButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.metaPress(TerminalKeyListener.META_CTRL_ON); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); ctrlButton.setOnLongClickListener(new OnLongClickListener() { public boolean onLongClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return false; TerminalView terminal = (TerminalView)flip; terminal.bridge.showCtrlDialog(); return true; } }); final ImageView escButton = (ImageView) findViewById(R.id.button_esc); escButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.sendEscape(); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); escButton.setOnLongClickListener(new OnLongClickListener() { public boolean onLongClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return false; TerminalView terminal = (TerminalView)flip; terminal.bridge.showFKeysDialog(); return true; } }); final ImageView upButton = (ImageView) findViewById(R.id.button_up); upButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.sendUp(); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); final ImageView leftButton = (ImageView) findViewById(R.id.button_left); leftButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.sendLeft(); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); final ImageView downButton = (ImageView) findViewById(R.id.button_down); downButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.sendDown(); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); final ImageView rightButton = (ImageView) findViewById(R.id.button_right); rightButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.sendRight(); ConsoleActivity.this.handler.removeCallbacks(fadeOutOnScreenKeys); ConsoleActivity.this.handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } }); // detect fling gestures to switch between terminals final GestureDetector detect = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { private float totalY = 0; @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { final float distx = e2.getRawX() - e1.getRawX(); final float disty = e2.getRawY() - e1.getRawY(); final int goalwidth = flip.getWidth() / 2; // need to slide across half of display to trigger console change // make sure user kept a steady hand horizontally if (Math.abs(disty) < (flip.getHeight() / 4)) { if (distx > goalwidth) { shiftCurrentTerminal(SHIFT_RIGHT); return true; } if (distx < -goalwidth) { shiftCurrentTerminal(SHIFT_LEFT); return true; } } return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // if copying, then ignore if (copySource != null && copySource.isSelectingForCopy()) return false; if (e1 == null || e2 == null) return false; // if releasing then reset total scroll if (e2.getAction() == MotionEvent.ACTION_UP) { totalY = 0; } // activate consider if within x tolerance if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) { View flip = findCurrentView(R.id.console_flip); if(flip == null) return false; TerminalView terminal = (TerminalView)flip; // estimate how many rows we have scrolled through // accumulate distance that doesn't trigger immediate scroll totalY += distanceY; final int moved = (int)(totalY / terminal.bridge.charHeight); // consume as scrollback only if towards right half of screen if (e2.getX() > flip.getWidth() / 2) { if (moved != 0) { int base = terminal.bridge.buffer.getWindowBase(); terminal.bridge.buffer.setWindowBase(base + moved); totalY = 0; return true; } } else { // otherwise consume as pgup/pgdown for every 5 lines if (moved > 5) { ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0); terminal.bridge.tryKeyVibrate(); totalY = 0; return true; } else if (moved < -5) { ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0); terminal.bridge.tryKeyVibrate(); totalY = 0; return true; } } } return false; } /* * Enables longpress and popups menu * * @see * android.view.GestureDetector.SimpleOnGestureListener# * onLongPress(android.view.MotionEvent) * * @return void */ @Override public void onLongPress(MotionEvent e) { List<String> itemList = new ArrayList<String>(); final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); if (terminalView == null) return; final TerminalBridge bridge = terminalView.bridge; if (fullScreen == FULLSCREEN_ON) itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_disable_full_screen_mode)); else itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_enable_full_screen_mode)); itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_change_font_size)); if (prefs.getBoolean(PreferenceConstants.EXTENDED_LONGPRESS,false)) { itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_arrows_dialog)); itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_fkeys_dialog)); itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_ctrl_dialog)); itemList.add(ConsoleActivity.this .getResources().getString(R.string.longpress_sym_dialog)); } if (itemList.size() > 0) { AlertDialog.Builder builder = new AlertDialog.Builder(ConsoleActivity.this); builder.setTitle(R.string.longpress_select_action); builder.setItems(itemList.toArray(new CharSequence[itemList.size()]), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { switch (item) { case 0: if (fullScreen == FULLSCREEN_ON) { setFullScreen(FULLSCREEN_OFF); } else setFullScreen(FULLSCREEN_ON); break; case 1: bridge.showFontSizeDialog(); break; case 2: bridge.showArrowsDialog(); break; case 3: bridge.showFKeysDialog(); break; case 4: bridge.showCtrlDialog(); break; case 5: bridge.showCharPickerDialog(); } } }); AlertDialog alert = builder.create(); alert.show(); } } }); flip.setLongClickable(true); flip.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { // when copying, highlight the area if (copySource != null && copySource.isSelectingForCopy()) { int row = (int)Math.floor(event.getY() / copySource.charHeight); int col = (int)Math.floor(event.getX() / copySource.charWidth); SelectionArea area = copySource.getSelectionArea(); switch(event.getAction()) { case MotionEvent.ACTION_DOWN: // recording starting area if (area.isSelectingOrigin()) { area.setRow(row); area.setColumn(col); lastTouchRow = row; lastTouchCol = col; copySource.redraw(); } return true; case MotionEvent.ACTION_MOVE: /* ignore when user hasn't moved since last time so * we can fine-tune with directional pad */ if (row == lastTouchRow && col == lastTouchCol) return true; // if the user moves, start the selection for other corner area.finishSelectingOrigin(); // update selected area area.setRow(row); area.setColumn(col); lastTouchRow = row; lastTouchCol = col; copySource.redraw(); return true; case MotionEvent.ACTION_UP: /* If they didn't move their finger, maybe they meant to * select the rest of the text with the directional pad. */ if (area.getLeft() == area.getRight() && area.getTop() == area.getBottom()) { return true; } // copy selected area to clipboard String copiedText = area.copyFrom(copySource.buffer); clipboard.setText(copiedText); Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_done, copiedText.length()), Toast.LENGTH_LONG).show(); // fall through to clear state case MotionEvent.ACTION_CANCEL: // make sure we clear any highlighted area area.reset(); copySource.setSelectingForCopy(false); copySource.redraw(); return true; } } Configuration config = getResources().getConfiguration(); if (event.getAction() == MotionEvent.ACTION_DOWN) { lastX = event.getX(); lastY = event.getY(); } else if (event.getAction() == MotionEvent.ACTION_UP && event.getEventTime() - event.getDownTime() < CLICK_TIME && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) { if (keyboardGroup.getVisibility() == View.GONE) { keyboardGroup.startAnimation(keyboard_fade_in); keyboardGroup.setVisibility(View.VISIBLE); } handler.removeCallbacks(fadeOutOnScreenKeys); handler.postDelayed(fadeOutOnScreenKeys, KEYBOARD_DISPLAY_TIME); } // pass any touch events back to detector return detect.onTouchEvent(event); } }); } /** * */ private void configureOrientation() { String rotateDefault; // Automatic rotation is now the default. //if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) // rotateDefault = PreferenceConstants.ROTATION_PORTRAIT; //else // rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE; rotateDefault = PreferenceConstants.ROTATION_AUTOMATIC; 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); View view = findCurrentView(R.id.console_flip); final boolean activeTerminal = (view instanceof TerminalView); boolean sessionOpen = false; boolean disconnected = false; boolean canForwardPorts = false; boolean canTransferFiles = false; if (activeTerminal) { TerminalBridge bridge = ((TerminalView) view).bridge; sessionOpen = bridge.isSessionOpen(); disconnected = bridge.isDisconnected(); canForwardPorts = bridge.canFowardPorts(); canTransferFiles = bridge.canTransferFiles(); } 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 = (TerminalView) findCurrentView(R.id.console_flip); TerminalBridge bridge = terminalView.bridge; bridge.dispatchDisconnect(true); return true; } }); copy = menu.add(R.string.console_menu_copy); if (hardKeyboard) copy.setAlphabeticShortcut('c'); copy.setIcon(android.R.drawable.ic_menu_set_as); copy.setEnabled(activeTerminal); copy.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { // mark as copying and reset any previous bounds TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); copySource = terminalView.bridge; SelectionArea area = copySource.getSelectionArea(); area.reset(); area.setBounds(copySource.buffer.getColumns(), copySource.buffer.getRows()); copySource.setSelectingForCopy(true); // Make sure we show the initial selection copySource.redraw(); 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'); paste.setIcon(android.R.drawable.ic_menu_edit); paste.setEnabled(clipboard.hasText() && sessionOpen); paste.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { // force insert of clipboard text into current console TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); TerminalBridge bridge = terminalView.bridge; // pull string from clipboard and generate all events to force down String clip = clipboard.getText().toString(); bridge.injectString(clip); return true; } }); resize = menu.add(R.string.console_menu_resize); if (hardKeyboard) resize.setAlphabeticShortcut('r'); resize.setIcon(android.R.drawable.ic_menu_crop); resize.setEnabled(sessionOpen); resize.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); final View resizeView = inflater.inflate(R.layout.dia_resize, null, false); ((EditText) resizeView.findViewById(R.id.width)) .setText(prefs.getString(PreferenceConstants.DEFAULT_FONT_SIZE_WIDTH, "80")); ((EditText) resizeView.findViewById(R.id.height)) .setText(prefs.getString(PreferenceConstants.DEFAULT_FONT_SIZE_HEIGHT, "25")); new AlertDialog.Builder(ConsoleActivity.this) .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; } if (width > 0 && height > 0) { terminalView.forceSize(width, height); } else { new AlertDialog.Builder(ConsoleActivity.this) .setTitle(R.string.resize_error_title) .setMessage(R.string.resize_error_width_height) .setNegativeButton(R.string.button_close, null) .show(); } } }).setNeutralButton(R.string.button_resize_reset, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { terminalView.bridge.resetSize(terminalView); } }).setNegativeButton(android.R.string.cancel, null) .create().show(); return true; } }); screenCapture = menu.add(R.string.console_menu_screencapture); if (hardKeyboard) screenCapture.setAlphabeticShortcut('s'); screenCapture.setIcon(android.R.drawable.ic_menu_camera); screenCapture.setEnabled(activeTerminal); screenCapture.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); terminalView.bridge.captureScreen(); 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 = (TerminalView) findCurrentView(R.id.console_flip); 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('l'); urlscan.setIcon(android.R.drawable.ic_menu_search); urlscan.setEnabled(activeTerminal); urlscan.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { View flip = findCurrentView(R.id.console_flip); if (flip == null) return true; TerminalView terminal = (TerminalView)flip; TerminalKeyListener handler = terminal.bridge.getKeyHandler(); handler.urlScan(terminal); return true; } }); download = menu.add(R.string.console_menu_download); download.setAlphabeticShortcut('d'); download.setEnabled(sessionOpen && canTransferFiles); download.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final String downloadFolder = prefs.getString(PreferenceConstants.DOWNLOAD_FOLDER, ""); final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); final TerminalBridge bridge = terminalView.bridge; final EditText textField = new EditText(ConsoleActivity.this); new AlertDialog.Builder(ConsoleActivity.this) .setTitle(R.string.transfer_select_remote_download_title) .setMessage(R.string.transfer_select_remote_download_desc) .setView(textField) .setPositiveButton(R.string.transfer_button_download, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { TransferThread transfer = new TransferThread(ConsoleActivity.this, handler); if (!prefs.getBoolean(PreferenceConstants.BACKGROUND_FILE_TRANSFER,true)) transfer.setProgressDialogMessage(getString(R.string.transfer_downloading)); transfer.download(bridge, textField.getText().toString(), null, downloadFolder); } }).setNegativeButton(android.R.string.cancel, null).create().show(); return true; } }); upload = menu.add(R.string.console_menu_upload); upload.setAlphabeticShortcut('u'); upload.setEnabled(sessionOpen && canTransferFiles); upload.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { FileChooser.selectFile(ConsoleActivity.this, ConsoleActivity.this, FileChooser.REQUEST_CODE_SELECT_FILE, getString(R.string.file_chooser_select_file, getString(R.string.select_for_upload))); return true; } }); MenuItem help = menu.add(R.string.title_help); help.setIcon(android.R.drawable.ic_menu_help); help.setIntent(new Intent(ConsoleActivity.this, HelpActivity.class)); return true; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); switch (requestCode) { case FileChooser.REQUEST_CODE_SELECT_FILE: if (resultCode == RESULT_OK && intent != null) { Uri uri = intent.getData(); try { if (uri != null) { fileSelected(new File(URI.create(uri.toString()))); } else { String filename = intent.getDataString(); if (filename != null) { fileSelected(new File(URI.create(filename))); } } } catch (IllegalArgumentException e) { Log.e(TAG, "Couldn't read from selected file", e); } } break; } } public void fileSelected(final File f) { String destFileName; String uploadFolder = prefs.getString(PreferenceConstants.REMOTE_UPLOAD_FOLDER,null); final TransferThread transfer = new TransferThread(ConsoleActivity.this, handler); Log.d(TAG, "File chooser returned " + f); if (uploadFolder == null) uploadFolder = ""; if (!uploadFolder.equals("") && uploadFolder.charAt(uploadFolder.length()-1) != '/' ) destFileName = uploadFolder + "/" + f.getName(); else destFileName = uploadFolder + f.getName(); if (prefs.getBoolean(PreferenceConstants.UPLOAD_DESTINATION_PROMPT,true)) { final EditText fileDest = new EditText(ConsoleActivity.this); fileDest.setSingleLine(); fileDest.setText(destFileName); new AlertDialog.Builder(ConsoleActivity.this) .setTitle(R.string.transfer_select_remote_upload_dest_title) .setMessage(getResources().getString(R.string.transfer_select_remote_upload_dest_desc) + "\n" + f.getPath()) .setView(fileDest) .setPositiveButton(R.string.transfer_button_upload, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { if (!prefs.getBoolean(PreferenceConstants.BACKGROUND_FILE_TRANSFER,true)) transfer.setProgressDialogMessage(getString(R.string.transfer_uploading)); TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); TerminalBridge bridge = terminalView.bridge; File uf = new File(fileDest.getText().toString()); String name = "", parent = ""; if (uf.getParent() != null) parent = uf.getParent().toString(); if (uf.getName() != null) name = uf.getName().toString(); transfer.upload(bridge, f.toString(), name, parent); } }).setNegativeButton(android.R.string.cancel, null).create().show(); } else { if (!prefs.getBoolean(PreferenceConstants.BACKGROUND_FILE_TRANSFER,true)) transfer.setProgressDialogMessage(getString(R.string.transfer_uploading)); TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); TerminalBridge bridge = terminalView.bridge; transfer.upload(bridge, f.toString(), null, uploadFolder); } } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); setVolumeControlStream(AudioManager.STREAM_NOTIFICATION); final View view = findCurrentView(R.id.console_flip); boolean activeTerminal = (view instanceof TerminalView); boolean sessionOpen = false; boolean disconnected = false; boolean canForwardPorts = false; boolean canTransferFiles = false; if (activeTerminal) { TerminalBridge bridge = ((TerminalView) view).bridge; sessionOpen = bridge.isSessionOpen(); disconnected = bridge.isDisconnected(); canForwardPorts = bridge.canFowardPorts(); canTransferFiles = bridge.canTransferFiles(); } disconnect.setEnabled(activeTerminal); if (sessionOpen || !disconnected) disconnect.setTitle(R.string.list_host_disconnect); else disconnect.setTitle(R.string.console_menu_close); copy.setEnabled(activeTerminal); paste.setEnabled(clipboard.hasText() && sessionOpen); portForward.setEnabled(sessionOpen && canForwardPorts); urlscan.setEnabled(activeTerminal); resize.setEnabled(sessionOpen); download.setEnabled(sessionOpen && canTransferFiles); upload.setEnabled(sessionOpen && canTransferFiles); return true; } @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); if (getResources().getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) { this.mKeyboardButton.setVisibility(View.GONE); } } @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 (flip) { 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())); requestedBridge = 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; } requestedIndex = addNewTerminalView(requestedBridge); } else { final int flipIndex = getFlipIndex(requestedBridge); if (flipIndex > requestedIndex) { requestedIndex = flipIndex; } } setDisplayedTerminal(requestedIndex); } } @Override public void onStop() { super.onStop(); unbindService(connection); } protected void shiftCurrentTerminal(final int direction) { View overlay; synchronized (flip) { boolean shouldAnimate = flip.getChildCount() > 1; // Only show animation if there is something else to go to. if (shouldAnimate) { // keep current overlay from popping up again overlay = findCurrentView(R.id.terminal_overlay); if (overlay != null) overlay.startAnimation(fade_stay_hidden); if (direction == SHIFT_LEFT) { flip.setInAnimation(slide_left_in); flip.setOutAnimation(slide_left_out); flip.showNext(); } else if (direction == SHIFT_RIGHT) { flip.setInAnimation(slide_right_in); flip.setOutAnimation(slide_right_out); flip.showPrevious(); } } ConsoleActivity.this.updateDefault(); if (shouldAnimate) { // show overlay on new slide and start fade overlay = findCurrentView(R.id.terminal_overlay); if (overlay != null) overlay.startAnimation(fade_out_delayed); } updatePromptVisible(); } } /** * 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 View view = findCurrentView(R.id.console_flip); if(!(view instanceof TerminalView)) return; TerminalView terminal = (TerminalView)view; if(bound == null) return; bound.defaultBridge = terminal.bridge; } protected void updateEmptyVisible() { // update visibility of empty status message empty.setVisibility((flip.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 View view = findCurrentView(R.id.console_flip); // Hide all the prompts in case a prompt request was canceled hideAllPrompts(); if(!(view instanceof TerminalView)) { // we dont have an active view, so hide any prompts return; } PromptHelper prompt = ((TerminalView)view).bridge.promptHelper; if(String.class.equals(prompt.promptRequested)) { stringPromptGroup.setVisibility(View.VISIBLE); String instructions = prompt.promptInstructions; boolean password = prompt.passwordRequested; if (instructions != null && instructions.length() > 0) { stringPromptInstructions.setVisibility(View.VISIBLE); stringPromptInstructions.setText(instructions); } else stringPromptInstructions.setVisibility(View.GONE); if (password) { stringPrompt.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); stringPrompt.setTransformationMethod(PasswordTransformationMethod.getInstance()); } else { stringPrompt.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); stringPrompt.setTransformationMethod(SingleLineTransformationMethod.getInstance()); } 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(); } } @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); } } /** * Adds a new TerminalBridge to the current set of views in our ViewFlipper. * * @param bridge TerminalBridge to add to our ViewFlipper * @return the child index of the new view in the ViewFlipper */ private int addNewTerminalView(TerminalBridge bridge) { // let them know about our prompt handler services bridge.promptHelper.setHandler(promptHandler); // inflate each terminal view RelativeLayout view = (RelativeLayout)inflater.inflate(R.layout.item_terminal, flip, false); // set the terminal overlay text TextView overlay = (TextView)view.findViewById(R.id.terminal_overlay); overlay.setText(bridge.host.getNickname()); // and add our terminal view control, using index to place behind overlay TerminalView terminal = new TerminalView(ConsoleActivity.this, bridge); terminal.setId(R.id.console_flip); view.addView(terminal, 0); synchronized (flip) { // finally attach to the flipper flip.addView(view); return flip.getChildCount() - 1; } } private int getFlipIndex(TerminalBridge bridge) { synchronized (flip) { final int children = flip.getChildCount(); for (int i = 0; i < children; i++) { final View view = flip.getChildAt(i).findViewById(R.id.console_flip); if (view == null || !(view instanceof TerminalView)) { // How did that happen? continue; } final TerminalView tv = (TerminalView) view; if (tv.bridge == bridge) { return i; } } } return -1; } /** * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts. * * @param requestedIndex the index of the terminal view to display */ private void setDisplayedTerminal(int requestedIndex) { synchronized (flip) { try { // show the requested bridge if found, also fade out overlay flip.setDisplayedChild(requestedIndex); flip.getCurrentView().findViewById(R.id.terminal_overlay) .startAnimation(fade_out_delayed); } catch (NullPointerException npe) { Log.d(TAG, "View went away when we were about to display it", npe); } updatePromptVisible(); updateEmptyVisible(); } } private void setFullScreen(int fullScreen) { if (fullScreen != this.fullScreen) { if (fullScreen == FULLSCREEN_ON) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // if (!PreferenceConstants.PRE_HONEYCOMB) { // if (this.getActionBar() != null) // this.getActionBar().hide(); // } } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); // if (!PreferenceConstants.PRE_HONEYCOMB && !prefs.getBoolean(PreferenceConstants.HIDE_ACTIONBAR, false)) { // if (this.getActionBar() != null && !prefs.getBoolean(PreferenceConstants.HIDE_ACTIONBAR, false)) // this.getActionBar().show(); // } } this.fullScreen = fullScreen; if (bound != null) bound.setFullScreen(this.fullScreen); } } /* private void showActionBar() { try { if (this.getActionBar() != null) this.getActionBar().show(); } catch (Exception e) { Log.e(TAG, "Error showing ActionBar", e); } } private void hideActionBar() { try { if (this.getActionBar() != null) this.getActionBar().hide(); } catch (Exception e) { Log.e(TAG, "Error hiding ActionBar", e); } } */ }