/*
* Copyright (C) 2007-2008 Esmertec AG. Copyright (C) 2007-2008 The Android Open
* Source Project
*
* 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 info.guardianproject.otr.app.im.service;
import info.guardianproject.otr.IOtrKeyManager;
import info.guardianproject.otr.OtrAndroidKeyManagerImpl;
import info.guardianproject.otr.OtrChatManager;
import info.guardianproject.otr.OtrDebugLogger;
import info.guardianproject.otr.app.im.IConnectionCreationListener;
import info.guardianproject.otr.app.im.IImConnection;
import info.guardianproject.otr.app.im.IRemoteImService;
import info.guardianproject.otr.app.im.ImService;
import info.guardianproject.otr.app.im.R;
import info.guardianproject.otr.app.im.app.ImApp;
import info.guardianproject.otr.app.im.app.ImPluginHelper;
import info.guardianproject.otr.app.im.app.NetworkConnectivityListener;
import info.guardianproject.otr.app.im.app.NetworkConnectivityListener.State;
import info.guardianproject.otr.app.im.app.NewChatActivity;
import info.guardianproject.otr.app.im.engine.ConnectionFactory;
import info.guardianproject.otr.app.im.engine.HeartbeatService.Callback;
import info.guardianproject.otr.app.im.engine.ImConnection;
import info.guardianproject.otr.app.im.engine.ImException;
import info.guardianproject.otr.app.im.plugin.ImPluginInfo;
import info.guardianproject.otr.app.im.provider.Imps;
import info.guardianproject.util.Debug;
import info.guardianproject.util.LogCleaner;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import net.java.otr4j.OtrEngineListener;
import net.java.otr4j.OtrKeyManager;
import net.java.otr4j.OtrPolicy;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionStatus;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;
public class RemoteImService extends Service implements OtrEngineListener, ImService {
private static final String[] ACCOUNT_PROJECTION = { Imps.Account._ID, Imps.Account.PROVIDER,
Imps.Account.USERNAME,
Imps.Account.PASSWORD, };
// TODO why aren't these Imps.Account.* values?
private static final int ACCOUNT_ID_COLUMN = 0;
private static final int ACCOUNT_PROVIDER_COLUMN = 1;
private static final int ACCOUNT_USERNAME_COLUMN = 2;
private static final int ACCOUNT_PASSOWRD_COLUMN = 3;
private static final int EVENT_SHOW_TOAST = 100;
private static final int EVENT_NETWORK_STATE_CHANGED = 200;
// Our heartbeat interval in seconds.
// The user controlled preference heartbeat interval is in these units (i.e. minutes).
private static final long HEARTBEAT_INTERVAL = 1000 * 60;
private StatusBarNotifier mStatusBarNotifier;
private Handler mServiceHandler;
NetworkConnectivityListener mNetworkConnectivityListener;
private int mNetworkType;
private boolean mNeedCheckAutoLogin;
//private SettingsMonitor mSettingsMonitor;
private OtrChatManager mOtrChatManager;
private ImPluginHelper mPluginHelper;
Vector<ImConnectionAdapter> mConnections;
private Imps.ProviderSettings.QueryMap mGlobalSettings;
private Handler mHandler;
final RemoteCallbackList<IConnectionCreationListener> mRemoteListeners = new RemoteCallbackList<IConnectionCreationListener>();
private ForegroundStarter mForegroundStarter;
public long mHeartbeatInterval;
private static final String TAG = "Gibberbot.ImService";
public RemoteImService() {
mConnections = new Vector<ImConnectionAdapter>();
mHandler = new Handler();
}
public long getHeartbeatInterval() {
return mHeartbeatInterval;
}
public static void debug(String msg) {
LogCleaner.debug(TAG, msg);
}
public static void debug(String msg, Exception e) {
LogCleaner.error(TAG, msg, e);
}
private synchronized void initOtr() {
int otrPolicy = convertPolicy();
if (mOtrChatManager == null) {
try {
OtrKeyManager otrKeyManager = OtrAndroidKeyManagerImpl.getInstance(this);
if (otrKeyManager != null)
{
// TODO OTRCHAT add support for more than one connection type (this is a kludge)
mOtrChatManager = OtrChatManager.getInstance(otrPolicy, this, otrKeyManager);
mOtrChatManager.addOtrEngineListener(this);
}
else
{
throw new RuntimeException("could not instantiate OTR manager");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
mOtrChatManager.setPolicy(otrPolicy);
}
}
private int convertPolicy() {
int otrPolicy = OtrPolicy.OPPORTUNISTIC;
String otrModeSelect = getGlobalSettings().getOtrMode();
if (otrModeSelect.equals("auto")) {
otrPolicy = OtrPolicy.OPPORTUNISTIC;
} else if (otrModeSelect.equals("disabled")) {
otrPolicy = OtrPolicy.NEVER;
} else if (otrModeSelect.equals("force")) {
otrPolicy = OtrPolicy.OTRL_POLICY_ALWAYS;
} else if (otrModeSelect.equals("requested")) {
otrPolicy = OtrPolicy.OTRL_POLICY_MANUAL;
}
return otrPolicy;
}
private Imps.ProviderSettings.QueryMap getGlobalSettings() {
if (mGlobalSettings == null) {
mGlobalSettings = new Imps.ProviderSettings.QueryMap(getContentResolver(), true,
mHandler);
}
return mGlobalSettings;
}
@Override
public void onCreate() {
debug("ImService started");
Debug.onServiceStart();
// Clear all account statii to logged-out, since we just got started and we don't want
// leftovers from any previous crash.
clearConnectionStatii();
mStatusBarNotifier = new StatusBarNotifier(this);
mServiceHandler = new ServiceHandler();
mNetworkConnectivityListener = new NetworkConnectivityListener();
mNetworkConnectivityListener.registerHandler(mServiceHandler, EVENT_NETWORK_STATE_CHANGED);
mNetworkConnectivityListener.startListening(this);
//mSettingsMonitor = new SettingsMonitor();
/*
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED);
registerReceiver(mSettingsMonitor, intentFilter);
*/
// setBackgroundData(ImApp.getApplication().isNetworkAvailableAndConnected());
mPluginHelper = ImPluginHelper.getInstance(this);
mPluginHelper.loadAvailablePlugins();
AndroidSystemService.getInstance().initialize(this);
AndroidSystemService.getInstance().getHeartbeatService()
.startHeartbeat(new HeartbeatHandler(), HEARTBEAT_INTERVAL);
// Have the heartbeat start autoLogin, unless onStart turns this off
mNeedCheckAutoLogin = true;
// if (getGlobalSettings().getUseForegroundPriority())
startForegroundCompat();
}
private void startForegroundCompat() {
Notification notification = new Notification(R.drawable.notify_chatsecure, getString(R.string.app_name),
System.currentTimeMillis());
notification.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
Intent notificationIntent = new Intent(this, NewChatActivity.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
notification.contentIntent = PendingIntent.getActivity(this, 0, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(this, getString(R.string.app_name), getString(R.string.presence_available), notification.contentIntent);
mForegroundStarter = new ForegroundStarter(this);
mForegroundStarter.startForegroundCompat(1000, notification);
}
class HeartbeatHandler implements Callback {
@SuppressWarnings("finally")
@Override
public long sendHeartbeat() {
Debug.onHeartbeat();
try {
if (mNeedCheckAutoLogin
&& mNetworkConnectivityListener.getState() != State.NOT_CONNECTED) {
debug("autoLogin from heartbeat");
mNeedCheckAutoLogin = false;
autoLogin();
}
mHeartbeatInterval = getGlobalSettings().getHeartbeatInterval();
for (Iterator<ImConnectionAdapter> iter = mConnections.iterator(); iter.hasNext();) {
ImConnectionAdapter conn = iter.next();
conn.sendHeartbeat();
}
} finally {
return HEARTBEAT_INTERVAL;
}
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onLowMemory() {
super.onLowMemory();
}
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
if (intent != null && intent.hasExtra(ImServiceConstants.EXTRA_CHECK_AUTO_LOGIN))
mNeedCheckAutoLogin = intent.getBooleanExtra(ImServiceConstants.EXTRA_CHECK_AUTO_LOGIN,
false);
else
mNeedCheckAutoLogin = true;
debug("ImService.onStart, checkAutoLogin=" + mNeedCheckAutoLogin + " intent =" + intent
+ " startId =" + startId);
// Check and login accounts if network is ready, otherwise it's checked
// when the network becomes available.
if (mNeedCheckAutoLogin && mNetworkConnectivityListener.getState() != State.NOT_CONNECTED) {
mNeedCheckAutoLogin = false;
autoLogin();
}
}
private void clearConnectionStatii() {
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues(2);
values.put(Imps.AccountStatus.PRESENCE_STATUS, Imps.Presence.OFFLINE);
values.put(Imps.AccountStatus.CONNECTION_STATUS, Imps.ConnectionStatus.OFFLINE);
try
{
//insert on the "account_status" uri actually replaces the existing value
cr.update(Imps.AccountStatus.CONTENT_URI, values, null, null);
}
catch (Exception e)
{
//this can throw NPE on restart sometimes if database has not been unlocked
debug("database is not unlocked yet. caught NPE from mDbHelper in ImpsProvider");
}
}
private void autoLogin() {
// Try empty passphrase. We can't autologin if this fails.
if (!Imps.setEmptyPassphrase(this, true)) {
debug("Cannot autologin with non-empty passphrase");
return;
}
if (!mConnections.isEmpty()) {
// This can happen because the UI process may be restarted and may think that we need
// to autologin, while we (the Service process) are already up.
debug("Got autoLogin request, but we have one or more connections");
return;
}
debug("Scanning accounts and login automatically");
ContentResolver resolver = getContentResolver();
String where = Imps.Account.KEEP_SIGNED_IN + "=1 AND " + Imps.Account.ACTIVE + "=1";
Cursor cursor = resolver.query(Imps.Account.CONTENT_URI, ACCOUNT_PROJECTION, where, null,
null);
if (cursor == null) {
Log.w(TAG, "Can't query account!");
return;
}
while (cursor.moveToNext()) {
long accountId = cursor.getLong(ACCOUNT_ID_COLUMN);
long providerId = cursor.getLong(ACCOUNT_PROVIDER_COLUMN);
IImConnection conn = createConnection(providerId, accountId);
try
{
if (conn.getState() != ImConnection.LOGGED_IN)
{
try {
conn.login(null, true, true);
} catch (RemoteException e) {
Log.w(TAG, "Logging error while automatically login!");
}
}
}
catch (Exception e){
Log.d(ImApp.LOG_TAG,"error auto logging into ImConnection",e);
}
}
cursor.close();
}
private Map<String, String> loadProviderSettings(long providerId) {
ContentResolver cr = getContentResolver();
Map<String, String> settings = Imps.ProviderSettings.queryProviderSettings(cr, providerId);
// NetworkInfo networkInfo = mNetworkConnectivityListener.getNetworkInfo();
// Insert a fake msisdn on emulator. We don't need this on device
// because the mobile network will take care of it.
// if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) {
/*
if (false) {
settings.put(ImpsConfigNames.MSISDN, "15555218135");
} else if (networkInfo != null
&& networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
if (!TextUtils.isEmpty(settings.get(ImpsConfigNames.SMS_ADDR))) {
// Send authentication through sms if SMS data channel is
// supported and WiFi is used.
settings.put(ImpsConfigNames.SMS_AUTH, "true");
settings.put(ImpsConfigNames.SECURE_LOGIN, "false");
} else {
// Wi-Fi network won't insert a MSISDN, we should get from the SIM
// card. Assume we can always get the correct MSISDN from SIM, otherwise,
// the sign in would fail and an error message should be shown to warn
// the user to contact their operator.
String msisdn = ""; // TODO TelephonyManager.getDefault().getLine1Number();
if (TextUtils.isEmpty(msisdn)) {
Log.w(TAG, "Can not read MSISDN from SIM, use a fake one."
+ " SMS related feature won't work.");
msisdn = "15555218135";
}
settings.put(ImpsConfigNames.MSISDN, msisdn);
}
}*/
return settings;
}
@Override
public void onDestroy() {
if (mForegroundStarter != null)
mForegroundStarter.stopForegroundCompat();
Log.w(TAG, "ImService stopped.");
for (ImConnectionAdapter conn : mConnections) {
conn.logout();
}
AndroidSystemService.getInstance().shutdown();
mNetworkConnectivityListener.unregisterHandler(mServiceHandler);
mNetworkConnectivityListener.stopListening();
mNetworkConnectivityListener = null;
// unregisterReceiver(mSettingsMonitor);
if (mGlobalSettings != null)
mGlobalSettings.close();
Imps.clearPassphrase(this);
/*
if (mKillProcessOnStop)
{
int pid = android.os.Process.myPid();
Log.w(TAG, "ImService: killing process: " + pid);
android.os.Process.killProcess(pid);
}*/
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public void showToast(CharSequence text, int duration) {
Message msg = Message.obtain(mServiceHandler, EVENT_SHOW_TOAST, duration, 0, text);
msg.sendToTarget();
}
public StatusBarNotifier getStatusBarNotifier() {
return mStatusBarNotifier;
}
public OtrChatManager getOtrChatManager() {
initOtr() ;
return mOtrChatManager;
}
public void scheduleReconnect(long delay) {
if (!isNetworkAvailable()) {
// Don't schedule reconnect if no network available. We will try to
// reconnect when network state become CONNECTED.
return;
}
mServiceHandler.postDelayed(new Runnable() {
public void run() {
reestablishConnections();
}
}, delay);
}
IImConnection createConnection(final long providerId, final long accountId) {
final IImConnection[] results = new IImConnection[1];
Debug.wrapExceptions(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
results[0] = do_createConnection(providerId, accountId);
}
});
return results[0];
}
IImConnection do_createConnection(long providerId, long accountId) {
Map<String, String> settings = loadProviderSettings(providerId);
ConnectionFactory factory = ConnectionFactory.getInstance();
try {
ImConnection conn = factory.createConnection(settings, this);
ImConnectionAdapter imConnectionAdapter =
new ImConnectionAdapter(providerId, accountId, conn, this);
mConnections.add(imConnectionAdapter);
initOtr();
mOtrChatManager.addConnection(imConnectionAdapter);
final int N = mRemoteListeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IConnectionCreationListener listener = mRemoteListeners.getBroadcastItem(i);
try {
listener.onConnectionCreated(imConnectionAdapter);
} catch (RemoteException e) {
// The RemoteCallbackList will take care of removing the
// dead listeners.
}
}
mRemoteListeners.finishBroadcast();
return imConnectionAdapter;
} catch (ImException e) {
debug("Error creating connection", e);
return null;
}
}
void removeConnection(ImConnectionAdapter connection) {
if (mOtrChatManager != null)
mOtrChatManager.removeConnection(connection);
mConnections.remove(connection);
}
private boolean isNetworkAvailable() {
return mNetworkConnectivityListener.getState() == State.CONNECTED;
}
void networkStateChanged() {
if (mNetworkConnectivityListener == null) {
return;
}
NetworkInfo networkInfo = mNetworkConnectivityListener.getNetworkInfo();
NetworkInfo.State state = networkInfo.getState();
debug("networkStateChanged:" + state);
int oldType = mNetworkType;
mNetworkType = networkInfo.getType();
// Notify the connection that network type has changed. Note that this
// only work for connected connections, we need to reestablish if it's
// suspended.
if (mNetworkType != oldType && isNetworkAvailable()) {
for (ImConnectionAdapter conn : mConnections) {
conn.networkTypeChanged();
}
}
switch (state) {
case CONNECTED:
if (mNeedCheckAutoLogin) {
mNeedCheckAutoLogin = false;
autoLogin();
break;
}
reestablishConnections();
break;
case DISCONNECTED:
if (!isNetworkAvailable()) {
suspendConnections();
}
break;
}
}
// package private for inner class access
void reestablishConnections() {
if (!isNetworkAvailable()) {
return;
}
for (ImConnectionAdapter conn : mConnections) {
int connState = conn.getState();
if (connState == ImConnection.SUSPENDED) {
conn.reestablishSession();
}
}
}
private void suspendConnections() {
for (ImConnectionAdapter conn : mConnections) {
if (conn.getState() != ImConnection.LOGGED_IN) {
continue;
}
conn.suspend();
}
}
private final IRemoteImService.Stub mBinder = new IRemoteImService.Stub() {
@Override
public List<ImPluginInfo> getAllPlugins() {
return new ArrayList<ImPluginInfo>(mPluginHelper.getPluginsInfo());
}
@Override
public void addConnectionCreatedListener(IConnectionCreationListener listener) {
if (listener != null) {
mRemoteListeners.register(listener);
}
}
@Override
public void removeConnectionCreatedListener(IConnectionCreationListener listener) {
if (listener != null) {
mRemoteListeners.unregister(listener);
}
}
@Override
public IImConnection createConnection(long providerId, long accountId) {
return RemoteImService.this.createConnection(providerId, accountId);
}
@Override
public List getActiveConnections() {
ArrayList<IBinder> result = new ArrayList<IBinder>(mConnections.size());
for (IImConnection conn : mConnections) {
result.add(conn.asBinder());
}
return result;
}
@Override
public void dismissNotifications(long providerId) {
mStatusBarNotifier.dismissNotifications(providerId);
}
@Override
public void dismissChatNotification(long providerId, String username) {
mStatusBarNotifier.dismissChatNotification(providerId, username);
}
@Override
public boolean unlockOtrStore (String password)
{
OtrAndroidKeyManagerImpl.setKeyStorePassword(password);
return true;
}
@Override
public IOtrKeyManager getOtrKeyManager ()
{
try {
return OtrAndroidKeyManagerImpl.getInstance(RemoteImService.this);
} catch (IOException e) {
OtrDebugLogger.log("unable to get keymanager instance", e);
return null;
}
}
@Override
public void setKillProcessOnStop (boolean killProcessOnStop)
{
mKillProcessOnStop = killProcessOnStop;
}
};
private boolean mKillProcessOnStop = false;
/*
//the concept of "background data is deprecated from Android
// the only thing that matters is checking if Network is available and connected
private final class SettingsMonitor extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED.equals(action)) {
ConnectivityManager manager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
setBackgroundData(manager.getBackgroundDataSetting());
handleBackgroundDataSettingChange();
}
}
}
*/
private final class ServiceHandler extends Handler {
public ServiceHandler() {
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_SHOW_TOAST:
Toast.makeText(RemoteImService.this, (CharSequence) msg.obj, msg.arg1).show();
break;
case EVENT_NETWORK_STATE_CHANGED:
networkStateChanged();
break;
default:
}
}
}
@Override
public void sessionStatusChanged(SessionID sessionID) {
SessionStatus sStatus = mOtrChatManager.getSessionStatus(sessionID);
String msg = "";
if (sStatus == SessionStatus.PLAINTEXT) {
msg = getString(R.string.otr_session_status_plaintext);
} else if (sStatus == SessionStatus.ENCRYPTED) {
msg = getString(R.string.otr_session_status_encrypted);
} else if (sStatus == SessionStatus.FINISHED) {
msg = getString(R.string.otr_session_status_finished);
}
//showToast(msg, Toast.LENGTH_SHORT);
}
}