package org.yaxim.androidclient.service; import java.util.HashSet; 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 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.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Handler; import android.os.IBinder; import android.os.RemoteCallbackList; import android.os.RemoteException; public class XMPPService extends GenericService { 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 ServiceNotification mServiceNotification = null; private Smackable mSmackable; private boolean create_account = false; private IXMPPRosterService.Stub mService2RosterConnection; private IXMPPChatService.Stub mServiceChatConnection; 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) { super.onBind(intent); String chatPartner = intent.getDataString(); if ((chatPartner != null)) { resetNotificationCounter(chatPartner); mIsBoundTo.add(chatPartner); return mServiceChatConnection; } return mService2RosterConnection; } @Override public void onRebind(Intent intent) { super.onRebind(intent); 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); } return true; } @Override public void onCreate() { super.onCreate(); createServiceRosterStub(); createServiceChatStub(); mPAlarmIntent = PendingIntent.getBroadcast(this, 0, mAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); registerReceiver(mAlarmReceiver, new IntentFilter(RECONNECT_ALARM)); 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); } mServiceNotification = ServiceNotification.getInstance(); } @Override public void onDestroy() { super.onDestroy(); ((AlarmManager)getSystemService(Context.ALARM_SERVICE)).cancel(mPAlarmIntent); mRosterCallbacks.kill(); if (mSmackable != null) { manualDisconnect(); mSmackable.unRegisterCallback(); } unregisterReceiver(mAlarmReceiver); } @Override public int onStartCommand(Intent intent, int flags, int startId) { logInfo("onStartCommand(), mConnectionDemanded=" + mConnectionDemanded.get()); 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.sendServerPing(); return START_STICKY; } // if not yet connected, fall through to doConnect() } } 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 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) throws RemoteException { try { mSmackable.addRosterItem(user, alias, group); } 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(StatusMode.fromString(mConfig.statusMode).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 broadcastConnectionState(cs); // do not show notification if not a foreground service if (!mConfig.foregroundService) return; if (cs == ConnectionState.OFFLINE) { mServiceNotification.hideNotification(this, SERVICE_NOTIFICATION); return; } Notification n = new Notification(R.drawable.ic_offline, null, System.currentTimeMillis()); n.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR | Notification.FLAG_ONLY_ALERT_ONCE; Intent notificationIntent = new Intent(this, MainWindow.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); n.contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); if (cs == ConnectionState.ONLINE) n.icon = R.drawable.ic_online; String title = getString(R.string.conn_title, mConfig.jabberID); String message = getStatusTitle(cs); n.setLatestEventInfo(this, title, message, n.contentIntent); mServiceNotification.showNotification(this, SERVICE_NOTIFICATION, n); } private void doConnect() { mReconnectInfo = getString(R.string.conn_connecting); updateServiceNotification(); if (mSmackable == null) { createAdapter(); } mSmackable.requestConnectionState(ConnectionState.ONLINE, create_account); } 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 = ""; mServiceNotification.hideNotification(this, SERVICE_NOTIFICATION); } 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 newMessage(String from, String message, boolean silent_notification) { logInfo("notification: " + from); notifyClient(from, mSmackable.getNameForJID(from), message, !mIsBoundTo.contains(from), silent_notification, false); } public void messageError(final String from, final String error, final boolean silent_notification) { logInfo("error notification: " + from); mMainHandler.post(new Runnable() { public void run() { // work around Toast fallback for errors notifyClient(from, mSmackable.getNameForJID(from), error, !mIsBoundTo.contains(from), silent_notification, true); }}); } public void rosterChanged() { } public void connectionStateChanged() { // TODO: OFFLINE is sometimes caused by XMPPConnection calling // connectionClosed() callback on an error, need to catch that? switch (mSmackable.getConnectionState()) { //case OFFLINE: case DISCONNECTED: connectionFailed(getString(R.string.conn_disconnected)); break; case ONLINE: mReconnectTimeout = RECONNECT_AFTER; default: updateServiceNotification(); } } }); } 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(); } } }