/*
Yaaic - Yet Another Android IRC Client
Copyright 2009-2013 Sebastian Kaspari
This file is part of Yaaic.
Yaaic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Yaaic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Yaaic. If not, see <http://www.gnu.org/licenses/>.
*/
package indrora.atomic.activity;
import android.annotation.SuppressLint;
import android.support.v7.app.ActionBar;
//import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.IBinder;
import android.speech.RecognizerIntent;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.support.v7.widget.Toolbar;
import android.text.InputType;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.method.TextKeyListener;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
import org.jibble.pircbot.NickConstants;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import indrora.atomic.App;
import indrora.atomic.Atomic;
import indrora.atomic.R;
import indrora.atomic.adapter.ConversationPagerAdapter;
import indrora.atomic.adapter.MessageListAdapter;
import indrora.atomic.command.CommandParser;
import indrora.atomic.indicator.ConversationIndicator;
import indrora.atomic.indicator.ConversationTitlePageIndicator.IndicatorStyle;
import indrora.atomic.irc.IRCBinder;
import indrora.atomic.irc.IRCConnection;
import indrora.atomic.irc.IRCService;
import indrora.atomic.listener.ConversationListener;
import indrora.atomic.listener.ServerListener;
import indrora.atomic.model.Broadcast;
import indrora.atomic.model.ColorScheme;
import indrora.atomic.model.Conversation;
import indrora.atomic.model.Extra;
import indrora.atomic.model.Message;
import indrora.atomic.model.Query;
import indrora.atomic.model.Scrollback;
import indrora.atomic.model.Server;
import indrora.atomic.model.ServerInfo;
import indrora.atomic.model.Settings;
import indrora.atomic.model.Status;
import indrora.atomic.receiver.ConversationReceiver;
import indrora.atomic.receiver.ServerReceiver;
/**
* The server view with a scrollable list of all channels
*
* @author Sebastian Kaspari <sebastian@yaaic.org>
*/
public class ConversationActivity extends AppCompatActivity implements
ServiceConnection, ServerListener, ConversationListener,
OnPageChangeListener {
public static final int REQUEST_CODE_SPEECH = 99;
private static final int REQUEST_CODE_JOIN = 1;
@SuppressWarnings("unused")
private static final int REQUEST_CODE_USERS = 2;
@SuppressWarnings("unused")
private static final int REQUEST_CODE_USER = 3;
private static final int REQUEST_CODE_NICK_COMPLETION = 4;
public static final String EXTRA_TARGET = "target";
private static ColorScheme _scheme;
public static ColorScheme getScheme() {
return _scheme;
}
private int serverId;
private Server server;
private IRCBinder binder;
private ConversationReceiver channelReceiver;
private ServerReceiver serverReceiver;
private ViewPager pager;
private ConversationIndicator indicator;
private ConversationPagerAdapter pagerAdapter;
private Scrollback scrollback;
// XXX: This is ugly. This is a buffer for a channel that should be joined
// after showing the
// JoinActivity. As onActivityResult() is called before onResume() a
// "channel joined"
// broadcast may get lost as the broadcast receivers are registered in
// onResume() but the
// join command would be called in onActivityResult(). joinChannelBuffer
// will save the
// channel name in onActivityResult() and run the join command in
// onResume().
private String joinChannelBuffer;
private int historySize;
private boolean reconnectDialogActive = false;
private final OnKeyListener inputKeyListener = new OnKeyListener() {
/**
* On key pressed (input line)
*/
@Override
public boolean onKey(View view, int keyCode, KeyEvent event) {
EditText input = (EditText)view;
if( event.getAction() != KeyEvent.ACTION_DOWN ) {
return false;
}
if( keyCode == KeyEvent.KEYCODE_DPAD_UP ) {
String message = scrollback.goBack();
if( message != null ) {
input.setText(message);
}
return true;
}
if( keyCode == KeyEvent.KEYCODE_DPAD_DOWN ) {
String message = scrollback.goForward();
if( message != null ) {
input.setText(message);
}
return true;
}
if( keyCode == KeyEvent.KEYCODE_ENTER ) {
sendMessage(input.getText().toString());
// Workaround for
// a race
// condition in
// EditText
// Instead of
// calling
// input.setText("");
// See:
// -
// https://github.com/pocmo/Yaaic/issues/67
// -
// http://code.google.com/p/android/issues/detail?id=17508
TextKeyListener.clear(input.getText());
return true;
}
// Nick completion
if( keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_TAB ) {
doNickCompletion(input);
return true;
}
return false;
}
};
Settings settings;
Toolbar _mainToolbar;
/**
* On create
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
serverId = getIntent().getExtras().getInt("serverId");
server = Atomic.getInstance().getServerById(serverId);
settings = App.getSettings();
if(settings == null) {
Log.wtf("OH GOD WHY", "ALL THE SETTINGS ARE GON OH GOD WHY");
finish(); }
_scheme = new ColorScheme(
settings.getColorScheme(),
settings.getUseDarkColors()
);
if( settings.tintActionbar() ) {
setTheme(settings.getUseDarkColors() ? indrora.atomic.R.style.AppThemeDark
: indrora.atomic.R.style.AppThemeLight);
} else {
setTheme(indrora.atomic.R.style.AppThemeDark);
}
super.onCreate(savedInstanceState);
// Finish activity if server does not exist anymore - See #55
if( server == null ) {
this.finish();
}
setTitle(server.getTitle());
setContentView(R.layout.conversations);
_mainToolbar = (Toolbar)findViewById(R.id.toolbar);
_mainToolbar.inflateMenu(R.menu.conversations);
if(settings.tintActionbar()) {
_mainToolbar.setTitleTextColor(_scheme.getForeground());
_mainToolbar.setSubtitleTextColor(_scheme.getForeground());
_mainToolbar.setBackgroundColor(_scheme.getBackground());
}
this.setSupportActionBar(_mainToolbar);
boolean isLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
EditText input = (EditText)findViewById(R.id.input);
input.setOnKeyListener(inputKeyListener);
// Fix from https://groups.google.com/forum/#!topic/yaaic/Z4bXZXvW7UM
input.setOnClickListener(new EditText.OnClickListener() {
public void onClick(View v) {
openSoftKeyboard(v);
}
});
pager = (ViewPager)findViewById(R.id.pager);
pagerAdapter = new ConversationPagerAdapter(this, server);
pager.setAdapter(pagerAdapter);
final float density = getResources().getDisplayMetrics().density;
indicator = (ConversationIndicator)findViewById(R.id.titleIndicator);
indicator.setServer(server);
indicator.setTypeface(Typeface.MONOSPACE);
indicator.setViewPager(pager);
indicator.setFooterColor(_scheme.getForeground());
indicator.setFooterLineHeight(1 * density);
indicator.setFooterIndicatorHeight(3 * density);
indicator.setFooterIndicatorStyle(IndicatorStyle.Underline);
indicator.setSelectedColor(_scheme.getForeground());
indicator.setSelectedBold(true);
indicator.setBackgroundColor(_scheme.getBackground());
indicator.setVisibility(View.GONE);
indicator.setOnPageChangeListener(this);
historySize = settings.getHistorySize();
if( server.getStatus() == Status.PRE_CONNECTING ) {
server.clearConversations();
pagerAdapter.clearConversations();
server.getConversation(ServerInfo.DEFAULT_NAME).setHistorySize(
historySize);
}
float fontSize = settings.getFontSize();
indicator.setTextSize(fontSize * density);
input.setTextSize(settings.getFontSize());
input.setTypeface(Typeface.MONOSPACE);
// Optimization : cache field lookups
Collection<Conversation> mConversations = server.getConversations();
for( Conversation conversation : mConversations ) {
// Only scroll to new conversation if it was selected before
if( conversation.getStatus() == Conversation.STATUS_SELECTED ) {
onNewConversation(conversation.getName());
} else {
createNewConversation(conversation.getName());
}
}
int setInputTypeFlags = 0;
setInputTypeFlags |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
if( settings.autoCapSentences() ) {
setInputTypeFlags |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
}
if( isLandscape && settings.imeExtract() ) {
setInputTypeFlags |= InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
}
if( !settings.imeExtract() ) {
input.setImeOptions(input.getImeOptions()
| EditorInfo.IME_FLAG_NO_EXTRACT_UI);
}
input.setInputType(input.getInputType() | setInputTypeFlags);
this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
input.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus) {
openSoftKeyboard(v);
}
}
});
// Add handling for tab-completing from the input box.
int tabCompleteDrawableResource = (settings.getUseDarkColors() ? R.drawable.ic_tabcomplete_light
: R.drawable.ic_tabcomplete_dark);
// The drawable that makes up the actual clicky
final Drawable tabcompleteDrawable = getResources().getDrawable(
tabCompleteDrawableResource);
// Set the bounds to the Intrinsic width
// We'll resize this later (well, the input box will handle that for us)
tabcompleteDrawable.setBounds(0, 0,
tabcompleteDrawable.getIntrinsicWidth(),
tabcompleteDrawable.getIntrinsicWidth());
// Set the input compound drawables.
input.setCompoundDrawables(null, null, tabcompleteDrawable, null);
// Magic.
final EditText tt = input;
final ConversationActivity cv = this;
input.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// This is where we handle some things.
boolean tappedX = event.getX() > (tt.getWidth() - tt.getPaddingRight() - tabcompleteDrawable
.getIntrinsicWidth());
if (event.getAction() == MotionEvent.ACTION_UP && tappedX) {
cv.doNickCompletion(tt);
} else {
// Blarrarharhguhaguhaguhaeguahguh STFU linter.
// :3
}
return false;
}
});
setupColors();
setupIndicator();
// Create a new scrollback history
scrollback = new Scrollback();
}
@Override
protected void onNewIntent(Intent intent) {
// Debugging: Blarg our new intent.
for( String s : intent.getExtras().keySet() ) {
Log.d("ConversationActivty",
String.format("k=%s v=\"%s\"", s, intent.getExtras().get(s)));
}
// If we are not the intended server, we should swap to the intended server.
if( intent.getExtras().getInt("serverId") != serverId ) {
// Set the flag that lets us clear the top activity (killing ourselves,
// but resurrecting after the jump)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// Start the activity
startActivity(intent);
// And commit seppuku
finish();
}
super.onNewIntent(intent);
// If our new intent is to change to a target, do it.
if( intent.getExtras().containsKey(ConversationActivity.EXTRA_TARGET) ) {
ShuffleToHighlight(intent);
}
}
private void ShuffleToHighlight(Intent inten) {
// Try and find the conversation given in the intent.
String convo = inten.getExtras().getString(
ConversationActivity.EXTRA_TARGET);
Log.d("ConversationActivity", "Trying to change to conversation " + convo);
if( convo == null ) {
Log.d("ConversationActivity", "Conversation given was NULL, jump invalid");
return;
}
int convCount = pagerAdapter.getCount();
for( int idx = 0; idx < convCount; idx++ ) {
if( pagerAdapter.getItem(idx) == null )
continue;
String tConvo = pagerAdapter.getItem(idx).getName();
Log.d("ConversationActivity", "is it " + tConvo + "?");
if( tConvo.toLowerCase(Locale.US).equals(convo.toLowerCase(Locale.US)) ) {
pager.setCurrentItem(idx, false);
Log.d("ConversationActivity", "Found conversation " + tConvo);
return;
}
}
Log.d("ConversationActivity", "Didn't find conversation!?!?!!?");
}
private void setupColors() {
if( settings.tintActionbar() ) {
// the ActionBar can be tinted. This is really cool.
// Get the ActionBar
ActionBar ab = getSupportActionBar();
// Make its background drawable a ColorDrawable
ab.setBackgroundDrawable(new ColorDrawable(_scheme.getBackground()));
_mainToolbar.setBackgroundColor(_scheme.getBackground());
// Create a SpannableString from the current server.
SpannableString st = new SpannableString(server.getTitle());
// Make its forground color (through a ForgroundColorSpan) to be the
// foreground of the scheme.
// This is because you can't guarantee that the ActionBar text color and
// actionbar color aren't going to be the same.
st.setSpan(new ForegroundColorSpan(_scheme.getForeground()),
0, st.length(), SpannableString.SPAN_INCLUSIVE_INCLUSIVE);
// Now, set our spannable to be the ActionBar title.
_mainToolbar.setTitle(st);
} else {
(getSupportActionBar()).setTitle(server.getTitle());
}
EditText input = (EditText)findViewById(R.id.input);
LinearLayout lll = (LinearLayout)(input.getParent());
lll.setBackgroundColor(_scheme.getBackground());
input.setTextColor(_scheme.getForeground());
}
/**
* On resume
*/
@SuppressLint("InlinedApi")
@Override
public void onResume() {
// register the receivers as early as possible, otherwise we may drop a
// broadcast message
channelReceiver = new ConversationReceiver(server.getId(), this);
registerReceiver(channelReceiver, new IntentFilter(
Broadcast.CONVERSATION_MESSAGE));
registerReceiver(channelReceiver, new IntentFilter(
Broadcast.CONVERSATION_NEW));
registerReceiver(channelReceiver, new IntentFilter(
Broadcast.CONVERSATION_REMOVE));
registerReceiver(channelReceiver, new IntentFilter(
Broadcast.CONVERSATION_TOPIC));
registerReceiver(channelReceiver, new IntentFilter(
Broadcast.CONVERSATION_CLEAR));
serverReceiver = new ServerReceiver(this);
registerReceiver(serverReceiver, new IntentFilter(Broadcast.SERVER_UPDATE));
super.onResume();
// Start service
Intent intent = new Intent(this, IRCService.class);
intent.setAction(IRCService.ACTION_FOREGROUND);
startService(intent);
int flags = 0;
if( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH ) {
flags |= Context.BIND_ABOVE_CLIENT;
flags |= Context.BIND_IMPORTANT;
}
bindService(intent, this, flags);
// Let's be explicit about this.
((EditText)findViewById(R.id.input)).setEnabled(true);
// Optimization - cache field lookup
Collection<Conversation> mConversations = server.getConversations();
MessageListAdapter mAdapter;
// Fill view with messages that have been buffered while paused
for( Conversation conversation : mConversations ) {
String name = conversation.getName();
mAdapter = pagerAdapter.getItemAdapter(name);
if( mAdapter != null ) {
mAdapter.addBulkMessages(conversation.getBuffer());
conversation.clearBuffer();
} else {
// Was conversation created while we were paused?
if( pagerAdapter.getPositionByName(name) == -1 ) {
onNewConversation(name);
}
}
// Clear new message notifications for the selected conversation
if( conversation.getStatus() == Conversation.STATUS_SELECTED
&& conversation.getNewMentions() > 0 ) {
Intent ackIntent = new Intent(this, IRCService.class);
ackIntent.setAction(IRCService.ACTION_ACK_NEW_MENTIONS);
ackIntent.putExtra(IRCService.EXTRA_ACK_SERVERID, serverId);
ackIntent.putExtra(IRCService.EXTRA_ACK_CONVTITLE, name);
startService(ackIntent);
}
}
// Remove views for conversations that ended while we were paused
int numViews = pagerAdapter.getCount();
if( numViews > mConversations.size() ) {
for( int i = 0; i < numViews; ++i ) {
if( !mConversations.contains(pagerAdapter.getItem(i)) ) {
pagerAdapter.removeConversation(i--);
--numViews;
}
}
}
// Join channel that has been selected in JoinActivity
// (onActivityResult())
if( joinChannelBuffer != null ) {
new Thread() {
@Override
public void run() {
binder.getService().getConnection(serverId)
.joinChannel(joinChannelBuffer);
joinChannelBuffer = null;
}
}.start();
}
setupColors();
setupIndicator();
server.setIsForeground(true);
if( this.getIntent().hasExtra(ConversationActivity.EXTRA_TARGET) ) {
Log.d("ConversationActivity",
"onResume: " + (this.getIntent().getStringExtra(EXTRA_TARGET)));
}
if( getIntent().getExtras().containsKey(EXTRA_TARGET) ) {
ShuffleToHighlight(getIntent());
}
}
/**
* On Pause
*/
@Override
public void onPause() {
super.onPause();
// Mark the current visible line.
server.setIsForeground(false);
if( binder != null && binder.getService() != null ) {
binder.getService().checkServiceStatus();
}
unbindService(this);
unregisterReceiver(channelReceiver);
unregisterReceiver(serverReceiver);
// Force the OSK to go away
// This makes it so that if the implicit keyboard doesn't work, the explicit
// one
// will force close.
EditText input = (EditText)findViewById(R.id.input);
((InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(input.getWindowToken(), 0);
}
/**
* On service connected
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
this.binder = (IRCBinder)service;
// connect to irc server if connect has been requested
if( server.getStatus() == Status.PRE_CONNECTING
&& getIntent().hasExtra("connect") ) {
server.setStatus(Status.CONNECTING);
binder.connect(server);
} else {
onStatusUpdate();
}
}
/**
* On service disconnected
*/
@Override
public void onServiceDisconnected(ComponentName name) {
this.binder = null;
}
/**
* On options menu requested
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// inflate from xml
MenuInflater inflater = new MenuInflater(this);
inflater.inflate(R.menu.conversations, menu);
return true;
}
/**
* On menu item selected
*/
// @Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case android.R.id.home:
finish();
break;
case R.id.disconnect:
server.setStatus(Status.DISCONNECTED);
server.setMayReconnect(false);
binder.getService().getConnection(serverId).quitServer();
server.clearConversations();
setResult(RESULT_OK);
finish();
break;
case R.id.close:
Conversation conversationToClose = pagerAdapter.getItem(pager
.getCurrentItem());
// Make sure we part a channel when closing the channel conversation
if( conversationToClose.getType() == Conversation.TYPE_CHANNEL ) {
IRCConnection conn = binder.getService().getConnection(serverId);
conn.partChannel(conversationToClose.getName());
// server.removeConversation(conversationToClose.getName());
// onRemoveConversation(conversationToClose.getName());
} else if( conversationToClose.getType() == Conversation.TYPE_QUERY ) {
server.removeConversation(conversationToClose.getName());
onRemoveConversation(conversationToClose.getName());
} else {
Toast.makeText(this,
getResources().getString(R.string.close_server_window),
Toast.LENGTH_SHORT).show();
}
break;
case R.id.join:
startActivityForResult(new Intent(this, JoinActivity.class),
REQUEST_CODE_JOIN);
break;
/* Get users in the channel. */
case R.id.users:
Conversation conversationForUserList = pagerAdapter.getItem(pager
.getCurrentItem());
if( conversationForUserList.getType() == Conversation.TYPE_CHANNEL ) {
final String[] nicks = binder.getService()
.getConnection(server.getId())
.getUsersAsStringArray(conversationForUserList.getName());
final Context _tContext = (Context)this;
AlertDialog.Builder userlistBuilder = new AlertDialog.Builder(
_tContext);
userlistBuilder.setTitle("Users: " + nicks.length);
OnClickListener NickSelectorListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String nick = nicks[which];
// This is the OnClickListener to actually do something.
OnClickListener NickActionListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
/* ********************* */
String nicknameWithoutPrefix = removeStatusChar(nick);
final IRCConnection connection = binder.getService()
.getConnection(server.getId());
final String conversation = server.getSelectedConversation();
switch ( which ) {
case 0:
final String replyText = nicknameWithoutPrefix + ": ";
/*
* handler.post(new Runnable() {
*
* @Override public void run() {
*/
EditText input = (EditText)findViewById(R.id.input);
input.setText(replyText);
input.setSelection(replyText.length());
openSoftKeyboard(input);
input.requestFocus();
// }
// });
break;
case 1:
Conversation query = server
.getConversation(nicknameWithoutPrefix);
if( query == null ) {
// Open a query if there's none yet
query = new Query(nicknameWithoutPrefix);
query.setHistorySize(binder.getService().getSettings()
.getHistorySize());
server.addConversation(query);
Intent intent = Broadcast.createConversationIntent(
Broadcast.CONVERSATION_NEW, server.getId(),
nicknameWithoutPrefix);
binder.getService().sendBroadcast(intent);
}
break;
case 2:
connection.op(conversation, nicknameWithoutPrefix);
break;
case 3:
connection.deOp(conversation, nicknameWithoutPrefix);
break;
case 4:
connection.halfOp(conversation, nicknameWithoutPrefix);
break;
case 5:
connection.deHalfOp(conversation, nicknameWithoutPrefix);
break;
case 6:
connection.voice(conversation, nicknameWithoutPrefix);
break;
case 7:
connection.deVoice(conversation, nicknameWithoutPrefix);
break;
case 8:
connection.ban(conversation, nicknameWithoutPrefix
+ "!*@*");
break;
case 9:
connection.kick(conversation, nicknameWithoutPrefix);
break;
}
/* ********************* */
}
}; // <-- Thats all for the actions listener.
AlertDialog.Builder ActionMenuBuilder = new Builder(_tContext);
ActionMenuBuilder.setItems(R.array.user_actions,
NickActionListener);
ActionMenuBuilder.show();
}
};
ArrayList<CharSequence> coloredNicks = new ArrayList<CharSequence>();
for( String nick : nicks ) {
SpannableString ss = new SpannableString(nick);
if( NickConstants.nickPrefixes.contains(nick.charAt(0)) ) {
int drawableRes = R.drawable.user;
switch ( nick.charAt(0) ) {
case '~':
case '&':
case '@':
drawableRes = R.drawable.op;
break;
case '+':
drawableRes = R.drawable.voice;
break;
default:
drawableRes = R.drawable.user;
break;
}
Drawable d = getResources().getDrawable(drawableRes);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
ss.setSpan(new ImageSpan(d), 0, 1,
SpannableString.SPAN_INCLUSIVE_EXCLUSIVE);
}
ss.setSpan(new ForegroundColorSpan(Message.getSenderColor(nick, _scheme)),
0, nick.length(), SpannableString.SPAN_INCLUSIVE_INCLUSIVE);
coloredNicks.add(ss);
}
userlistBuilder.setItems(
coloredNicks.toArray(new CharSequence[coloredNicks.size()]),
NickSelectorListener);
userlistBuilder.show();
} else {
Toast.makeText(this,
getResources().getString(R.string.only_usable_from_channel),
Toast.LENGTH_SHORT).show();
}
break;
/* Choose a conversation option. */
case R.id.chooseConversation:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Choose Conversation");
CharSequence[] conversationsArr = new CharSequence[pagerAdapter
.getCount()];
for( int i = 0; i < pagerAdapter.getCount(); i++ ) {
Conversation c = pagerAdapter.getItem(i);
CharSequence title = (c.getName().equals("") ? server.getTitle() : c
.getName());
if( c.getNewMentions() > 0 ) {
SpannableString unread = new SpannableString("("
+ c.getNewMentions() + ")");
unread.setSpan(
new ForegroundColorSpan(getResources().getColor(
android.R.color.secondary_text_dark_nodisable)), 0,
unread.length(), SpannableString.SPAN_INCLUSIVE_INCLUSIVE);
title = TextUtils.concat(title, " ", unread);
}
conversationsArr[i] = title;
}
OnClickListener listener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Change the page here.
pager.setCurrentItem(which);
}
};
builder.setItems(conversationsArr, listener);
builder.show();
break;
}
return true;
}
/**
* Get server object assigned to this activity
*
* @return the server object
*/
public Server getServer() {
return server;
}
/**
* On conversation message
*/
@Override
public void onConversationMessage(String target) {
Conversation conversation = server.getConversation(target);
if( conversation == null ) {
// In an early state it can happen that the conversation object
// is not created yet.
return;
}
MessageListAdapter adapter = pagerAdapter.getItemAdapter(target);
while ( conversation.hasBufferedMessages() ) {
Message message = conversation.pollBufferedMessage();
if( adapter != null && message != null ) {
adapter.addMessage(message);
int status;
switch ( message.getType() ) {
case Message.TYPE_MISC:
status = Conversation.STATUS_MISC;
break;
default:
status = Conversation.STATUS_MESSAGE;
break;
}
conversation.setStatus(status);
}
}
indicator.updateStateColors();
}
/**
* On new conversation
*/
@Override
public synchronized void onNewConversation(String target) {
createNewConversation(target);
/*
pager.setCurrentItem(pagerAdapter.getPositionByName(target));
*/
}
@Override
public synchronized void onClearConversation(String target) {
pagerAdapter.getItem(pagerAdapter.getPositionByName(target)).clearHistory();
pagerAdapter.getItemAdapter(target).clear();
Log.d("ConversationActivity", "Cleared conversation " + target);
}
/**
* Create a new conversation in the pager adapter for the given target
* conversation.
*
* @param target
*/
public void createNewConversation(String target) {
Conversation cv = server.getConversation(target);
if( cv == null )
return; // Hack!
// The above stops a bug with ZNC.
pagerAdapter.addConversation(server.getConversation(target));
}
/**
* On conversation remove
*/
@Override
public void onRemoveConversation(String target) {
int position = pagerAdapter.getPositionByName(target);
if( position != -1 ) {
pagerAdapter.removeConversation(position);
pager.setCurrentItem(position - 1);
}
}
/**
* On topic change
*/
@Override
public void onTopicChanged(String target) {
// No implementation
}
/**
* On server status update
*/
@Override
public void onStatusUpdate() {
// An issue in the tracker relates to this.
// It's way too late to figure out which one.
// EditText input = (EditText) findViewById(R.id.input);
if( server.isConnected() ) {
// input.setEnabled(true);
} else {
// input.setEnabled(false);
/*
*
* If we are disconnected, we should have three times where we don't care
* to pop up the dialog:
*
* * Total network loss has occurred and we're working on reconnecting a
* server (it happens!) * The network is transient and we're waiting on
* the network to become not-transient. * The server is in the
* preconnecting phases
*/
if( server.getStatus() == Status.DISCONNECTED
&& ((settings.reconnectLoss() && binder.getService().isReconnecting(
server.getId())) || (settings.reconnectTransient() && binder
.getService().isNetworkTransient()))
|| server.getStatus() == Status.CONNECTING ) {
return;
}
// Service is not connected or initialized yet - See #54
if( binder == null || binder.getService() == null
|| binder.getService().getSettings() == null ) {
return;
}
if( !binder.getService().getSettings().isReconnectEnabled()
&& !reconnectDialogActive
&& !binder.getService().isReconnecting(serverId) ) {
reconnectDialogActive = true;
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder
.setMessage(
getResources().getString(R.string.reconnect_after_disconnect,
server.getTitle()))
.setCancelable(false)
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
if( !server.isDisconnected() ) {
reconnectDialogActive = false;
return;
}
binder.getService().getConnection(server.getId())
.setAutojoinChannels(server.getCurrentChannelNames());
server.setStatus(Status.CONNECTING);
binder.connect(server);
reconnectDialogActive = false;
}
})
.setNegativeButton(getString(R.string.negative_button),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
server.setMayReconnect(false);
reconnectDialogActive = false;
dialog.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
}
/**
* On activity result
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if( resultCode != RESULT_OK ) {
// ignore other result codes
return;
}
switch ( requestCode ) {
case REQUEST_CODE_SPEECH:
ArrayList<String> matches = data
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
if( matches.size() > 0 ) {
((EditText)findViewById(R.id.input)).setText(matches.get(0));
}
break;
case REQUEST_CODE_JOIN:
joinChannelBuffer = data.getExtras().getString("channel");
break;
case REQUEST_CODE_NICK_COMPLETION:
insertNickCompletion((EditText)findViewById(R.id.input), data
.getExtras().getString(Extra.USER));
break;
}
}
// This is a guess, based on the fact that you'll probably never get more than 100 characters
// in the preamble of a line.
private static final int MAX_MESSAGE_LENGTH = 450;
/**
* Send a message in this conversation
*
* This should actually be handled somewhere deeper.
*
* @param text The text of the message
*/
private void sendMessage(String text) {
if( text.equals("") ) {
// ignore empty messages
return;
}
// If we've gotten a multiline message,
else if( text.contains("\n") ) {
// Split the multiline message into chunks
String lines[] = text.split("\\r?\\n");
// And send each line at at time.
for( String line : lines ) {
sendMessage(line);
}
// Since we don't want to send things twice, return.
return;
}
// The line handed to us is > 200 chars (an arbitrary limit)
else if( text.length() > MAX_MESSAGE_LENGTH + 2 ) { // arbitrary limit.
for( int idx = 0; idx < text.length(); idx += MAX_MESSAGE_LENGTH ) {
String real_line = text.substring(idx,
Math.min(idx + MAX_MESSAGE_LENGTH, text.length()));
if( idx == 0 || idx + MAX_MESSAGE_LENGTH < text.length() ) {
real_line += "\u2026";
}
if( idx > 0 ) {
real_line = "\u2026" + real_line;
}
sendMessage(real_line);
}
return;
}
if( !server.isConnected() ) {
Message message = new Message(getString(R.string.message_not_connected));
message.setColor(Message.MessageColor.ERROR);
message.setIcon(R.drawable.error);
server.getConversation(server.getSelectedConversation()).addMessage(
message);
onConversationMessage(server.getSelectedConversation());
}
scrollback.addMessage(text);
Conversation conversation = pagerAdapter.getItem(pager.getCurrentItem());
if( conversation != null ) {
if( !text.startsWith("/") || text.startsWith("//")) {
if( conversation.getType() != Conversation.TYPE_SERVER ) {
String nickname = binder.getService().getConnection(serverId)
.getNick();
// conversation.addMessage(new Message("<" + nickname + "> "
// + text));
conversation.addMessage(new Message(text, nickname));
binder.getService().getConnection(serverId)
.sendMessage(conversation.getName(), text);
} else {
Message message = new Message(
getString(R.string.chat_only_form_channel));
message.setColor(Message.MessageColor.TOPIC);
message.setIcon(R.drawable.warning);
conversation.addMessage(message);
}
onConversationMessage(conversation.getName());
} else {
CommandParser.getInstance().parse(text, server, conversation,
binder.getService());
}
}
}
/**
* Complete a nick in the input line
*/
private void doNickCompletion(EditText input) {
String text = input.getText().toString();
if( text.length() <= 0 ) {
return;
}
String[] tokens = text.split("[\\s,.-]+");
if( tokens.length <= 0 ) {
return;
}
String word = tokens[tokens.length - 1].toLowerCase(Locale.US);
tokens[tokens.length - 1] = null;
int begin = input.getSelectionStart();
int end = input.getSelectionEnd();
int cursor = Math.min(begin, end);
int sel_end = Math.max(begin, end);
boolean in_selection = (cursor != sel_end);
if( in_selection ) {
word = text.substring(cursor, sel_end);
} else {
// use the word at the curent cursor position
while ( true ) {
cursor -= 1;
if( cursor <= 0 || text.charAt(cursor) == ' ' ) {
break;
}
}
if( cursor < 0 ) {
cursor = 0;
}
if( text.charAt(cursor) == ' ' ) {
cursor += 1;
}
sel_end = text.indexOf(' ', cursor);
if( sel_end == -1 ) {
sel_end = text.length();
}
word = text.substring(cursor, sel_end);
}
// Log.d("Yaaic", "Trying to complete nick: " + word);
Conversation conversationForUserList = pagerAdapter.getItem(pager
.getCurrentItem());
String[] users = null;
if( conversationForUserList.getType() == Conversation.TYPE_CHANNEL ) {
users = binder.getService().getConnection(server.getId())
.getUsersAsStringArray(conversationForUserList.getName());
}
// go through users and add matches
if( users != null ) {
final List<Integer> result = new ArrayList<Integer>();
for( int i = 0; i < users.length; i++ ) {
String nick = removeStatusChar(users[i].toLowerCase(Locale.US));
if( nick.startsWith(word.toLowerCase(Locale.US)) ) {
result.add(Integer.valueOf(i));
}
}
if( result.size() == 1 ) {
input.setSelection(cursor, sel_end);
insertNickCompletion(input, users[result.get(0).intValue()]);
} else if( result.size() > 0 ) {
// There was an ambiguity. Choose who wins.
// in yaaic, this was 80% handled by an external intent.
// I find that inelegant, since we can handle it here and win on
// low-resource
// devices (e.g. the Moto Triumph).
// This uses more of the android native resources, being a
// little less break-y.
final EditText finput = input;
final int fCursor = cursor;
final int fSelEnd = sel_end;
AlertDialog.Builder b = new Builder(this);
b.setTitle("Disambiguation");
// Get the possible users.
final String[] extra = new String[result.size()];
int i = 0;
for( Integer n : result ) {
extra[i++] = users[n.intValue()];
}
// Now, take that list of possible user and let someone choose
// who wins.
b.setItems(extra, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finput.setSelection(fCursor, fSelEnd);
insertNickCompletion((EditText)findViewById(R.id.input),
extra[which]);
}
});
// And show that selection.
b.show();
}
}
}
/**
* Insert a given nick completion into the input line
*
* @param input The input line widget, with the incomplete nick selected
* @param nick The completed nick
*/
private void insertNickCompletion(EditText input, String nick) {
int start = input.getSelectionStart();
int end = input.getSelectionEnd();
nick = removeStatusChar(nick);
if( start == 0 ) {
nick += ":";
}
nick += " ";
input.getText().replace(start, end, nick, 0, nick.length());
// put cursor after inserted text
input.setSelection(start + nick.length());
input.clearComposingText();
input.post(new Runnable() {
@Override
public void run() {
// make the softkeyboard come up again (only if no hw keyboard
// is attached)
EditText input = (EditText)findViewById(R.id.input);
openSoftKeyboard(input);
}
});
input.requestFocus();
}
/**
* Open the soft keyboard (helper function)
*/
private void openSoftKeyboard(View view) {
((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE))
.showSoftInput(view, InputMethodManager.SHOW_FORCED);
}
/**
* Remove the status char off the front of a nick if one is present
*
* @param nick
* @return nick without statuschar
*/
private static String removeStatusChar(String nick) {
int idx = 0;
while ( NickConstants.nickPrefixes.contains(nick.charAt(idx)) ) {
idx++;
}
return nick.substring(idx);
}
@Override
public void onPageScrollStateChanged(int arg0) {
}
private void hideSubtitle() {
android.support.v7.app.ActionBar ab = getSupportActionBar();
CharSequence t = ab.getTitle();
ab.setDisplayShowTitleEnabled(false);
ab.setSubtitle(null);
ab.setDisplayShowTitleEnabled(true);
ab.setTitle(t);
}
private void showSubtitle() {
ActionBar ab = getSupportActionBar();
ab.setTitle(server.getTitle());
Conversation c = pagerAdapter.getItem(pager.getCurrentItem());
String newName = c.getName();
switch ( c.getType() ) {
case Conversation.TYPE_SERVER:
newName = getString(R.string.subtitle_server);
break;
case Conversation.TYPE_CHANNEL:
newName = c.getName();
break;
case Conversation.TYPE_QUERY:
newName = String
.format(getString(R.string.subtitle_query), c.getName());
break;
default:
newName = c.getName();
}
ab.setSubtitle(newName);
}
private void setupIndicator() {
// This either:
// * Hides the pager indicator (by setting its visibility to GONE)
// * Hides the subtitle (by calling hideSubtitle() )
if( settings.showChannelBar() ) {
indicator.setVisibility(View.VISIBLE);
hideSubtitle();
} else {
indicator.setVisibility(View.GONE);
showSubtitle();
}
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageSelected(int arg0) {
if( settings.showChannelBar() == false ) {
showSubtitle();
}
}
}