/*
* 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 net.danopia.protonet;
import net.danopia.protonet.service.TerminalBridge;
import net.danopia.protonet.service.TerminalManager;
import net.danopia.protonet.util.PreferenceConstants;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
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.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.ViewFlipper;
public class ConsoleActivity extends Activity {
public final static String TAG = "ConnectBot.ConsoleActivity";
protected static final int REQUEST_EDIT = 1;
// 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;
protected Uri requested;
private TextView empty;
private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out, fade_stay_hidden, fade_out_delayed;
private float lastX, lastY;
private MenuItem disconnect, channel;
private int lastTouchRow, lastTouchCol;
private Handler handler = new Handler();
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()));
// clear out any existing bridges and record requested index
flip.removeAllViews();
final String requestedNickname = (requested != null) ? requested.getFragment() : null;
int requestedIndex = 0;
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;
}
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 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);
}
};
/**
* @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);
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
this.setContentView(R.layout.act_console);
prefs = PreferenceManager.getDefaultSharedPreferences(this);
// 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);
// 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);
// 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 (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;
}
});
}
@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;
if (activeTerminal) {
TerminalBridge bridge = ((TerminalView) view).bridge;
sessionOpen = bridge.isSessionOpen();
disconnected = bridge.isDisconnected();
}
menu.setQwertyMode(true);
disconnect = menu.add(R.string.list_host_disconnect);
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;
}
});
channel = menu.add(R.string.console_menu_channels);
channel.setAlphabeticShortcut('f');
channel.setIcon(android.R.drawable.ic_menu_manage);
channel.setEnabled(sessionOpen);
channel.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, ChannelListActivity.class);
intent.putExtra(Intent.EXTRA_TITLE, bridge.host.getId());
ConsoleActivity.this.startActivityForResult(intent, REQUEST_EDIT);
return true;
}
});
return true;
}
@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;
if (activeTerminal) {
TerminalBridge bridge = ((TerminalView) view).bridge;
sessionOpen = bridge.isSessionOpen();
disconnected = bridge.isDisconnected();
}
disconnect.setEnabled(activeTerminal);
if (sessionOpen || !disconnected)
disconnect.setTitle(R.string.list_host_disconnect);
else
disconnect.setTitle(R.string.console_menu_close);
channel.setEnabled(activeTerminal);
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);
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause called");
}
@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);
}
}
/* (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);
}
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);
}
}
}
/**
* 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);
}
@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) {
bound.hardKeyboardHidden = (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);
}
}
/**
* 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) {
// 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);
}
updateEmptyVisible();
}
}
}