/*
* 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.woltage.irssiconnectbot;
import java.util.List;
import org.woltage.irssiconnectbot.bean.HostBean;
import org.woltage.irssiconnectbot.service.TerminalBridge;
import org.woltage.irssiconnectbot.service.TerminalManager;
import org.woltage.irssiconnectbot.transport.TransportFactory;
import org.woltage.irssiconnectbot.util.HostDatabase;
import org.woltage.irssiconnectbot.util.PreferenceConstants;
import org.woltage.irssiconnectbot.util.UpdateHelper;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
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.Intent.ShortcutIconResource;
import android.content.SharedPreferences.Editor;
import android.content.res.ColorStateList;
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.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View.OnKeyListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
import com.nullwire.trace.ExceptionHandler;
public class HostListActivity extends ListActivity {
public final static int REQUEST_EDIT = 1;
public final static int REQUEST_EULA = 2;
protected TerminalManager bound = null;
protected HostDatabase hostdb;
private List<HostBean> hosts;
protected LayoutInflater inflater = null;
protected boolean sortedByColor = false;
private MenuItem sortcolor;
private MenuItem sortlast;
private Spinner transportSpinner;
private TextView quickconnect;
private SharedPreferences prefs = null;
protected boolean makingShortcut = false;
protected Handler updateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
HostListActivity.this.updateList();
}
};
private ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
bound = ((TerminalManager.TerminalBinder) service).getService();
// update our listview binder to find the service
HostListActivity.this.updateList();
}
public void onServiceDisconnected(ComponentName className) {
bound = null;
HostListActivity.this.updateList();
}
};
@Override
public void onStart() {
super.onStart();
// start the terminal manager service
this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
if(this.hostdb == null)
this.hostdb = new HostDatabase(this);
}
@Override
public void onStop() {
super.onStop();
this.unbindService(connection);
if(this.hostdb != null) {
this.hostdb.close();
this.hostdb = null;
}
}
@Override
public void onResume() {
super.onResume();
ExceptionHandler.checkForTraces(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_EULA) {
if(resultCode == Activity.RESULT_OK) {
// yay they agreed, so store that info
Editor edit = prefs.edit();
edit.putBoolean(PreferenceConstants.EULA, true);
edit.commit();
} else {
// user didnt agree, so close
this.finish();
}
} else if (requestCode == REQUEST_EDIT) {
this.updateList();
}
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.act_hostlist);
this.setTitle(String.format("%s: %s",
getResources().getText(R.string.app_name),
getResources().getText(R.string.title_hosts_list)));
// check for eula agreement
this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean agreed = prefs.getBoolean(PreferenceConstants.EULA, false);
if(!agreed) {
this.startActivityForResult(new Intent(this, WizardActivity.class), REQUEST_EULA);
}
// start thread to check for new version
new UpdateHelper(this);
this.makingShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())
|| Intent.ACTION_PICK.equals(getIntent().getAction());
// connect with hosts database and populate list
this.hostdb = new HostDatabase(this);
ListView list = this.getListView();
this.sortedByColor = prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false);
//this.list.setSelector(R.drawable.highlight_disabled_pressed);
list.setOnItemClickListener(new OnItemClickListener() {
public synchronized void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// launch off to console details
HostBean host = (HostBean) parent.getAdapter().getItem(position);
Uri uri = host.getUri();
Intent contents = new Intent(Intent.ACTION_VIEW, uri);
contents.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (makingShortcut) {
// create shortcut if requested
ShortcutIconResource icon = Intent.ShortcutIconResource.fromContext(HostListActivity.this, R.drawable.icon);
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, contents);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, host.getNickname());
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
setResult(RESULT_OK, intent);
finish();
} else {
// otherwise just launch activity to show this host
HostListActivity.this.startActivity(contents);
}
}
});
this.registerForContextMenu(list);
quickconnect = (TextView) this.findViewById(R.id.front_quickconnect);
quickconnect.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
quickconnect.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;
return startConsoleActivity();
}
});
transportSpinner = (Spinner)findViewById(R.id.transport_selection);
transportSpinner.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
ArrayAdapter<String> transportSelection = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, TransportFactory.getTransportNames());
transportSelection.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
transportSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> arg0, View view, int position, long id) {
String formatHint = TransportFactory.getFormatHint(
(String) transportSpinner.getSelectedItem(),
HostListActivity.this);
quickconnect.setHint(formatHint);
quickconnect.setError(null);
quickconnect.requestFocus();
}
public void onNothingSelected(AdapterView<?> arg0) { }
});
transportSpinner.setAdapter(transportSelection);
this.inflater = LayoutInflater.from(this);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
// don't offer menus when creating shortcut
if (makingShortcut) return true;
sortcolor.setVisible(!sortedByColor);
sortlast.setVisible(sortedByColor);
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// don't offer menus when creating shortcut
if(makingShortcut) return true;
// add host, ssh keys, about
sortcolor = menu.add(R.string.list_menu_sortcolor);
sortcolor.setIcon(android.R.drawable.ic_menu_share);
sortcolor.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
sortedByColor = true;
updateList();
return true;
}
});
sortlast = menu.add(R.string.list_menu_sortname);
sortlast.setIcon(android.R.drawable.ic_menu_share);
sortlast.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
sortedByColor = false;
updateList();
return true;
}
});
MenuItem keys = menu.add(R.string.list_menu_pubkeys);
keys.setIcon(android.R.drawable.ic_lock_lock);
keys.setIntent(new Intent(HostListActivity.this, PubkeyListActivity.class));
MenuItem colors = menu.add("Colors");
colors.setIcon(android.R.drawable.ic_menu_slideshow);
colors.setIntent(new Intent(HostListActivity.this, ColorsActivity.class));
MenuItem settings = menu.add(R.string.list_menu_settings);
settings.setIcon(android.R.drawable.ic_menu_preferences);
settings.setIntent(new Intent(HostListActivity.this, SettingsActivity.class));
MenuItem help = menu.add(R.string.title_help);
help.setIcon(android.R.drawable.ic_menu_help);
help.setIntent(new Intent(HostListActivity.this, HelpActivity.class));
return true;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
// create menu to handle hosts
// create menu to handle deleting and sharing lists
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
final HostBean host = (HostBean) this.getListView().getItemAtPosition(info.position);
menu.setHeaderTitle(host.getNickname());
// edit, disconnect, delete
MenuItem connect = menu.add(R.string.list_host_disconnect);
final TerminalBridge bridge = bound.getConnectedBridge(host);
connect.setEnabled((bridge != null));
connect.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
bridge.dispatchDisconnect(true);
updateHandler.sendEmptyMessage(-1);
return true;
}
});
MenuItem edit = menu.add(R.string.list_host_edit);
edit.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
Intent intent = new Intent(HostListActivity.this, HostEditorActivity.class);
intent.putExtra(Intent.EXTRA_TITLE, host.getId());
HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
return true;
}
});
MenuItem portForwards = menu.add(R.string.list_host_portforwards);
portForwards.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
Intent intent = new Intent(HostListActivity.this, PortForwardListActivity.class);
intent.putExtra(Intent.EXTRA_TITLE, host.getId());
HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
return true;
}
});
if (!TransportFactory.canForwardPorts(host.getProtocol()))
portForwards.setEnabled(false);
MenuItem delete = menu.add(R.string.list_host_delete);
delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// prompt user to make sure they really want this
new AlertDialog.Builder(HostListActivity.this)
.setMessage(getString(R.string.delete_message, host.getNickname()))
.setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// make sure we disconnect
if(bridge != null)
bridge.dispatchDisconnect(true);
hostdb.deleteHost(host);
updateHandler.sendEmptyMessage(-1);
}
})
.setNegativeButton(R.string.delete_neg, null).create().show();
return true;
}
});
}
/**
* @param text
* @return
*/
private boolean startConsoleActivity() {
Uri uri = TransportFactory.getUri((String) transportSpinner
.getSelectedItem(), quickconnect.getText().toString());
if (uri == null) {
quickconnect.setError(getString(R.string.list_format_error,
TransportFactory.getFormatHint(
(String) transportSpinner.getSelectedItem(),
HostListActivity.this)));
return false;
}
if (hostdb == null)
hostdb = new HostDatabase(this);
HostBean host = TransportFactory.findHost(hostdb, uri);
if (host == null) {
host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
host.setColor(HostDatabase.COLOR_GRAY);
host.setPubkeyId(HostDatabase.PUBKEYID_ANY);
hostdb.saveHost(host);
}
Intent intent = new Intent(HostListActivity.this, ConsoleActivity.class);
intent.setData(uri);
startActivity(intent);
return true;
}
protected void updateList() {
if (prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false) != sortedByColor) {
Editor edit = prefs.edit();
edit.putBoolean(PreferenceConstants.SORT_BY_COLOR, sortedByColor);
edit.commit();
}
if (hostdb == null)
hostdb = new HostDatabase(this);
hosts = hostdb.getHosts(sortedByColor);
// Don't lose hosts that are connected via shortcuts but not in the database.
if (bound != null) {
for (TerminalBridge bridge : bound.bridges) {
if (!hosts.contains(bridge.host))
hosts.add(0, bridge.host);
}
}
HostAdapter adapter = new HostAdapter(this, hosts, bound);
this.setListAdapter(adapter);
}
class HostAdapter extends ArrayAdapter<HostBean> {
private List<HostBean> hosts;
private final TerminalManager manager;
private final ColorStateList red, green, blue;
public final static int STATE_UNKNOWN = 1, STATE_CONNECTED = 2, STATE_DISCONNECTED = 3;
class ViewHolder {
public TextView nickname;
public TextView caption;
public ImageView icon;
}
public HostAdapter(Context context, List<HostBean> hosts, TerminalManager manager) {
super(context, R.layout.item_host, hosts);
this.hosts = hosts;
this.manager = manager;
red = context.getResources().getColorStateList(R.color.red);
green = context.getResources().getColorStateList(R.color.green);
blue = context.getResources().getColorStateList(R.color.blue);
}
/**
* Check if we're connected to a terminal with the given host.
*/
private int getConnectedState(HostBean host) {
// always disconnected if we dont have backend service
if (this.manager == null)
return STATE_UNKNOWN;
if (manager.getConnectedBridge(host) != null)
return STATE_CONNECTED;
if (manager.disconnected.contains(host))
return STATE_DISCONNECTED;
return STATE_UNKNOWN;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.item_host, null, false);
holder = new ViewHolder();
holder.nickname = (TextView)convertView.findViewById(android.R.id.text1);
holder.caption = (TextView)convertView.findViewById(android.R.id.text2);
holder.icon = (ImageView)convertView.findViewById(android.R.id.icon);
convertView.setTag(holder);
} else
holder = (ViewHolder) convertView.getTag();
HostBean host = hosts.get(position);
if (host == null) {
// Well, something bad happened. We can't continue.
Log.e("HostAdapter", "Host bean is null!");
holder.nickname.setText("Error during lookup");
holder.caption.setText("see 'adb logcat' for more");
return convertView;
}
holder.nickname.setText(host.getNickname());
switch (this.getConnectedState(host)) {
case STATE_UNKNOWN:
holder.icon.setImageState(new int[] { }, true);
break;
case STATE_CONNECTED:
holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true);
break;
case STATE_DISCONNECTED:
holder.icon.setImageState(new int[] { android.R.attr.state_expanded }, true);
break;
}
ColorStateList chosen = null;
if (HostDatabase.COLOR_RED.equals(host.getColor()))
chosen = this.red;
else if (HostDatabase.COLOR_GREEN.equals(host.getColor()))
chosen = this.green;
else if (HostDatabase.COLOR_BLUE.equals(host.getColor()))
chosen = this.blue;
Context context = convertView.getContext();
if (chosen != null) {
// set color normally if not selected
holder.nickname.setTextColor(chosen);
holder.caption.setTextColor(chosen);
} else {
// selected, so revert back to default black text
holder.nickname.setTextAppearance(context, android.R.attr.textAppearanceLarge);
holder.caption.setTextAppearance(context, android.R.attr.textAppearanceSmall);
}
long now = System.currentTimeMillis() / 1000;
String nice = context.getString(R.string.bind_never);
if (host.getLastConnect() > 0) {
int minutes = (int)((now - host.getLastConnect()) / 60);
if (minutes >= 60) {
int hours = (minutes / 60);
if (hours >= 24) {
int days = (hours / 24);
nice = context.getString(R.string.bind_days, days);
} else
nice = context.getString(R.string.bind_hours, hours);
} else
nice = context.getString(R.string.bind_minutes, minutes);
}
holder.caption.setText(nice);
return convertView;
}
}
}