/*
* 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.OtrChatManager;
import info.guardianproject.otr.app.im.IChatSessionManager;
import info.guardianproject.otr.app.im.IConnectionListener;
import info.guardianproject.otr.app.im.IContactListManager;
import info.guardianproject.otr.app.im.IInvitationListener;
import info.guardianproject.otr.app.im.app.ImApp;
import info.guardianproject.otr.app.im.engine.ChatGroupManager;
import info.guardianproject.otr.app.im.engine.ConnectionListener;
import info.guardianproject.otr.app.im.engine.Contact;
import info.guardianproject.otr.app.im.engine.ImConnection;
import info.guardianproject.otr.app.im.engine.ImErrorInfo;
import info.guardianproject.otr.app.im.engine.ImException;
import info.guardianproject.otr.app.im.engine.Invitation;
import info.guardianproject.otr.app.im.engine.InvitationListener;
import info.guardianproject.otr.app.im.engine.Presence;
import info.guardianproject.otr.app.im.provider.Imps;
import info.guardianproject.util.Debug;
import java.util.HashMap;
import java.util.Map;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
public class ImConnectionAdapter extends info.guardianproject.otr.app.im.IImConnection.Stub {
private static final String[] SESSION_COOKIE_PROJECTION = { Imps.SessionCookies.NAME,
Imps.SessionCookies.VALUE, };
private static final int COLUMN_SESSION_COOKIE_NAME = 0;
private static final int COLUMN_SESSION_COOKIE_VALUE = 1;
ImConnection mConnection;
private ConnectionListenerAdapter mConnectionListener;
private InvitationListenerAdapter mInvitationListener;
final RemoteCallbackList<IConnectionListener> mRemoteConnListeners = new RemoteCallbackList<IConnectionListener>();
ChatSessionManagerAdapter mChatSessionManager;
ContactListManagerAdapter mContactListManager;
ChatGroupManager mGroupManager;
RemoteImService mService;
long mProviderId = -1;
long mAccountId = -1;
boolean mAutoLoadContacts;
int mConnectionState = ImConnection.DISCONNECTED;
public ImConnectionAdapter(long providerId, long accountId, ImConnection connection, RemoteImService service) {
mProviderId = providerId;
mAccountId = accountId;
mConnection = connection;
mService = service;
mConnectionListener = new ConnectionListenerAdapter();
mConnection.addConnectionListener(mConnectionListener);
if ((connection.getCapability() & ImConnection.CAPABILITY_GROUP_CHAT) != 0) {
mGroupManager = mConnection.getChatGroupManager();
mInvitationListener = new InvitationListenerAdapter();
mGroupManager.setInvitationListener(mInvitationListener);
}
mChatSessionManager = new ChatSessionManagerAdapter(this);
mContactListManager = new ContactListManagerAdapter(this);
}
public ImConnection getAdaptee() {
return mConnection;
}
public RemoteImService getContext() {
return mService;
}
@Override
public long getProviderId() {
return mProviderId;
}
@Override
public long getAccountId() {
return mAccountId;
}
@Override
public boolean isUsingTor() {
return mConnection.isUsingTor();
}
@Override
public int[] getSupportedPresenceStatus() {
return mConnection.getSupportedPresenceStatus();
}
public void networkTypeChanged() {
mConnection.networkTypeChanged();
}
boolean reestablishSession() {
mConnectionState = ImConnection.LOGGING_IN;
ContentResolver cr = mService.getContentResolver();
if ((mConnection.getCapability() & ImConnection.CAPABILITY_SESSION_REESTABLISHMENT) != 0) {
Map<String, String> cookie = querySessionCookie(cr);
if (cookie != null) {
RemoteImService.debug("re-establish session");
try {
mConnection.reestablishSessionAsync(cookie);
return true;
} catch (IllegalArgumentException e) {
RemoteImService.debug("Invalid session cookie, probably modified by others.");
clearSessionCookie(cr);
}
}
}
return false;
}
private Uri getSessionCookiesUri() {
Uri.Builder builder = Imps.SessionCookies.CONTENT_URI_SESSION_COOKIES_BY.buildUpon();
ContentUris.appendId(builder, mProviderId);
ContentUris.appendId(builder, mAccountId);
return builder.build();
}
@Override
public void login(final String passwordTemp, final boolean autoLoadContacts, final boolean retry) {
Debug.wrapExceptions(new Runnable() {
@Override
public void run() {
do_login(passwordTemp, autoLoadContacts, retry);
}
});
}
public void do_login(String passwordTemp, boolean autoLoadContacts, boolean retry) {
mAutoLoadContacts = autoLoadContacts;
mConnectionState = ImConnection.LOGGING_IN;
mConnection.loginAsync(mAccountId, passwordTemp, mProviderId, retry);
}
private void loadSavedPresence ()
{
ContentResolver cr = mService.getContentResolver();
// Imps.ProviderSettings.setPresence(cr, mProviderId, status, statusText);
int presenceState = Imps.ProviderSettings.getIntValue(cr, mProviderId, Imps.ProviderSettings.PRESENCE_STATE);
String presenceStatusMessage = Imps.ProviderSettings.getStringValue(cr, mProviderId, Imps.ProviderSettings.PRESENCE_STATUS_MESSAGE);
if (presenceState != -1)
{
Presence presence = new Presence();
presence.setStatus(presenceState);
presence.setStatusText(presenceStatusMessage);
try {
mConnection.updateUserPresenceAsync(presence);
} catch (ImException e) {
Log.e(ImApp.LOG_TAG,"unable able to update presence",e);
}
}
}
@Override
public void sendHeartbeat() throws RemoteException {
if (mConnection != null)
mConnection.sendHeartbeat(mService.getHeartbeatInterval());
}
@Override
public void setProxy(String type, String host, int port) throws RemoteException {
mConnection.setProxy(type, host, port);
}
private HashMap<String, String> querySessionCookie(ContentResolver cr) {
Cursor c = cr.query(getSessionCookiesUri(), SESSION_COOKIE_PROJECTION, null, null, null);
if (c == null) {
return null;
}
HashMap<String, String> cookie = null;
if (c.getCount() > 0) {
cookie = new HashMap<String, String>();
while (c.moveToNext()) {
cookie.put(c.getString(COLUMN_SESSION_COOKIE_NAME),
c.getString(COLUMN_SESSION_COOKIE_VALUE));
}
}
c.close();
return cookie;
}
@Override
public void logout() {
OtrChatManager.endSessionsForAccount(mConnection.getLoginUser());
mConnectionState = ImConnection.LOGGING_OUT;
mConnection.logout();
}
@Override
public synchronized void cancelLogin() {
if (mConnectionState >= ImConnection.LOGGED_IN) {
// too late
return;
}
mConnectionState = ImConnection.LOGGING_OUT;
mConnection.logout();
}
void suspend() {
mConnectionState = ImConnection.SUSPENDING;
mConnection.suspend();
}
@Override
public void registerConnectionListener(IConnectionListener listener) {
if (listener != null) {
mRemoteConnListeners.register(listener);
}
}
@Override
public void unregisterConnectionListener(IConnectionListener listener) {
if (listener != null) {
mRemoteConnListeners.unregister(listener);
}
}
@Override
public void setInvitationListener(IInvitationListener listener) {
if (mInvitationListener != null) {
mInvitationListener.mRemoteListener = listener;
}
}
@Override
public IChatSessionManager getChatSessionManager() {
return mChatSessionManager;
}
@Override
public IContactListManager getContactListManager() {
return mContactListManager;
}
@Override
public int getChatSessionCount() {
if (mChatSessionManager == null) {
return 0;
}
return mChatSessionManager.getChatSessionCount();
}
public Contact getLoginUser() {
return mConnection.getLoginUser();
}
@Override
public Presence getUserPresence() {
return mConnection.getUserPresence();
}
@Override
public int updateUserPresence(Presence newPresence) {
try {
mConnection.updateUserPresenceAsync(newPresence);
} catch (ImException e) {
return e.getImError().getCode();
}
return ImErrorInfo.NO_ERROR;
}
@Override
public int getState() {
return mConnectionState;
}
@Override
public void rejectInvitation(long id) {
handleInvitation(id, false);
}
@Override
public void acceptInvitation(long id) {
handleInvitation(id, true);
}
private void handleInvitation(long id, boolean accept) {
if (mGroupManager == null) {
return;
}
ContentResolver cr = mService.getContentResolver();
Cursor c = cr.query(ContentUris.withAppendedId(Imps.Invitation.CONTENT_URI, id), null,
null, null, null);
if (c == null) {
return;
}
if (c.moveToFirst()) {
String inviteId = c.getString(c.getColumnIndexOrThrow(Imps.Invitation.INVITE_ID));
int status;
if (accept) {
mGroupManager.acceptInvitationAsync(inviteId);
status = Imps.Invitation.STATUS_ACCEPTED;
} else {
mGroupManager.rejectInvitationAsync(inviteId);
status = Imps.Invitation.STATUS_REJECTED;
}
// TODO c.updateInt(c.getColumnIndexOrThrow(Imps.Invitation.STATUS), status);
// c.commitUpdates();
}
c.close();
}
void saveSessionCookie(ContentResolver cr) {
Map<String, String> cookies = mConnection.getSessionContext();
int i = 0;
ContentValues[] valuesList = new ContentValues[cookies.size()];
for (Map.Entry<String, String> entry : cookies.entrySet()) {
ContentValues values = new ContentValues(2);
values.put(Imps.SessionCookies.NAME, entry.getKey());
values.put(Imps.SessionCookies.VALUE, entry.getValue());
valuesList[i++] = values;
}
cr.bulkInsert(getSessionCookiesUri(), valuesList);
}
void clearSessionCookie(ContentResolver cr) {
cr.delete(getSessionCookiesUri(), null, null);
}
void updateAccountStatusInDb() {
Presence p = getUserPresence();
int presenceStatus = Imps.Presence.OFFLINE;
int connectionStatus = convertConnStateForDb(mConnectionState);
if (p != null) {
presenceStatus = ContactListManagerAdapter.convertPresenceStatus(p);
}
ContentResolver cr = mService.getContentResolver();
Uri uri = Imps.AccountStatus.CONTENT_URI;
ContentValues values = new ContentValues();
values.put(Imps.AccountStatus.ACCOUNT, mAccountId);
values.put(Imps.AccountStatus.PRESENCE_STATUS, presenceStatus);
values.put(Imps.AccountStatus.CONNECTION_STATUS, connectionStatus);
cr.insert(uri, values);
}
private static int convertConnStateForDb(int state) {
switch (state) {
case ImConnection.DISCONNECTED:
case ImConnection.LOGGING_OUT:
return Imps.ConnectionStatus.OFFLINE;
case ImConnection.LOGGING_IN:
return Imps.ConnectionStatus.CONNECTING;
case ImConnection.LOGGED_IN:
return Imps.ConnectionStatus.ONLINE;
case ImConnection.SUSPENDED:
case ImConnection.SUSPENDING:
return Imps.ConnectionStatus.SUSPENDED;
default:
return Imps.ConnectionStatus.OFFLINE;
}
}
final class ConnectionListenerAdapter implements ConnectionListener {
@Override
public void onStateChanged(final int state, final ImErrorInfo error) {
synchronized (this) {
if (state == ImConnection.LOGGED_IN && mConnectionState == ImConnection.LOGGING_OUT) {
// A bit tricky here. The engine did login successfully
// but the notification comes a bit late; user has already
// issued a cancelLogin() and that cannot be undone. Here
// we have to ignore the LOGGED_IN event and wait for
// the upcoming DISCONNECTED.
return;
}
if (state != ImConnection.DISCONNECTED) {
mConnectionState = state;
}
}
ContentResolver cr = mService.getContentResolver();
if (state == ImConnection.LOGGED_IN) {
if ((mConnection.getCapability() & ImConnection.CAPABILITY_SESSION_REESTABLISHMENT) != 0) {
saveSessionCookie(cr);
}
if (mAutoLoadContacts)
{
mContactListManager.loadContactLists();
}
for (ChatSessionAdapter session : mChatSessionManager.mActiveChatSessionAdapters
.values()) {
session.sendPostponedMessages();
}
// mService.getStatusBarNotifier().notifyLoggedIn(mProviderId, mAccountId);
loadSavedPresence();
} else if (state == ImConnection.DISCONNECTED) {
clearSessionCookie(cr);
// mContactListManager might still be null if we fail
// immediately in loginAsync (say, an invalid host URL)
if (mContactListManager != null) { // n8fr8 2015-01-21 Why are we clearing this?
mContactListManager.clearOnLogout();
}
mConnectionState = state;
} else if (state == ImConnection.SUSPENDED && error != null) {
// re-establish failed, schedule to retry
mService.scheduleReconnect(5000);
}
updateAccountStatusInDb();
synchronized (mRemoteConnListeners) {
final int N = mRemoteConnListeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IConnectionListener listener = mRemoteConnListeners.getBroadcastItem(i);
try {
listener.onStateChanged(ImConnectionAdapter.this, state, error);
} catch (RemoteException e) {
// The RemoteCallbackList will take care of removing the
// dead listeners.
}
}
mRemoteConnListeners.finishBroadcast();
}
if (state == ImConnection.DISCONNECTED) {
// NOTE: if this logic is changed, the logic in ImApp.MyConnListener must be changed to match
mService.removeConnection(ImConnectionAdapter.this);
}
}
@Override
public void onUserPresenceUpdated() {
updateAccountStatusInDb();
final int N = mRemoteConnListeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IConnectionListener listener = mRemoteConnListeners.getBroadcastItem(i);
try {
listener.onUserPresenceUpdated(ImConnectionAdapter.this);
} catch (RemoteException e) {
// The RemoteCallbackList will take care of removing the
// dead listeners.
}
}
mRemoteConnListeners.finishBroadcast();
}
@Override
public void onUpdatePresenceError(final ImErrorInfo error) {
final int N = mRemoteConnListeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IConnectionListener listener = mRemoteConnListeners.getBroadcastItem(i);
try {
listener.onUpdatePresenceError(ImConnectionAdapter.this, error);
} catch (RemoteException e) {
// The RemoteCallbackList will take care of removing the
// dead listeners.
}
}
mRemoteConnListeners.finishBroadcast();
}
}
final class InvitationListenerAdapter implements InvitationListener {
IInvitationListener mRemoteListener;
@Override
public void onGroupInvitation(Invitation invitation) {
String sender = invitation.getSender().getUser();
ContentValues values = new ContentValues(7);
values.put(Imps.Invitation.PROVIDER, mProviderId);
values.put(Imps.Invitation.ACCOUNT, mAccountId);
values.put(Imps.Invitation.INVITE_ID, invitation.getInviteID());
values.put(Imps.Invitation.SENDER, sender);
values.put(Imps.Invitation.GROUP_NAME, invitation.getGroupAddress().getUser());
values.put(Imps.Invitation.NOTE, invitation.getReason());
values.put(Imps.Invitation.STATUS, Imps.Invitation.STATUS_PENDING);
ContentResolver resolver = mService.getContentResolver();
Uri uri = resolver.insert(Imps.Invitation.CONTENT_URI, values);
long id = ContentUris.parseId(uri);
try {
if (mRemoteListener != null) {
mRemoteListener.onGroupInvitation(id);
return;
}
} catch (RemoteException e) {
RemoteImService.debug("onGroupInvitation: dead listener " + mRemoteListener
+ "; removing", e);
mRemoteListener = null;
}
// No listener registered or failed to notify the listener, send a
// notification instead.
mService.getStatusBarNotifier().notifyGroupInvitation(mProviderId, mAccountId, id,
sender);
}
}
}