533 lines
18 KiB
Java
533 lines
18 KiB
Java
/*
|
|
Yaaic - Yet Another Android IRC Client
|
|
|
|
Copyright 2009-2011 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 org.yaaic.irc;
|
|
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
|
import org.jibble.pircbot.IrcException;
|
|
import org.jibble.pircbot.NickAlreadyInUseException;
|
|
import org.yaaic.R;
|
|
import org.yaaic.Yaaic;
|
|
import org.yaaic.activity.ServersActivity;
|
|
import org.yaaic.db.Database;
|
|
import org.yaaic.model.Broadcast;
|
|
import org.yaaic.model.Conversation;
|
|
import org.yaaic.model.Message;
|
|
import org.yaaic.model.Server;
|
|
import org.yaaic.model.ServerInfo;
|
|
import org.yaaic.model.Settings;
|
|
import org.yaaic.model.Status;
|
|
|
|
import android.app.Notification;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.app.Service;
|
|
import android.content.Intent;
|
|
|
|
/**
|
|
* The background service for managing the irc connections
|
|
*
|
|
* @author Sebastian Kaspari <sebastian@yaaic.org>
|
|
*/
|
|
public class IRCService extends Service
|
|
{
|
|
private final IRCBinder binder;
|
|
private final HashMap<Integer, IRCConnection> connections;
|
|
private boolean foreground = false;
|
|
private final ArrayList<String> connectedServerTitles;
|
|
private final LinkedHashMap<String, Conversation> mentions;
|
|
private int newMentions = 0;
|
|
|
|
private static final int FOREGROUND_NOTIFICATION = 1;
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private static final Class[] mStartForegroundSignature = new Class[] { int.class, Notification.class };
|
|
@SuppressWarnings("unchecked")
|
|
private static final Class[] mStopForegroundSignature = new Class[] { boolean.class };
|
|
|
|
public static final String ACTION_FOREGROUND = "org.yaaic.service.foreground";
|
|
public static final String ACTION_BACKGROUND = "org.yaaic.service.background";
|
|
public static final String ACTION_ACK_NEW_MENTIONS = "org.yaaic.service.ack_new_mentions";
|
|
public static final String EXTRA_ACK_SERVERID = "org.yaaic.service.ack_serverid";
|
|
public static final String EXTRA_ACK_CONVTITLE = "org.yaaic.service.ack_convtitle";
|
|
|
|
private NotificationManager notificationManager;
|
|
private Method mStartForeground;
|
|
private Method mStopForeground;
|
|
private final Object[] mStartForegroundArgs = new Object[2];
|
|
private final Object[] mStopForegroundArgs = new Object[1];
|
|
private Notification notification;
|
|
private Settings settings;
|
|
|
|
/**
|
|
* Create new service
|
|
*/
|
|
public IRCService()
|
|
{
|
|
super();
|
|
|
|
this.connections = new HashMap<Integer, IRCConnection>();
|
|
this.binder = new IRCBinder(this);
|
|
this.connectedServerTitles = new ArrayList<String>();
|
|
this.mentions = new LinkedHashMap<String, Conversation>();
|
|
}
|
|
|
|
/**
|
|
* On create
|
|
*/
|
|
@Override
|
|
public void onCreate()
|
|
{
|
|
super.onCreate();
|
|
|
|
settings = new Settings(getBaseContext());
|
|
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
|
|
try {
|
|
mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
|
|
mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
|
|
} catch (NoSuchMethodException e) {
|
|
// Running on an older platform.
|
|
mStartForeground = mStopForeground = null;
|
|
}
|
|
|
|
// Load servers from Database
|
|
Database db = new Database(this);
|
|
Yaaic.getInstance().setServers(db.getServers());
|
|
db.close();
|
|
|
|
// Broadcast changed server list
|
|
sendBroadcast(new Intent(Broadcast.SERVER_UPDATE));
|
|
}
|
|
|
|
/**
|
|
* Get Settings object
|
|
*
|
|
* @return the settings helper object
|
|
*/
|
|
public Settings getSettings()
|
|
{
|
|
return settings;
|
|
}
|
|
|
|
/**
|
|
* On start (will be called on pre-2.0 platform. On 2.0 or later onStartCommand()
|
|
* will be called)
|
|
*/
|
|
@Override
|
|
public void onStart(Intent intent, int startId)
|
|
{
|
|
super.onStart(intent, startId);
|
|
handleCommand(intent);
|
|
}
|
|
|
|
/**
|
|
* On start command (Android >= 2.0)
|
|
*
|
|
* @param intent
|
|
* @param flags
|
|
* @param startId
|
|
* @return
|
|
*/
|
|
public int onStartCommand(Intent intent, int flags, int startId)
|
|
{
|
|
if (intent != null) {
|
|
handleCommand(intent);
|
|
}
|
|
|
|
// We want this service to continue running until it is explicitly
|
|
// stopped, so return sticky.
|
|
//return START_STICKY;
|
|
return 1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle command
|
|
*
|
|
* @param intent
|
|
*/
|
|
private void handleCommand(Intent intent)
|
|
{
|
|
if (ACTION_FOREGROUND.equals(intent.getAction())) {
|
|
if (foreground) {
|
|
return; // XXX: We are already in foreground...
|
|
}
|
|
foreground = true;
|
|
|
|
// Set the icon, scrolling text and timestamp
|
|
notification = new Notification(R.drawable.icon, getText(R.string.notification_running), System.currentTimeMillis());
|
|
|
|
// The PendingIntent to launch our activity if the user selects this notification
|
|
Intent notifyIntent = new Intent(this, ServersActivity.class);
|
|
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
|
|
|
|
// Set the info for the views that show in the notification panel.
|
|
notification.setLatestEventInfo(this, getText(R.string.app_name), "", contentIntent);
|
|
|
|
startForegroundCompat(FOREGROUND_NOTIFICATION, notification);
|
|
} else if (ACTION_BACKGROUND.equals(intent.getAction()) && !foreground) {
|
|
stopForegroundCompat(FOREGROUND_NOTIFICATION);
|
|
} else if (ACTION_ACK_NEW_MENTIONS.equals(intent.getAction())) {
|
|
ackNewMentions(intent.getIntExtra(EXTRA_ACK_SERVERID, -1), intent.getStringExtra(EXTRA_ACK_CONVTITLE));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update notification and vibrate if needed
|
|
*
|
|
* @param text The ticker text to display
|
|
* @param contentText The text to display in the notification dropdown
|
|
* @param vibrate True if the device should vibrate, false otherwise
|
|
* @param sound True if the device should make sound, false otherwise
|
|
*/
|
|
private void updateNotification(String text, String contentText, boolean vibrate, boolean sound)
|
|
{
|
|
if (foreground) {
|
|
notification = new Notification(R.drawable.icon, text, System.currentTimeMillis());
|
|
Intent notifyIntent = new Intent(this, ServersActivity.class);
|
|
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
|
|
|
|
if (contentText == null) {
|
|
if (newMentions >= 1) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (Conversation conv : mentions.values()) {
|
|
sb.append(conv.getName() + " (" + conv.getNewMentions() + "), ");
|
|
}
|
|
contentText = getString(R.string.notification_mentions, sb.substring(0, sb.length()-2));
|
|
} else if (!connectedServerTitles.isEmpty()) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (String title : connectedServerTitles) {
|
|
sb.append(title + ", ");
|
|
}
|
|
contentText = getString(R.string.notification_connected, sb.substring(0, sb.length()-2));
|
|
} else {
|
|
contentText = "";
|
|
}
|
|
}
|
|
|
|
notification.setLatestEventInfo(this, getText(R.string.app_name), contentText, contentIntent);
|
|
|
|
if (vibrate) {
|
|
notification.defaults |= Notification.DEFAULT_VIBRATE;
|
|
}
|
|
|
|
if (sound) {
|
|
notification.defaults |= Notification.DEFAULT_SOUND;
|
|
}
|
|
|
|
notification.number = newMentions;
|
|
|
|
notificationManager.notify(FOREGROUND_NOTIFICATION, notification);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a string uniquely identifying a conversation.
|
|
*/
|
|
public String getConversationId(int serverId, String title) {
|
|
return "" + serverId + ":" + title;
|
|
}
|
|
|
|
/**
|
|
* Notify the service of a new mention (updates the status bar notification)
|
|
*
|
|
* @param conversation The conversation where the new mention occurred
|
|
* @param msg The text of the new message
|
|
* @param vibrate Whether the notification should include vibration
|
|
* @param sound Whether the notification should include sound
|
|
*/
|
|
public synchronized void addNewMention(int serverId, Conversation conversation, String msg, boolean vibrate, boolean sound)
|
|
{
|
|
if (conversation == null) {
|
|
return;
|
|
}
|
|
|
|
conversation.addNewMention();
|
|
++newMentions;
|
|
String convId = getConversationId(serverId, conversation.getName());
|
|
if (!mentions.containsKey(convId)) {
|
|
mentions.put(convId, conversation);
|
|
}
|
|
|
|
if (newMentions == 1) {
|
|
updateNotification(msg, msg, vibrate, sound);
|
|
} else {
|
|
updateNotification(msg, null, vibrate, sound);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify the service that new mentions have been viewed (updates the status bar notification)
|
|
*
|
|
* @param convTitle The title of the conversation whose new mentions have been read
|
|
*/
|
|
public synchronized void ackNewMentions(int serverId, String convTitle)
|
|
{
|
|
if (convTitle == null) {
|
|
return;
|
|
}
|
|
|
|
Conversation conversation = mentions.remove(getConversationId(serverId, convTitle));
|
|
if (conversation == null) {
|
|
return;
|
|
}
|
|
newMentions -= conversation.getNewMentions();
|
|
conversation.clearNewMentions();
|
|
if (newMentions < 0) {
|
|
newMentions = 0;
|
|
}
|
|
|
|
updateNotification(null, null, false, false);
|
|
}
|
|
|
|
/**
|
|
* Notify the service of connection to a server (updates the status bar notification)
|
|
*
|
|
* @param title The title of the newly connected server
|
|
*/
|
|
public synchronized void notifyConnected(String title)
|
|
{
|
|
connectedServerTitles.add(title);
|
|
updateNotification(getString(R.string.notification_connected, title), null, false, false);
|
|
}
|
|
|
|
/**
|
|
* Notify the service of disconnection from a server (updates the status bar notification)
|
|
*
|
|
* @param title The title of the disconnected server
|
|
*/
|
|
public synchronized void notifyDisconnected(String title)
|
|
{
|
|
connectedServerTitles.remove(title);
|
|
updateNotification(getString(R.string.notification_disconnected, title), null, false, false);
|
|
}
|
|
|
|
|
|
/**
|
|
* This is a wrapper around the new startForeground method, using the older
|
|
* APIs if it is not available.
|
|
*/
|
|
private void startForegroundCompat(int id, Notification notification)
|
|
{
|
|
// If we have the new startForeground API, then use it.
|
|
if (mStartForeground != null) {
|
|
mStartForegroundArgs[0] = Integer.valueOf(id);
|
|
mStartForegroundArgs[1] = notification;
|
|
try {
|
|
mStartForeground.invoke(this, mStartForegroundArgs);
|
|
} catch (InvocationTargetException e) {
|
|
// Should not happen.
|
|
} catch (IllegalAccessException e) {
|
|
// Should not happen.
|
|
}
|
|
} else {
|
|
// Fall back on the old API.
|
|
setForeground(true);
|
|
notificationManager.notify(id, notification);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a wrapper around the new stopForeground method, using the older
|
|
* APIs if it is not available.
|
|
*/
|
|
public void stopForegroundCompat(int id)
|
|
{
|
|
foreground = false;
|
|
|
|
// If we have the new stopForeground API, then use it.
|
|
if (mStopForeground != null) {
|
|
mStopForegroundArgs[0] = Boolean.TRUE;
|
|
try {
|
|
mStopForeground.invoke(this, mStopForegroundArgs);
|
|
} catch (InvocationTargetException e) {
|
|
// Should not happen.
|
|
} catch (IllegalAccessException e) {
|
|
// Should not happen.
|
|
}
|
|
} else {
|
|
// Fall back on the old API. Note to cancel BEFORE changing the
|
|
// foreground state, since we could be killed at that point.
|
|
notificationManager.cancel(id);
|
|
setForeground(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect to the given server
|
|
*/
|
|
public void connect(final Server server)
|
|
{
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
IRCConnection connection = getConnection(server.getId());
|
|
|
|
connection.setNickname(server.getIdentity().getNickname());
|
|
connection.setAliases(server.getIdentity().getAliases());
|
|
connection.setIdent(server.getIdentity().getIdent());
|
|
connection.setRealName(server.getIdentity().getRealName());
|
|
connection.setUseSSL(server.useSSL());
|
|
|
|
if (server.getCharset() != null) {
|
|
connection.setEncoding(server.getCharset());
|
|
}
|
|
|
|
if (server.getAuthentication().hasSaslCredentials()) {
|
|
connection.setSaslCredentials(
|
|
server.getAuthentication().getSaslUsername(),
|
|
server.getAuthentication().getSaslPassword()
|
|
);
|
|
}
|
|
|
|
if (server.getPassword() != "") {
|
|
connection.connect(server.getHost(), server.getPort(), server.getPassword());
|
|
} else {
|
|
connection.connect(server.getHost(), server.getPort());
|
|
}
|
|
}
|
|
catch (Exception e) {
|
|
server.setStatus(Status.DISCONNECTED);
|
|
|
|
Intent sIntent = Broadcast.createServerIntent(Broadcast.SERVER_UPDATE, server.getId());
|
|
sendBroadcast(sIntent);
|
|
|
|
IRCConnection connection = getConnection(server.getId());
|
|
|
|
Message message;
|
|
|
|
if (e instanceof NickAlreadyInUseException) {
|
|
message = new Message(getString(R.string.nickname_in_use, connection.getNick()));
|
|
} else if (e instanceof IrcException) {
|
|
message = new Message(getString(R.string.irc_login_error, server.getHost(), server.getPort()));
|
|
} else {
|
|
message = new Message(getString(R.string.could_not_connect, server.getHost(), server.getPort()));
|
|
}
|
|
|
|
message.setColor(Message.COLOR_RED);
|
|
message.setIcon(R.drawable.error);
|
|
server.getConversation(ServerInfo.DEFAULT_NAME).addMessage(message);
|
|
|
|
Intent cIntent = Broadcast.createConversationIntent(
|
|
Broadcast.CONVERSATION_MESSAGE,
|
|
server.getId(),
|
|
ServerInfo.DEFAULT_NAME
|
|
);
|
|
sendBroadcast(cIntent);
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
/**
|
|
* Get connection for given server
|
|
*
|
|
* @param serverId
|
|
* @return
|
|
*/
|
|
public synchronized IRCConnection getConnection(int serverId)
|
|
{
|
|
IRCConnection connection = connections.get(serverId);
|
|
|
|
if (connection == null) {
|
|
connection = new IRCConnection(this, serverId);
|
|
connections.put(serverId, connection);
|
|
}
|
|
|
|
return connection;
|
|
}
|
|
|
|
/**
|
|
* Does the service keep a connection object for this server?
|
|
*
|
|
* @return true if there's a connection object, false otherwise
|
|
*/
|
|
public boolean hasConnection(int serverId)
|
|
{
|
|
return connections.containsKey(serverId);
|
|
}
|
|
|
|
/**
|
|
* Check status of service
|
|
*/
|
|
public void checkServiceStatus()
|
|
{
|
|
boolean shutDown = true;
|
|
ArrayList<Server> mServers = Yaaic.getInstance().getServersAsArrayList();
|
|
int mSize = mServers.size();
|
|
Server server;
|
|
|
|
for (int i = 0; i < mSize; i++) {
|
|
server = mServers.get(i);
|
|
if (server.isDisconnected()) {
|
|
int serverId = server.getId();
|
|
synchronized(this) {
|
|
IRCConnection connection = connections.get(serverId);
|
|
if (connection != null) {
|
|
connection.dispose();
|
|
}
|
|
connections.remove(serverId);
|
|
}
|
|
} else {
|
|
shutDown = false;
|
|
}
|
|
}
|
|
|
|
if (shutDown) {
|
|
foreground = false;
|
|
stopForegroundCompat(R.string.app_name);
|
|
stopSelf();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On Destroy
|
|
*/
|
|
@Override
|
|
public void onDestroy()
|
|
{
|
|
// Make sure our notification is gone.
|
|
if (foreground) {
|
|
stopForegroundCompat(R.string.app_name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On Activity binding to this service
|
|
*
|
|
* @param intent
|
|
* @return
|
|
*/
|
|
@Override
|
|
public IRCBinder onBind(Intent intent)
|
|
{
|
|
return binder;
|
|
}
|
|
}
|