package org.yaxim.androidclient.service;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.yaxim.androidclient.IXMPPRosterCallback;
import org.yaxim.androidclient.MainWindow;
import org.yaxim.androidclient.R;
import org.yaxim.androidclient.data.RosterProvider;
import org.yaxim.androidclient.exceptions.YaximXMPPException;
import org.yaxim.androidclient.util.ConnectionState;
import org.yaxim.androidclient.util.StatusMode;
import org.jivesoftware.smack.packet.Message.Type;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
public class XMPPService extends GenericService {
private static final String TAG="yaxim.XMPPService";
private AtomicBoolean mConnectionDemanded = new AtomicBoolean(false); // should we try to reconnect?
private static final int RECONNECT_AFTER = 5;
private static final int RECONNECT_MAXIMUM = 10*60;
private static final String RECONNECT_ALARM = "org.yaxim.androidclient.RECONNECT_ALARM";
private int mReconnectTimeout = RECONNECT_AFTER;
private String mReconnectInfo = "";
private Intent mAlarmIntent = new Intent(RECONNECT_ALARM);
private PendingIntent mPAlarmIntent;
private BroadcastReceiver mAlarmReceiver = new ReconnectAlarmReceiver();
private BroadcastReceiver mRingerModeReceiver = new RingerModeReceiver();
private Smackable mSmackable;
private boolean create_account = false;
private IXMPPRosterService.Stub mService2RosterConnection;
private IXMPPChatService.Stub mServiceChatConnection;
private IXMPPMucService.Stub mServiceMucConnection;
private RemoteCallbackList<IXMPPRosterCallback> mRosterCallbacks = new RemoteCallbackList<IXMPPRosterCallback>();
private HashSet<String> mIsBoundTo = new HashSet<String>();
private Handler mMainHandler = new Handler();
@Override
public IBinder onBind(Intent intent) {
userStartedWatching();
String chatPartner = intent.getDataString();
if(chatPartner != null && chatPartner.endsWith("?chat")) {
return mServiceMucConnection;
} else if (chatPartner != null) {
resetNotificationCounter(chatPartner);
mIsBoundTo.add(chatPartner);
return mServiceChatConnection;
}
return mService2RosterConnection;
}
@Override
public void onRebind(Intent intent) {
userStartedWatching();
String chatPartner = intent.getDataString();
if ((chatPartner != null)) {
mIsBoundTo.add(chatPartner);
resetNotificationCounter(chatPartner);
}
}
@Override
public boolean onUnbind(Intent intent) {
String chatPartner = intent.getDataString();
if ((chatPartner != null)) {
mIsBoundTo.remove(chatPartner);
}
userStoppedWatching();
return true;
}
@Override
public void onCreate() {
super.onCreate();
createServiceRosterStub();
createServiceChatStub();
createServiceMucStub();
mPAlarmIntent = PendingIntent.getBroadcast(this, 0, mAlarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
registerReceiver(mAlarmReceiver, new IntentFilter(RECONNECT_ALARM));
registerReceiver(mRingerModeReceiver, new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION));
configureSmartAwayMode();
YaximBroadcastReceiver.initNetworkStatus(getApplicationContext());
if (mConfig.autoConnect && mConfig.jid_configured) {
/*
* start our own service so it remains in background even when
* unbound
*/
Intent xmppServiceIntent = new Intent(this, XMPPService.class);
startService(xmppServiceIntent);
}
}
@Override
public void onDestroy() {
super.onDestroy();
((AlarmManager)getSystemService(Context.ALARM_SERVICE)).cancel(mPAlarmIntent);
mRosterCallbacks.kill();
if (mSmackable != null) {
manualDisconnect();
mSmackable.unRegisterCallback();
}
unregisterReceiver(mAlarmReceiver);
unregisterReceiver(mRingerModeReceiver);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
logInfo("onStartCommand(), mConnectionDemanded=" + mConnectionDemanded.get());
logInfo(" intent=" + intent);
if (intent != null) {
create_account = intent.getBooleanExtra("create_account", false);
if ("disconnect".equals(intent.getAction())) {
failConnection(getString(R.string.conn_no_network));
return START_STICKY;
} else
if ("reconnect".equals(intent.getAction())) {
// TODO: integrate the following steps into one "RECONNECT"
failConnection(getString(R.string.conn_no_network));
// reset reconnection timeout
mReconnectTimeout = RECONNECT_AFTER;
doConnect();
return START_STICKY;
} else
if ("ping".equals(intent.getAction())) {
if (mSmackable != null && mSmackable.isAuthenticated()) {
mSmackable.sendServerPing();
return START_STICKY;
}
// if not authenticated, fall through to doConnect()
} else
if ("respond".equals(intent.getAction())) {
// clear notifications and send a message from Android Auto/Wear event
String jid = intent.getDataString();
String replystring = intent.getStringExtra("message");
if (replystring != null) {
Log.d(TAG, "got reply: " + replystring);
mSmackable.sendMessage(jid, replystring);
}
org.yaxim.androidclient.data.ChatHelper.markAsRead(this, jid);
clearNotification(jid);
return START_STICKY;
}
}
mConnectionDemanded.set(mConfig.autoConnect);
doConnect();
return START_STICKY;
}
private void createServiceChatStub() {
mServiceChatConnection = new IXMPPChatService.Stub() {
public void sendMessage(String user, String message)
throws RemoteException {
if (mSmackable != null)
mSmackable.sendMessage(user, message);
else
SmackableImp.sendOfflineMessage(getContentResolver(),
user, message);
}
public boolean isAuthenticated() throws RemoteException {
if (mSmackable != null) {
return mSmackable.isAuthenticated();
}
return false;
}
public void clearNotifications(String Jid) throws RemoteException {
clearNotification(Jid);
}
};
}
private void createServiceMucStub() {
mServiceMucConnection = new IXMPPMucService.Stub() {
private void fail(String error) {
Toast toast = Toast.makeText(getApplicationContext(),
error, Toast.LENGTH_LONG);
toast.show();
}
@Override
public void sendMessage(String room, String message)
throws RemoteException {
if(mSmackable!=null)
mSmackable.sendMucMessage(room, message);
else
shortToastNotify(getString(R.string.Global_authenticate_first));
}
@Override
public void syncDbRooms() throws RemoteException {
if(mSmackable!=null)
new Thread() {
@Override
public void run() {
mSmackable.syncDbRooms();
}
}.start();
}
@Override
public boolean inviteToRoom(String contactJid, String roomJid) {
if(mSmackable!=null)
return mSmackable.inviteToRoom(contactJid, roomJid);
else {
shortToastNotify(getString(R.string.Global_authenticate_first));
return false;
}
}
@Override
public String getMyMucNick(String jid) throws RemoteException {
if(mSmackable!=null)
return mSmackable.getMyMucNick(jid);
return null;
}
@Override
public List<ParcelablePresence> getUserList(String jid) throws RemoteException {
if(mSmackable!=null)
return mSmackable.getUserList(jid);
else {
shortToastNotify(getString(R.string.Global_authenticate_first));
return null;
}
}
};
}
private void createServiceRosterStub() {
mService2RosterConnection = new IXMPPRosterService.Stub() {
public void registerRosterCallback(IXMPPRosterCallback callback)
throws RemoteException {
if (callback != null)
mRosterCallbacks.register(callback);
}
public void unregisterRosterCallback(IXMPPRosterCallback callback)
throws RemoteException {
if (callback != null)
mRosterCallbacks.unregister(callback);
}
public int getConnectionState() throws RemoteException {
if (mSmackable != null) {
return mSmackable.getConnectionState().ordinal();
} else {
return ConnectionState.OFFLINE.ordinal();
}
}
public String getConnectionStateString() throws RemoteException {
return XMPPService.this.getConnectionStateString();
}
public void setStatusFromConfig()
throws RemoteException {
if (mSmackable != null) { // this should always be true, but stil...
mSmackable.setStatusFromConfig();
updateServiceNotification();
}
}
public void addRosterItem(String user, String alias, String group, String token)
throws RemoteException {
try {
mSmackable.addRosterItem(user, alias, group, token);
} catch (YaximXMPPException e) {
shortToastNotify(e);
}
}
public void addRosterGroup(String group) throws RemoteException {
mSmackable.addRosterGroup(group);
}
public void removeRosterItem(String user) throws RemoteException {
try {
mSmackable.removeRosterItem(user);
} catch (YaximXMPPException e) {
shortToastNotify(e);
}
}
public void moveRosterItemToGroup(String user, String group)
throws RemoteException {
try {
mSmackable.moveRosterItemToGroup(user, group);
} catch (YaximXMPPException e) {
shortToastNotify(e);
}
}
public void renameRosterItem(String user, String newName)
throws RemoteException {
try {
mSmackable.renameRosterItem(user, newName);
} catch (YaximXMPPException e) {
shortToastNotify(e);
}
}
public void renameRosterGroup(String group, String newGroup)
throws RemoteException {
mSmackable.renameRosterGroup(group, newGroup);
}
@Override
public String changePassword(String newPassword)
throws RemoteException {
return mSmackable.changePassword(newPassword);
}
public void disconnect() throws RemoteException {
manualDisconnect();
}
public void connect() throws RemoteException {
mConnectionDemanded.set(true);
mReconnectTimeout = RECONNECT_AFTER;
doConnect();
}
public void sendPresenceRequest(String jid, String type)
throws RemoteException {
mSmackable.sendPresenceRequest(jid, type);
}
};
}
private String getConnectionStateString() {
StringBuilder sb = new StringBuilder();
sb.append(mReconnectInfo);
if (mSmackable != null && mSmackable.getLastError() != null) {
sb.append("\n");
sb.append(mSmackable.getLastError());
}
return sb.toString();
}
public String getStatusTitle(ConnectionState cs) {
if (cs != ConnectionState.ONLINE)
return mReconnectInfo;
String status = getString(mConfig.getPresenceMode().getTextId());
if (mConfig.statusMessage.length() > 0) {
status = status + " (" + mConfig.statusMessage + ")";
}
return status;
}
private void updateServiceNotification() {
ConnectionState cs = ConnectionState.OFFLINE;
if (mSmackable != null) {
cs = mSmackable.getConnectionState();
}
// HACK to trigger show-offline when XEP-0198 reconnect is going on
getContentResolver().notifyChange(RosterProvider.CONTENT_URI, null);
getContentResolver().notifyChange(RosterProvider.GROUPS_URI, null);
// end-of-HACK
Log.d(TAG, "updateServiceNotification: " + cs);
broadcastConnectionState(cs);
// do not show notification if not a foreground service
if (!mConfig.foregroundService)
return;
if (cs == ConnectionState.OFFLINE) {
stopForeground(true);
return;
}
Intent notificationIntent = new Intent(this, MainWindow.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
Notification n = new NotificationCompat.Builder(this)
.setSmallIcon((cs == ConnectionState.ONLINE) ? R.drawable.ic_online : R.drawable.ic_offline)
.setLargeIcon(android.graphics.BitmapFactory.decodeResource(getResources(), R.drawable.icon))
.setWhen(mSmackable.getConnectionStateTimestamp())
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT))
.setContentTitle(getString(R.string.conn_title, mConfig.jabberID))
.setContentText(getStatusTitle(cs))
.build();
startForeground(SERVICE_NOTIFICATION, n);
}
private void doConnect() {
mReconnectInfo = getString(R.string.conn_connecting);
updateServiceNotification();
if (mSmackable == null) {
createAdapter();
}
mSmackable.requestConnectionState(ConnectionState.ONLINE, create_account);
create_account = false;
}
private void broadcastConnectionState(ConnectionState cs) {
final int broadCastItems = mRosterCallbacks.beginBroadcast();
for (int i = 0; i < broadCastItems; i++) {
try {
mRosterCallbacks.getBroadcastItem(i).connectionStateChanged(cs.ordinal());
} catch (RemoteException e) {
logError("caught RemoteException: " + e.getMessage());
}
}
mRosterCallbacks.finishBroadcast();
}
private NetworkInfo getNetworkInfo() {
Context ctx = getApplicationContext();
ConnectivityManager connMgr =
(ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
return connMgr.getActiveNetworkInfo();
}
private boolean networkConnected() {
NetworkInfo info = getNetworkInfo();
return info != null && info.isConnected();
}
private boolean networkConnectedOrConnecting() {
NetworkInfo info = getNetworkInfo();
return info != null && info.isConnectedOrConnecting();
}
// call this when Android tells us to shut down
private void failConnection(String reason) {
logInfo("failConnection: " + reason);
mReconnectInfo = reason;
updateServiceNotification();
if (mSmackable != null)
mSmackable.requestConnectionState(ConnectionState.DISCONNECTED);
}
// called from Smackable when connection broke down
private void connectionFailed(String reason) {
logInfo("connectionFailed: " + reason);
//TODO: error message from downstream?
//mLastConnectionError = reason;
if (!networkConnected()) {
mReconnectInfo = getString(R.string.conn_no_network);
mSmackable.requestConnectionState(ConnectionState.RECONNECT_NETWORK);
} else if (mConnectionDemanded.get()) {
mReconnectInfo = getString(R.string.conn_reconnect, mReconnectTimeout);
mSmackable.requestConnectionState(ConnectionState.RECONNECT_DELAYED);
logInfo("connectionFailed(): registering reconnect in " + mReconnectTimeout + "s");
((AlarmManager)getSystemService(Context.ALARM_SERVICE)).set(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + mReconnectTimeout * 1000, mPAlarmIntent);
mReconnectTimeout = mReconnectTimeout * 2;
if (mReconnectTimeout > RECONNECT_MAXIMUM)
mReconnectTimeout = RECONNECT_MAXIMUM;
} else {
connectionClosed();
}
}
private void connectionClosed() {
logInfo("connectionClosed.");
mReconnectInfo = "";
stopForeground(true);
mSmackable.requestConnectionState(ConnectionState.OFFLINE);
}
public void manualDisconnect() {
mConnectionDemanded.set(false);
mReconnectInfo = getString(R.string.conn_disconnecting);
performDisconnect();
}
public void performDisconnect() {
if (mSmackable != null) {
// this is non-blocking
mSmackable.requestConnectionState(ConnectionState.OFFLINE);
}
}
private void createAdapter() {
System.setProperty("smack.debugEnabled", "" + mConfig.smackdebug);
try {
mSmackable = new SmackableImp(mConfig, getContentResolver(), this);
} catch (NullPointerException e) {
e.printStackTrace();
}
mSmackable.registerCallback(new XMPPServiceCallback() {
public void notifyMessage(final String[] from, final String message,
final boolean silent_notification, final Type msgType) {
final String name = mSmackable.getNameForJID(from[0]);
logInfo("notification: " + from[0] + "=" + name +" with type: "+msgType.name());
mMainHandler.post(new Runnable() {
public void run() {
// work around Toast fallback for errors
notifyClient(from, name, message,
!mIsBoundTo.contains(from[0]), silent_notification, msgType);
}});
}
public void connectionStateChanged() {
// TODO: OFFLINE is sometimes caused by XMPPConnection calling
// connectionClosed() callback on an error, need to catch that?
updateServiceNotification();
switch (mSmackable.getConnectionState()) {
//case OFFLINE:
case DISCONNECTED:
connectionFailed(getString(R.string.conn_disconnected));
break;
case ONLINE:
mReconnectTimeout = RECONNECT_AFTER;
default:
}
}
@Override
public void mucInvitationReceived(String roomname, String room, String password, String invite_from, String roomdescription) {
String body = invite_from + ": " + roomname + "\n" + roomdescription;
Log.d(TAG, "Notifying MUC invitation for " + room + ". " + body);
Intent intent = new Intent(getApplicationContext(), MainWindow.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.setAction("android.intent.action.VIEW");
String uri = "xmpp:" + room;
Builder b = new Builder();
b.appendQueryParameter("join", null);
if (password != null)
b.appendQueryParameter("password", password);
b.appendQueryParameter("body", body);
intent.setData(Uri.parse(uri + b.toString()));
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
intent, 0);
Notification invNotify = new NotificationCompat.Builder(getApplicationContext())
.setContentTitle(roomname)
.setContentText(body)
.setSmallIcon(R.drawable.ic_action_group_dark)
.setTicker(invite_from + ": " + roomname)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(roomdescription)
.setSummaryText(invite_from)
.setBigContentTitle(roomname))
.setContentIntent(pi)
.setAutoCancel(true)
.build();
int notifyId;
if (notificationId.containsKey(room)) {
notifyId = notificationId.get(room);
} else {
lastNotificationId++;
notifyId = lastNotificationId;
notificationId.put(room, Integer.valueOf(notifyId));
}
mNotificationMGR.notify(notifyId, invNotify);
}
});
}
private class ReconnectAlarmReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent i) {
logInfo("Alarm received.");
if (!mConnectionDemanded.get()) {
return;
}
if (mSmackable != null && mSmackable.getConnectionState() == ConnectionState.ONLINE) {
logError("Reconnect attempt aborted: we are connected again!");
return;
}
doConnect();
}
}
private int number_of_eyes = 0;
private void userStartedWatching() {
number_of_eyes += 1;
logInfo("userStartedWatching: " + number_of_eyes);
if (mSmackable != null)
mSmackable.setUserWatching(true);
}
private void userStoppedWatching() {
number_of_eyes -= 1;
logInfo("userStoppedWatching: " + number_of_eyes);
// delay deactivation by 3s, in case we happen to be immediately re-bound
mMainHandler.postDelayed(new Runnable() {
public void run() {
if (mSmackable != null && number_of_eyes == 0)
mSmackable.setUserWatching(false);
}}, 3000);
}
private void configureSmartAwayMode() {
AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
boolean is_silent = (am.getRingerMode() == AudioManager.RINGER_MODE_SILENT);
mConfig.smartAwayMode = is_silent ? StatusMode.dnd : null;
logInfo("configureSmartAwayMode: " + mConfig.smartAwayMode);
}
private class RingerModeReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent i) {
logInfo("Ringer mode changed: " + i);
configureSmartAwayMode();
if (mSmackable != null && mSmackable.isAuthenticated()) {
mSmackable.setStatusFromConfig();
updateServiceNotification();
}
}
}
}