<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.SEND_SMS" />
+ <uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
public static final String CIR_CHANNEL = "imps.cir-channel";
/**
+ * The backup CIR channel used when the application is in background.
+ */
+ public static final String BACKUP_CIR_CHANNEL = "imps.backup-cir-channel";
+
+ /**
* The host of the IMPS server.
*/
public static final String HOST = "imps.host";
/**
+ * The address for SMS binding.
+ */
+ public static final String SMS_ADDR = "imps.sms.addr";
+
+ /**
+ * The port number for SMS binding.
+ */
+ public static final String SMS_PORT = "imps.sms.port";
+
+ /**
+ * The address for the SMS CIR channel.
+ */
+ public static final String SMS_CIR_ADDR = "imps.sms.cir.addr";
+
+ /**
+ * The port number for SMS CIR channel.
+ */
+ public static final String SMS_CIR_PORT = "imps.sms.cir.port";
+
+ /**
* The client ID.
*/
public static final String CLIENT_ID = "imps.client-id";
public static final String SECURE_LOGIN = "imps.secure-login";
/**
+ * Determines whether to send authentication through sms or not.
+ */
+ public static final String SMS_AUTH = "imps.sms-auth";
+
+ /**
* Determines whether only the basic presence will be fetched from the server.
*/
public static final String BASIC_PA_ONLY = "imps.basic-pa-only";
/**
+ * Determines whether to poll presence from the server or use subscribe/notify
+ * method.
+ */
+ public static final String POLL_PRESENCE = "imps.poll-presence";
+
+ /**
+ * The presence polling interval in milliseconds. Only valid when
+ * {@link #POLL_PRESENCE} is set to true.
+ */
+ public static final String PRESENCE_POLLING_INTERVAL = "imps.presence-polling-interval";
+
+ /**
* The full name of the custom presence mapping is to be used. If not set,
* the default one will be used.
*/
<MultiAutoCompleteTextView android:id="@+id/email"
android:layout_width="fill_parent"
- android:layout_height="wrap_content" android:layout_marginTop="10dip" />
+ android:layout_height="wrap_content" />
+
+ <TextView android:id="@+id/choose_list_label"
+ android:layout_marginTop="10dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/choose_list_label" />
+
+ <Spinner android:id="@+id/choose_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:drawSelectorOnTop="true"
+ android:prompt="@string/choose_list_label" />
<TextView android:layout_marginTop="10dip"
android:layout_width="fill_parent"
<item android:id="@+id/menu_view_friend_list"
android:title="@string/menu_view_contact_list"
- android:icon="@drawable/ic_menu_friend_list"/>
+ android:icon="@*android:drawable/ic_menu_cc" />
<item android:id="@+id/menu_switch_chats"
android:title="@string/menu_switch_chats"
android:alphabeticShortcut=" "
- android:icon="@drawable/ic_menu_chat_dashboard" />
+ android:icon="@*android:drawable/ic_menu_chat_dashboard" />
<item android:id="@+id/menu_insert_smiley"
android:title="@string/menu_insert_smiley"
- android:icon="@drawable/ic_menu_emoticons" />
+ android:icon="@*android:drawable/ic_menu_emoticons" />
<item android:id="@+id/menu_end_conversation"
android:title="@string/menu_end_conversation"
- android:icon="@drawable/ic_menu_end_conversation" />
+ android:icon="@*android:drawable/ic_menu_end_conversation" />
<item android:id="@+id/menu_block_contact"
android:title="@string/menu_block_contact"
- android:icon="@drawable/ic_menu_block" />
+ android:icon="@*android:drawable/ic_menu_block" />
<item android:id="@+id/menu_invite_contact"
android:title="@string/menu_invite_contact"
- android:icon="@drawable/ic_menu_invite" />
+ android:icon="@*android:drawable/ic_menu_invite" />
<item android:id="@+id/menu_view_profile"
android:title="@string/menu_view_profile"
android:alphabeticShortcut="k"
android:visible="false"
android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_0"
+ android:alphabeticShortcut="0"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_1"
+ android:alphabeticShortcut="1"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_2"
+ android:alphabeticShortcut="2"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_3"
+ android:alphabeticShortcut="3"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_4"
+ android:alphabeticShortcut="4"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_5"
+ android:alphabeticShortcut="5"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_6"
+ android:alphabeticShortcut="6"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_7"
+ android:alphabeticShortcut="7"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_8"
+ android:alphabeticShortcut="8"
+ android:visible="false"
+ android:enabled="true" />
+
+ <item
+ android:id="@+id/menu_quick_switch_9"
+ android:alphabeticShortcut="9"
+ android:visible="false"
+ android:enabled="true" />
+
</menu>
<item android:id="@+id/menu_invite_user"
android:title="@string/menu_add_contact"
- android:icon="@drawable/ic_menu_invite" />
+ android:icon="@*android:drawable/ic_menu_invite" />
<item android:id="@+id/menu_blocked_contacts"
android:title="@string/menu_view_blocked"
- android:icon="@drawable/ic_menu_blocked_user" />
+ android:icon="@*android:drawable/ic_menu_blocked_user" />
<item android:id="@+id/menu_view_accounts"
android:title="@string/menu_view_accounts"
- android:icon="@drawable/ic_menu_account_list" />
+ android:icon="@*android:drawable/ic_menu_account_list" />
<item android:id="@+id/menu_settings"
android:title="@string/menu_settings"
<!-- Chat screen menu and context menu items.-->
<!-- Screen menu item on the chat screen to go back the contact list screen.
- May be overrided by the plugin.-->
+ May be overrided by the plugin.-->
<string name="menu_view_contact_list">Contact list</string>
<!-- Screen menu item on the chat screen to invite another contact to join the chat.
Currently not supported.-->
<!-- Screen menu item on the chat screen to insert smiley. May be overrided by the plugin.-->
<string name="menu_insert_smiley">Insert smiley</string>
+ <!-- This string is used in the chat dashboard. When there is a shortcut assigned to a chat
+ it allows the user to press menu+[0-9] to quickly switch beteen chats. This string of
+ menu+ is used to build up the UI to show the user which shortcut to use. A number (0-9) will
+ be appended to the end of this. -->
+ <string name="menu_plus">Menu+"</string>
+
<!-- The title of a simple input dialog. Currently not used.-->
<string name="default_input_title">Input</string>
<string name="add_contact_title">Add contact</string>
<!-- The label of the email input box on the Add Contact Screen. -->
<string name="input_contact_label">Email address of person you wish to invite:</string>
+ <!-- The label of the list spinner on the Add Contact Screen. -->
+ <string name="choose_list_label">Choose a list:</string>
<!-- This is an instruction message displayed under the input box on the Add Contact Screen. -->
<string name="invite_instruction">Type a name to add from Contacts.</string>
<!-- The label of the button on the Add Contact Screen to send the invitation. -->
* match the smiley texts and smiley names defined in strings.xml.
*/
static final int[] SMILEY_RES_IDS = {
- R.drawable.emo_im_happy,
- R.drawable.emo_im_sad,
- R.drawable.emo_im_winking,
- R.drawable.emo_im_tongue_sticking_out,
- R.drawable.emo_im_surprised,
- R.drawable.emo_im_kissing,
- R.drawable.emo_im_yelling,
- R.drawable.emo_im_cool,
- R.drawable.emo_im_money_mouth,
- R.drawable.emo_im_foot_in_mouth,
- R.drawable.emo_im_embarrased,
- R.drawable.emo_im_angel,
- R.drawable.emo_im_undecided,
- R.drawable.emo_im_crying,
- R.drawable.emo_im_lips_are_sealed,
- R.drawable.emo_im_laughing,
- R.drawable.emo_im_wtf
+ android.R.drawable.emo_im_happy,
+ android.R.drawable.emo_im_sad,
+ android.R.drawable.emo_im_winking,
+ android.R.drawable.emo_im_tongue_sticking_out,
+ android.R.drawable.emo_im_surprised,
+ android.R.drawable.emo_im_kissing,
+ android.R.drawable.emo_im_yelling,
+ android.R.drawable.emo_im_cool,
+ android.R.drawable.emo_im_money_mouth,
+ android.R.drawable.emo_im_foot_in_mouth,
+ android.R.drawable.emo_im_embarrassed,
+ android.R.drawable.emo_im_angel,
+ android.R.drawable.emo_im_undecided,
+ android.R.drawable.emo_im_crying,
+ android.R.drawable.emo_im_lips_are_sealed,
+ android.R.drawable.emo_im_laughing,
+ android.R.drawable.emo_im_wtf
};
}
import static android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI;
import android.app.Activity;
import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
+import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.widget.Button;
import android.widget.MultiAutoCompleteTextView;
import android.widget.ResourceCursorAdapter;
+import android.widget.SimpleCursorAdapter;
+import android.widget.Spinner;
import android.widget.TextView;
import com.android.im.IContactList;
import com.android.im.IContactListManager;
public class AddContactActivity extends Activity {
+ private static final String[] CONTACT_LIST_PROJECTION = {
+ Im.ContactList._ID,
+ Im.ContactList.NAME,
+ };
+ private static final int CONTACT_LIST_NAME_COLUMN = 1;
+
private MultiAutoCompleteTextView mAddressList;
+ private Spinner mListSpinner;
Button mInviteButton;
ImApp mApp;
SimpleAlertHandler mHandler;
private long mProviderId;
- private String mListName;
+ private long mAccountId;
private String mDefaultDomain;
@Override
mAddressList.setAdapter(new EmailAddressAdapter(this));
mAddressList.setTokenizer(new Rfc822Tokenizer());
mAddressList.addTextChangedListener(mTextWatcher);
+
+ mListSpinner = (Spinner) findViewById(R.id.choose_list);
+
+ Cursor c = queryContactLists();
+ int initSelection = searchInitListPos(c, getIntent().getStringExtra(
+ ImServiceConstants.EXTRA_INTENT_LIST_NAME));
+ SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
+ android.R.layout.simple_spinner_item,
+ c,
+ new String[] {Im.ContactList.NAME},
+ new int[] {android.R.id.text1});
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mListSpinner.setAdapter(adapter);
+ mListSpinner.setSelection(initSelection);
+
mInviteButton = (Button) findViewById(R.id.invite);
- mInviteButton.setText(brandingRes.getString(BrandingResourceIDs.STRING_BUTTON_ADD_CONTACT));
+ mInviteButton.setText(brandingRes.getString(
+ BrandingResourceIDs.STRING_BUTTON_ADD_CONTACT));
mInviteButton.setOnClickListener(mButtonHandler);
mInviteButton.setEnabled(false);
}
+ private Cursor queryContactLists() {
+ Uri uri = Im.ContactList.CONTENT_URI;
+ uri = ContentUris.withAppendedId(uri, mProviderId);
+ uri = ContentUris.withAppendedId(uri, mAccountId);
+ Cursor c = managedQuery(uri, CONTACT_LIST_PROJECTION, null, null);
+ return c;
+ }
+
+ private int searchInitListPos(Cursor c, String listName) {
+ if (TextUtils.isEmpty(listName)) {
+ return 0;
+ }
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ if (listName.equals(c.getString(CONTACT_LIST_NAME_COLUMN))) {
+ return c.getPosition();
+ }
+ }
+ return 0;
+ }
+
private void resolveIntent(Intent intent) {
- mProviderId = intent.getLongExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, -1);
- mListName = intent.getStringExtra(ImServiceConstants.EXTRA_INTENT_LIST_NAME);
+ mProviderId = intent.getLongExtra(
+ ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, -1);
+ mAccountId = intent.getLongExtra(
+ ImServiceConstants.EXTRA_INTENT_ACCOUNT_ID, -1);
mDefaultDomain = Im.ProviderSettings.getStringValue(getContentResolver(),
mProviderId, ImpsConfigNames.DEFAULT_DOMAIN);
}
IImConnection conn = mApp.getConnection(mProviderId);
IContactList list = getContactList(conn);
if (list == null) {
- Log.e(ImApp.LOG_TAG, "<AddContactActivity> can't find given contact list:" + mListName);
+ Log.e(ImApp.LOG_TAG, "<AddContactActivity> can't find given contact list:"
+ + getSelectedListName());
finish();
} else {
boolean fail = false;
try {
IContactListManager contactListMgr = conn.getContactListManager();
- if (!TextUtils.isEmpty(mListName)) {
- return contactListMgr.getContactList(mListName);
+ String listName = getSelectedListName();
+ if (!TextUtils.isEmpty(listName)) {
+ return contactListMgr.getContactList(listName);
} else {
// Use the default list
List<IBinder> lists = contactListMgr.getContactLists();
}
}
+ private String getSelectedListName() {
+ Cursor c = (Cursor) mListSpinner.getSelectedItem();
+ return (c == null) ? null : c.getString(CONTACT_LIST_NAME_COLUMN);
+ }
+
private View.OnClickListener mButtonHandler = new View.OnClickListener() {
public void onClick(View v) {
mApp.callWhenServiceConnected(mHandler, new Runnable() {
case R.id.menu_invite_user:
Intent i = new Intent(ContactListActivity.this, AddContactActivity.class);
i.putExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, mProviderId);
+ i.putExtra(ImServiceConstants.EXTRA_INTENT_ACCOUNT_ID, mAccountId);
i.putExtra(ImServiceConstants.EXTRA_INTENT_LIST_NAME,
mContactListView.getSelectedContactList());
startActivity(i);
if (chatSelected) {
menu.add(0, MENU_END_CONVERSATION, 0, menu_end_conversation)
- .setIcon(R.drawable.ic_menu_end_conversation)
+ .setIcon(com.android.internal.R.drawable.ic_menu_end_conversation)
.setOnMenuItemClickListener(mContextMenuHandler);
menu.add(0, MENU_VIEW_PROFILE, 0, menu_view_profile)
.setIcon(R.drawable.ic_menu_my_profile)
.setOnMenuItemClickListener(mContextMenuHandler);
if (allowBlock) {
menu.add(0, MENU_BLOCK_CONTACT, 0, menu_block_contact)
- .setIcon(R.drawable.ic_menu_block)
+ .setIcon(com.android.internal.R.drawable.ic_menu_block)
.setOnMenuItemClickListener(mContextMenuHandler);
}
} else if (contactSelected) {
menu.add(0, MENU_START_CONVERSATION, 0, menu_start_conversation)
- .setIcon(R.drawable.ic_menu_start_conversation)
+ .setIcon(com.android.internal.R.drawable.ic_menu_start_conversation)
.setOnMenuItemClickListener(mContextMenuHandler);
menu.add(0, MENU_VIEW_PROFILE, 0, menu_view_profile)
.setIcon(R.drawable.ic_menu_view_profile)
.setOnMenuItemClickListener(mContextMenuHandler);
if (allowBlock) {
menu.add(0, MENU_BLOCK_CONTACT, 0, menu_block_contact)
- .setIcon(R.drawable.ic_menu_block)
+ .setIcon(com.android.internal.R.drawable.ic_menu_block)
.setOnMenuItemClickListener(mContextMenuHandler);
}
menu.add(0, MENU_DELETE_CONTACT, 0, menu_delete_contact)
String statusString = brandingRes.getString(
PresenceUtils.getStatusStringRes(status));
SpannableString s = new SpannableString("+ " + statusString);
- ImageSpan imageSpan = new ImageSpan(
- brandingRes.getDrawable(PresenceUtils.getStatusIconId(status)));
- s.setSpan(imageSpan, 0, 1, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ Drawable statusIcon = brandingRes.getDrawable(
+ PresenceUtils.getStatusIconId(status));
+ statusIcon.setBounds(0, 0, statusIcon.getIntrinsicWidth(),
+ statusIcon.getIntrinsicHeight());
+ s.setSpan(new ImageSpan(statusIcon), 0, 1,
+ SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
txtStatus.setText(s);
txtClientType.setText(getClientTypeString(clientType));
package com.android.im.app;
import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
import android.app.Activity;
import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.provider.Im;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
private String mUserName;
Activity mActivity;
+ private int mProviderIdColumn;
+ private int mAccountIdColumn;
+ private int mUsernameColumn;
+ private int mNicknameColumn;
+ private int mPresenceStatusColumn;
+ private int mLastUnreadMessageColumn;
+ private int mShortcutColumn;
+
public Dashboard(Context screen, AttributeSet attrs) {
super(screen, attrs);
}
ContentResolver cr = mContext.getContentResolver();
- mChats = cr.query(Im.Contacts.CONTENT_URI_CHAT_CONTACTS, null, null, null, null);
+ Cursor c = cr.query(Im.Contacts.CONTENT_URI_CHAT_CONTACTS,
+ null, null, null, null);
+
+ mProviderIdColumn = c.getColumnIndexOrThrow(Im.Contacts.PROVIDER);
+ mAccountIdColumn = c.getColumnIndexOrThrow(Im.Contacts.ACCOUNT);
+ mUsernameColumn = c.getColumnIndexOrThrow(Im.Contacts.USERNAME);
+ mNicknameColumn = c.getColumnIndexOrThrow(Im.Contacts.NICKNAME);
+ mPresenceStatusColumn = c.getColumnIndexOrThrow(Im.Contacts.PRESENCE_STATUS);
+ mLastUnreadMessageColumn = c.getColumnIndexOrThrow(Im.Chats.LAST_UNREAD_MESSAGE);
+ mShortcutColumn = c.getColumnIndexOrThrow(Im.Chats.SHORTCUT);
+ mChats = c;
+
mGallery.setAdapter(new DashboardAdapter(mContext, mChats));
mGallery.setSelection(getInitialPosition());
mGallery.setOnItemClickListener(this);
@Override
public final boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ int code = event.getKeyCode();
+ if (code == KeyEvent.KEYCODE_BACK) {
closeDashboard();
return true;
}
+ if (code >= KeyEvent.KEYCODE_0 && code <= KeyEvent.KEYCODE_9) {
+ if (quickSwitch(mActivity, mChats, code - KeyEvent.KEYCODE_0)) {
+ mActivity.finish();
+ closeDashboard();
+ return true;
+ }
+ }
return super.dispatchKeyEvent(event);
}
return -1;
}
- int usernameColumn = mChats.getColumnIndexOrThrow(Im.Contacts.USERNAME);
- int accountColumn = mChats.getColumnIndexOrThrow(Im.Contacts.ACCOUNT);
-
mChats.moveToPosition(-1);
while (mChats.moveToNext()) {
- if ((mAccountId == mChats.getLong(accountColumn))
- && mUserName.equals(mChats.getString(usernameColumn))) {
+ if ((mAccountId == mChats.getLong(mAccountIdColumn))
+ && mUserName.equals(mChats.getString(mUsernameColumn))) {
return mChats.getPosition();
}
}
}
private class DashboardAdapter extends ResourceCursorAdapter {
+ private String mMenuPlus;
+
public DashboardAdapter(Context context, Cursor c) {
super(context, R.layout.dashboard_item, c);
+
+ mMenuPlus = context.getString(R.string.menu_plus);
}
@Override
public void bindView(View view, Context context, Cursor c) {
- long providerId = c.getLong(c.getColumnIndexOrThrow(Im.Contacts.PROVIDER));
- String nickname = c.getString(c.getColumnIndexOrThrow(Im.Contacts.NICKNAME));
+
+ long providerId = c.getLong(mProviderIdColumn);
+ String nickname = c.getString(mNicknameColumn);
TextView t = (TextView) view.findViewById(R.id.name);
t.setText(nickname);
ImageView i = (ImageView) view.findViewById(R.id.presence);
- int presenceMode = c.getInt(c.getColumnIndexOrThrow(Im.Contacts.PRESENCE_STATUS));
+ int presenceMode = c.getInt(mPresenceStatusColumn);
+ String lastUnreadMsg = c.getString(mLastUnreadMessageColumn);
+
ImApp app = ImApp.getApplication(mActivity);
BrandingResources brandingRes = app.getBrandingResource(providerId);
- Drawable presenceIcon = brandingRes.getDrawable(
- PresenceUtils.getStatusIconId(presenceMode));
- i.setImageDrawable(presenceIcon);
+ if (!TextUtils.isEmpty(lastUnreadMsg)) {
+ i.setImageDrawable(brandingRes.getDrawable(
+ BrandingResourceIDs.DRAWABLE_UNREAD_CHAT));
+ } else {
+ i.setImageDrawable(brandingRes.getDrawable(
+ PresenceUtils.getStatusIconId(presenceMode)));
+ }
+
+ String shortcut = c.getString(mShortcutColumn);
+ TextView shortcutView = (TextView) view.findViewById(R.id.shortcut);
+ if (TextUtils.isEmpty(shortcut)) {
+ shortcutView.setVisibility(View.GONE);
+ } else {
+ shortcutView.setVisibility(View.VISIBLE);
+ shortcutView.setText(mMenuPlus + shortcut);
+ }
setAvatar(c, view);
}
public final void onItemClick(AdapterView parent, View view, int position, long id) {
Cursor c = (Cursor) mGallery.getItemAtPosition(position);
- String contact = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
- long account = c.getLong(c.getColumnIndexOrThrow(Im.Contacts.ACCOUNT));
- long provider = c.getLong(c.getColumnIndexOrThrow(Im.Contacts.PROVIDER));
+ String contact = c.getString(mUsernameColumn);
+ long account = c.getLong(mAccountIdColumn);
+ long provider = c.getLong(mProviderIdColumn);
closeDashboard();
return category;
}
- public static Intent makeChatIntent(ContentResolver resolver, long provider, long account,
+ public static boolean quickSwitch(Activity parent, Cursor cursor, int slot) {
+ if (cursor == null) {
+ return false;
+ }
+
+ if (slot < 0 || slot > 9) {
+ return false;
+ }
+
+ int shortcutColumn = cursor.getColumnIndexOrThrow(Im.Chats.SHORTCUT);
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ int shortcut = cursor.getInt(shortcutColumn);
+ if (shortcut == slot) {
+ long provider = cursor.getLong(
+ cursor.getColumnIndexOrThrow(Im.Contacts.PROVIDER));
+ long account = cursor.getLong(
+ cursor.getColumnIndexOrThrow(Im.Contacts.ACCOUNT));
+ String username = cursor.getString(
+ cursor.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ long chatId = cursor.getLong(
+ cursor.getColumnIndexOrThrow("_id"));
+
+ ContentResolver cr = parent.getContentResolver();
+ parent.startActivity(
+ makeChatIntent(cr, provider, account, username, chatId));
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static Intent makeChatIntent(ContentResolver resolver, long provider, long account,
String contact, long chatId) {
Intent intent = new Intent(Intent.ACTION_VIEW,
ContentUris.withAppendedId(Im.Chats.CONTENT_URI, chatId));
android.R.drawable.presence_invisible);
resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE,
android.R.drawable.presence_offline);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_READ_CHAT,
+ R.drawable.status_chat);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_UNREAD_CHAT,
+ R.drawable.status_chat_new);
resMapping.put(BrandingResourceIDs.DRAWABLE_BLOCK,
R.drawable.ic_im_block);
switch (state) {
case ImConnection.LOGGED_IN:
what = EVENT_CONNECTION_LOGGED_IN;
-
- // Update the active value. We restrict to only one active
- // account per provider right now, so update all accounts of
- // this provider to inactive first and then update this
- // account to active.
- ContentValues values = new ContentValues(1);
- values.put(Im.Account.ACTIVE, 0);
- ContentResolver cr = getContentResolver();
- cr.update(Im.Account.CONTENT_URI, values,
- Im.Account.PROVIDER + "=" + providerId, null);
-
- values.put(Im.Account.ACTIVE, 1);
- cr.update(ContentUris.withAppendedId(Im.Account.CONTENT_URI, conn.getAccountId()),
- values, null, null);
-
break;
case ImConnection.LOGGING_IN:
import android.app.Activity;
import android.app.AlertDialog;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.DialogInterface;
import android.content.Intent;
case R.id.menu_next_chat:
switchChat(1);
return true;
+
+ case R.id.menu_quick_switch_0:
+ case R.id.menu_quick_switch_1:
+ case R.id.menu_quick_switch_2:
+ case R.id.menu_quick_switch_3:
+ case R.id.menu_quick_switch_4:
+ case R.id.menu_quick_switch_5:
+ case R.id.menu_quick_switch_6:
+ case R.id.menu_quick_switch_7:
+ case R.id.menu_quick_switch_8:
+ case R.id.menu_quick_switch_9:
+ ContentResolver cr = getContentResolver();
+ Cursor c = cr.query(Im.Contacts.CONTENT_URI_CHAT_CONTACTS,
+ null,
+ null,
+ null,
+ null);
+ int slot = item.getAlphabeticShortcut() - '0';
+ if (Dashboard.quickSwitch(this, c, slot)) {
+ finish();
+ }
+ c.close();
+ return true;
}
return super.onOptionsItemSelected(item);
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
return;
}
- long providerId = c.getLong(c.getColumnIndexOrThrow(Im.Account.PROVIDER));
+ final long providerId = c.getLong(c.getColumnIndexOrThrow(Im.Account.PROVIDER));
final long accountId = c.getLong(c.getColumnIndexOrThrow(Im.Account._ID));
final String username = c.getString(c.getColumnIndexOrThrow(Im.Account.USERNAME));
String pwExtra = intent.getStringExtra(ImApp.EXTRA_INTENT_PASSWORD);
final String pw = pwExtra != null ? pwExtra
: c.getString(c.getColumnIndexOrThrow(Im.Account.PASSWORD));
+ final boolean isActive = c.getInt(c.getColumnIndexOrThrow(Im.Account.ACTIVE)) == 1;
c.close();
mApp = ImApp.getApplication(this);
mApp.callWhenServiceConnected(mHandler, new Runnable() {
public void run() {
if (mApp.serviceConnected()) {
+ if (!isActive) {
+ activateAccount(providerId, accountId);
+ }
signInAccount(provider, accountId, username, pw);
}
}
mConn.registerConnectionListener(mListener);
mConn.login(accountId, username, pw, true);
}
+
} catch (RemoteException e) {
mHandler.showServiceErrorAlert();
finish();
}
}
+ private void activateAccount(long providerId, long accountId) {
+ // Update the active value. We restrict to only one active
+ // account per provider right now, so update all accounts of
+ // this provider to inactive first and then update this
+ // account to active.
+ ContentValues values = new ContentValues(1);
+ values.put(Im.Account.ACTIVE, 0);
+ ContentResolver cr = getContentResolver();
+ cr.update(Im.Account.CONTENT_URI, values,
+ Im.Account.PROVIDER + "=" + providerId, null);
+
+ values.put(Im.Account.ACTIVE, 1);
+ cr.update(ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId),
+ values, null, null);
+ }
+
@Override
protected void onDestroy() {
if (mApp != null) {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0, ID_CANCEL_SIGNIN, 0, R.string.menu_cancel_signin);
+ menu.add(0, ID_CANCEL_SIGNIN, 0, R.string.menu_cancel_signin)
+ .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
return true;
}
import android.os.Handler;
import android.os.Bundle;
import android.os.RemoteException;
+import android.content.ContentValues;
import android.content.Intent;
import android.content.ContentResolver;
import android.net.Uri;
public class SignoutActivity extends Activity {
private String[] ACCOUNT_SELECTION = new String[] {
+ Im.Account._ID,
Im.Account.PROVIDER,
};
null /* selection args */,
null /* sort order */);
final long providerId;
+ final long accountId;
try {
if (!c.moveToFirst()) {
}
providerId = c.getLong(c.getColumnIndexOrThrow(Im.Account.PROVIDER));
+ accountId = c.getLong(c.getColumnIndexOrThrow(Im.Account._ID));
} finally {
c.close();
}
mApp = ImApp.getApplication(this);
mApp.callWhenServiceConnected(mHandler, new Runnable() {
public void run() {
- signOut(providerId);
+ signOut(providerId, accountId);
}
});
}
- private void signOut(long providerId) {
+ private void signOut(long providerId, long accountId) {
try {
IImConnection conn = mApp.getConnection(providerId);
if (conn != null) {
conn.logout();
+ } else {
+ // Normally, we can always get the connection when user chose to
+ // sign out. However, if the application crash unexpectedly, the
+ // status will never be updated. Clear the status in this case
+ // to make it recoverable from the crash.
+ ContentValues values = new ContentValues(2);
+ values.put(Im.AccountStatus.PRESENCE_STATUS,
+ Im.Presence.OFFLINE);
+ values.put(Im.AccountStatus.CONNECTION_STATUS,
+ Im.ConnectionStatus.OFFLINE);
+ String where = Im.AccountStatus.ACCOUNT + "=?";
+ getContentResolver().update(Im.AccountStatus.CONTENT_URI,
+ values, where,
+ new String[] { Long.toString(accountId) });
}
} catch (RemoteException ex) {
Log.e(ImApp.LOG_TAG, "signout: caught ", ex);
private int mState;
/**
+ * A pending list of blocking contacts which is used for checking duplicated
+ * block operation.
+ */
+ private Vector<String> mBlockPending;
+ /**
+ * A pending list of deleting contacts which is used for checking duplicated
+ * delete operation.
+ */
+ private Vector<Contact> mDeletePending;
+
+ /**
* Creates a new ContactListManager.
*
* @param conn The underlying protocol connection.
mContactListListeners = new CopyOnWriteArrayList<ContactListListener>();
mBlockedList = new Vector<Contact>();
+ mBlockPending = new Vector<String>(4);
+ mDeletePending = new Vector<Contact>(4);
+
mState = LISTS_NOT_LOADED;
}
return;
}
+ if (mBlockPending.contains(address)) {
+ return;
+ }
doBlockContactAsync(address, true);
}
throws ImException {
checkState();
+ if (mDeletePending.contains(contact)) {
+ return;
+ }
+
doRemoveContactFromListAsync(contact, list);
}
*/
protected void notifyContactError(int type, ImErrorInfo error,
String listName, Contact contact) {
+ if (type == ContactListListener.ERROR_REMOVING_CONTACT) {
+ mDeletePending.remove(contact);
+ } else if (type == ContactListListener.ERROR_BLOCKING_CONTACT) {
+ mBlockPending.remove(contact.getAddress().getFullName());
+ }
for (ContactListListener listener : mContactListListeners) {
listener.onContactError(type, error, listName, contact);
}
list.insertToCache(contact);
} else if (type == ContactListListener.LIST_CONTACT_REMOVED) {
list.removeFromCache(contact);
+ mDeletePending.remove(contact);
}
}
synchronized (this) {
if (blocked) {
mBlockedList.add(contact);
+ String addr = contact.getAddress().getFullName();
+ mBlockPending.remove(addr);
} else {
mBlockedList.remove(contact);
}
protected CopyOnWriteArrayList<ConnectionListener> mConnectionListeners;
protected Presence mUserPresence;
- protected HeartbeatService mHeartbeatService;
protected ImConnection() {
mConnectionListeners = new CopyOnWriteArrayList<ConnectionListener>();
public void networkTypeChanged(){
}
- public void setHeartBeatService(HeartbeatService service) {
- mHeartbeatService = service;
- }
-
- public HeartbeatService getHeartBeatService() {
- return mHeartbeatService;
- }
-
/**
* Tells the current state of the connection.
*/
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.engine;
+
+/**
+ * An abstract interface to access system SMS service.
+ */
+public interface SmsService {
+ /**
+ * The listener which will be notified when an incoming SMS is received.
+ *
+ */
+ public interface SmsListener {
+ /**
+ * Called on new SMS received.
+ *
+ * @param data
+ */
+ public void onIncomingSms(byte[] data);
+ }
+
+ /**
+ * Callback on send SMS failure.
+ *
+ */
+ public interface SmsSendFailureCallback {
+ /** Generic failure case.*/
+ int ERROR_GENERIC_FAILURE = 1;
+ /** Failed because radio was explicitly turned off.*/
+ int ERROR_RADIO_OFF = 2;
+
+ /**
+ * Called when send an SMS failed.
+ *
+ * @param errorCode the error code; will be one of
+ * {@link #ERROR_GENERIC_FAILURE},
+ * {@link #ERROR_RADIO_OFF}
+ */
+ public void onFailure(int errorCode);
+ }
+
+ /**
+ * The max number of bytes an SMS can take.
+ *
+ * @return the max number of bytes an SMS can take.
+ */
+ public int getMaxSmsLength();
+
+ /**
+ * Sends a data SMS to the destination.
+ *
+ * @param dest
+ * The address to send the message to.
+ * @param port
+ * The port to deliver the message to.
+ * @param data
+ * The body of the message to send.
+ */
+ public void sendSms(String dest, int port, byte[] data);
+
+ /**
+ * Sends a data SMS to the destination.
+ *
+ * @param dest
+ * The address to send the message to.
+ * @param port
+ * The port to deliver the message to.
+ * @param data
+ * The body of the message to send.
+ * @param callback
+ * If not null, it will be notified if the message could not be
+ * sent.
+ */
+ public void sendSms(String dest, int port, byte[] data,
+ SmsSendFailureCallback callback);
+
+ /**
+ * Add a SmsListener so that it can be notified when new SMS from specific
+ * address and application port has been received.
+ *
+ * @param from
+ * The address of the sender.
+ * @param port
+ * The application port.
+ * @param listener
+ * The listener which will be notified when SMS received.
+ */
+ public void addSmsListener(String from, int port, SmsListener listener);
+
+ /**
+ * Remove a SmsListener from the service so that it won't be notified
+ * anymore.
+ *
+ * @param listener
+ * The listener to be removed.
+ */
+ public void removeSmsListener(SmsListener listener);
+}
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.engine;
+
+import com.android.im.service.AndroidSystemService;
+
+/**
+ * The interface to access system service objects.
+ *
+ */
+public abstract class SystemService {
+ /**
+ * Gets the default instance of the system service.
+ *
+ * @return the default instance of the system service.
+ */
+ public static SystemService getDefault() {
+ return AndroidSystemService.getInstance();
+ }
+
+ /**
+ * Gets the system HeartbeatService.
+ *
+ * @return the instance of the HeartbeatService.
+ */
+ public abstract HeartbeatService getHeartbeatService();
+
+ /**
+ * Gets the system SmsService.
+ *
+ * @return the instance of the SmsService.
+ */
+ public abstract SmsService getSmsService();
+}
protected DataChannel(ImpsConnection connection) throws ImException {
mConnection = connection;
- mParser = mConnection.getConfig().createPrimitiveParser();
- mSerializer = mConnection.getConfig().createPrimitiveSerializer();
}
/**
import com.android.im.engine.HeartbeatService;
import com.android.im.engine.ImErrorInfo;
import com.android.im.engine.ImException;
+import com.android.im.engine.SystemService;
import com.android.im.imps.Primitive.TransactionMode;
/**
mContentTypeHeader = new BasicHeader("Content-Type", cfg.getTransportContentType());
String msisdn = cfg.getMsisdn();
mMsisdnHeader = (msisdn != null) ? new BasicHeader("MSISDN", msisdn) : null;
+
+ mParser = cfg.createPrimitiveParser();
+ mSerializer = cfg.createPrimitiveSerializer();
}
@Override
Primitive polling = new Primitive(ImpsTags.Polling_Request);
polling.setSession(mConnection.getSession().getID());
sendPrimitive(polling);
+ startHeartbeat();
return true;
}
@Override
public void shutdown() {
- HeartbeatService heartbeatService = mConnection.getHeartBeatService();
+ HeartbeatService heartbeatService
+ = SystemService.getDefault().getHeartbeatService();
if (heartbeatService != null) {
heartbeatService.stopHeartbeat(this);
}
ImpsLog.log("Negative keep alive time. Won't send keep-alive");
}
mKeepAlivePrimitive = new Primitive(ImpsTags.KeepAlive_Request);
- HeartbeatService heartbeatService = mConnection.getHeartBeatService();
+ startHeartbeat();
+ }
+
+ private void startHeartbeat() {
+ HeartbeatService heartbeatService
+ = SystemService.getDefault().getHeartbeatService();
if (heartbeatService != null) {
heartbeatService.startHeartbeat(this, mKeepAliveMillis);
}
}
public long sendHeartbeat() {
+ if (mSuspended) {
+ return 0;
+ }
+
long inactiveTime = SystemClock.elapsedRealtime() - mLastActive;
if (needSendKeepAlive(inactiveTime)) {
sendKeepAlive();
* @param msg the incoming message.
*/
void processMessage(Message msg) {
- ImpsAddress selfAddress = mConnection.getSession().getLoginUserAddress();
- // If the message is not sent to the currently logged user, it must be
- // sent to a group.
- ImpsAddress address = (msg.getTo().equals(selfAddress) ?
- (ImpsAddress)msg.getFrom() : (ImpsAddress)msg.getTo());
+ ImpsAddress from = (ImpsAddress) msg.getFrom();
+ ImpsAddress to = (ImpsAddress) msg.getTo();
+
+ ImpsAddress address = (to instanceof ImpsGroupAddress) ? to : from;
synchronized (this) {
ChatSession ses = findSession(address);
public static CirMethod[] getSupportedCirMethods() {
return new CirMethod[] {
CirMethod.STCP,
+ CirMethod.SSMS,
CirMethod.SHTTP,
};
}
import com.android.im.engine.Presence;
import com.android.im.imps.ImpsConnectionConfig.CirMethod;
import com.android.im.imps.ImpsConnectionConfig.TransportType;
+import com.android.im.imps.Primitive.TransactionMode;
/**
* An implementation of ImConnection of Wireless Village IMPS protocol.
private void doLogin() {
try {
- initDataChannel();
+ if (mConfig.useSmsAuth()) {
+ mDataChannel = new SmsDataChannel(this);
+ } else {
+ mDataChannel = createDataChannel();
+ }
mDataChannel.connect();
} catch (ImException e) {
ImErrorInfo error = e.getImError();
}
private void onAuthenticated() {
+ // The user has chosen logout before the session established, just
+ // send the Logout-Request in this case.
+ if (mState == LOGGING_OUT) {
+ sendLogoutRequest();
+ return;
+ }
+
+ if (mConfig.useSmsAuth()
+ && mConfig.getDataChannelBinding() != TransportType.SMS) {
+ // SMS data channel was used if it's set to send authentication
+ // over SMS. Switch to the config data channel after authentication
+ // completed.
+ try {
+ DataChannel dataChannel = createDataChannel();
+ dataChannel.connect();
+
+ mDataChannel.shutdown();
+ mDataChannel = dataChannel;
+ mDispatcherThread.changeDataChannel(dataChannel);
+ } catch (ImException e) {
+ // This should not happen since only http data channel which
+ // does not do the real network connection in connect() is
+ // valid here now.
+ logoutAsync();
+ return;
+ }
+ }
+
if(mSession.isCapablityRequestRequired()) {
mSession.negotiateCapabilityAsync(new AsyncCompletion(){
public void onComplete() {
mCirChannel = null;
}
- LogoutCompletion logoutCompletion = new LogoutCompletion();
- AsyncTransaction tx = new SimpleAsyncTransaction(mTransactionManager,
- logoutCompletion);
- Primitive logoutPrimitive = new Primitive(ImpsTags.Logout_Request);
- tx.sendRequest(logoutPrimitive);
+ // Only send the Logout-Request if the session has been established.
+ if (mSession.getID() != null) {
+ sendLogoutRequest();
+ }
}
- // We cannot shut down our connections in ImpsAsyncTransaction.onResponse()
- // because at that time the logout transaction itself hasn't ended yet. So
- // we have to do this in this completion object.
- class LogoutCompletion implements AsyncCompletion {
- public void onComplete() {
- shutdown();
- }
+ void sendLogoutRequest() {
+ // We cannot shut down our connections in ImpsAsyncTransaction.onResponse()
+ // because at that time the logout transaction itself hasn't ended yet. So
+ // we have to do this in this completion object.
+ AsyncCompletion completion = new AsyncCompletion() {
+ public void onComplete() {
+ shutdown();
+ }
- public void onError(ImErrorInfo error) {
- // We simply ignore all errors when logging out.
- // NowIMP responds a <Disconnect> instead of <Status> on logout request.
- shutdown();
- }
+ public void onError(ImErrorInfo error) {
+ // We simply ignore all errors when logging out.
+ // NowIMP responds a <Disconnect> instead of <Status> on logout request.
+ shutdown();
+ }
+ };
+ AsyncTransaction tx = new SimpleAsyncTransaction(mTransactionManager,
+ completion);
+ Primitive logoutPrimitive = new Primitive(ImpsTags.Logout_Request);
+ tx.sendRequest(logoutPrimitive);
}
public ImpsSession getSession() {
mDataChannel.sendPrimitive(pollingRequest);
}
- private void initDataChannel() throws ImException {
+ private DataChannel createDataChannel() throws ImException {
TransportType dataChannelBinding = mConfig.getDataChannelBinding();
if (dataChannelBinding == TransportType.HTTP) {
- mDataChannel = new HttpDataChannel(this);
+ return new HttpDataChannel(this);
+ } else if (dataChannelBinding == TransportType.SMS) {
+ return new SmsDataChannel(this);
} else {
throw new ImException("Unsupported data channel binding");
}
mCirChannel = new HttpCirChannel(this, mDataChannel);
} else if (cirMethod == CirMethod.STCP) {
mCirChannel = new TcpCirChannel(this);
+ } else if (cirMethod == CirMethod.SSMS) {
+ mCirChannel = new SmsCirChannel(this);
} else if (cirMethod == CirMethod.NONE) {
//Do nothing
} else {
mChannel = channel;
}
+ public void changeDataChannel(DataChannel channel) {
+ mChannel = channel;
+ interrupt();
+ }
+
@Override
public void run() {
Primitive primitive = null;
}
}
+ if (primitive.getTransactionMode() == TransactionMode.Response) {
+ ImpsErrorInfo error = ImpsUtils.checkResultError(primitive);
+ if (error != null) {
+ int code = error.getCode();
+ if (code == ImpsErrorInfo.SESSION_EXPIRED
+ || code == ImpsErrorInfo.FORCED_LOGOUT
+ || code == ImpsErrorInfo.INVALID_SESSION) {
+ shutdownOnError(error);
+ return;
+ }
+ }
+ }
+
// According to the IMPS spec, only VersionDiscoveryResponse which
// are not supported now doesn't have a transaction ID.
if (primitive.getTransactionID() != null) {
private static final int DEFAULT_KEEPALIVE_SECONDS = 2 * 60 * 60; // 2 hour
private static final int DEFAULT_MIN_SERVER_POLL = 30; // seconds
+ private static final int DEFAULT_SMS_PORT = 3590;
+ private static final int DEFAULT_SMS_CIR_PORT = 3716;
+ private static final long DEFAULT_PRESENCE_POLL_INTERVAL = 60 * 1000; // 1 minute
+
// DeliveryReport is good for acknowledgment but consumes 2 extra
// transactions + 1 possible CIR notification per message. Should not
// be enabled on limited/slow connections.
private String mPluginPath;
+ private Map<String, String> mOthers;
+
public ImpsConnectionConfig() {
setupVersionStrings();
}
mMsisdn = map.get(ImpsConfigNames.MSISDN);
}
if (map.get(ImpsConfigNames.SECURE_LOGIN) != null) {
- mSecureLogin = "true".equalsIgnoreCase(map.get(ImpsConfigNames.SECURE_LOGIN));
+ mSecureLogin = isTrue(map.get(ImpsConfigNames.SECURE_LOGIN));
}
if (map.get(ImpsConfigNames.BASIC_PA_ONLY) != null) {
- mBasicPresenceOnly = "true".equalsIgnoreCase(
- map.get(ImpsConfigNames.BASIC_PA_ONLY));
+ mBasicPresenceOnly = isTrue(map.get(ImpsConfigNames.BASIC_PA_ONLY));
+ }
+ if (map.containsKey(ImpsConfigNames.VERSION)) {
+ mImpsVersion = ImpsVersion.fromString(
+ map.get(ImpsConfigNames.VERSION));
}
setupVersionStrings();
mPluginPath = map.get(ImpsConfigNames.PLUGIN_PATH);
mCustomPasswordDigest = map.get(ImpsConfigNames.CUSTOM_PASSWORD_DIGEST);
mCustomPresenceMapping = map.get(ImpsConfigNames.CUSTOM_PRESENCE_MAPPING);
+
+ mOthers = map;
}
@Override
}
private void setupVersionStrings() {
- if (mImpsVersion == ImpsVersion.IMPS_VERSION_12) {
+ if (mImpsVersion == ImpsVersion.IMPS_VERSION_11) {
+ if (mDataEncoding == EncodingType.XML) {
+ mContentType = "application/vnd.wv.csp.xml";
+ } else if (mDataEncoding == EncodingType.WBXML) {
+ mContentType = "application/vnd.wv.csp.wbxml";
+ } else if (mDataEncoding == EncodingType.SMS) {
+ mContentType = "application/vnd.wv.csp.sms";
+ }
+ mVersionNs = ImpsConstants.VERSION_11_NS;
+ mTransactionNs = ImpsConstants.TRANSACTION_11_NS;
+ mPresenceNs = ImpsConstants.PRESENCE_11_NS;
+ } else if (mImpsVersion == ImpsVersion.IMPS_VERSION_12) {
if (mDataEncoding == EncodingType.XML) {
mContentType = "application/vnd.wv.csp.xml";
} else if (mDataEncoding == EncodingType.WBXML) {
return mSecureLogin;
}
+ public boolean useSmsAuth() {
+ return isTrue(mOthers.get(ImpsConfigNames.SMS_AUTH));
+ }
+
public boolean needDeliveryReport() {
return NEED_DELIVERY_REPORT;
}
return mUdpPort;
}
+ public String getSmsAddr() {
+ return mOthers.get(ImpsConfigNames.SMS_ADDR);
+ }
+
+ public int getSmsPort() {
+ String value = mOthers.get(ImpsConfigNames.SMS_PORT);
+ if (value == null) {
+ return DEFAULT_SMS_PORT;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return DEFAULT_SMS_PORT;
+ }
+ }
+
+ public String getSmsCirAddr() {
+ return mOthers.get(ImpsConfigNames.SMS_CIR_ADDR);
+ }
+
+ public int getSmsCirPort() {
+ String value = mOthers.get(ImpsConfigNames.SMS_CIR_PORT);
+ if (value == null) {
+ return DEFAULT_SMS_CIR_PORT;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return DEFAULT_SMS_CIR_PORT;
+ }
+ }
+
+ public boolean usePrensencePolling() {
+ String value = mOthers.get(ImpsConfigNames.POLL_PRESENCE);
+ return isTrue(value);
+ }
+
+ public long getPresencePollInterval() {
+ String value = mOthers.get(ImpsConfigNames.PRESENCE_POLLING_INTERVAL);
+ if (value == null) {
+ return DEFAULT_PRESENCE_POLL_INTERVAL;
+ }
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return DEFAULT_PRESENCE_POLL_INTERVAL;
+ }
+ }
+
public int getDefaultServerPollMin() {
return DEFAULT_MIN_SERVER_POLL;
}
return mDefaultDomain;
}
+ private boolean isTrue(String value) {
+ return "true".equalsIgnoreCase(value);
+ }
+
/**
* Represents the type of protocol binding for data channel.
*/
public class ImpsConstants {
public static enum ImpsVersion {
+ IMPS_VERSION_11,
IMPS_VERSION_12,
- IMPS_VERSION_13,
+ IMPS_VERSION_13;
+
+ public static ImpsVersion fromString(String value) {
+ if ("1.1".equals(value)) {
+ return IMPS_VERSION_11;
+ } else if ("1.2".equals(value)) {
+ return IMPS_VERSION_12;
+ } else if ("1.3".equals(value)) {
+ return IMPS_VERSION_13;
+ } else {
+ // Unknown version, use 1.2 as default
+ return IMPS_VERSION_12;
+ }
+ }
}
// TODO: move these to some place else?
public static final String CLIENT_PRODUCER = "MOKIA";
public static final String CLIENT_VERSION = "0.1";
- public static final String VERSION_11_NS = "http://www.wireless-village.org/CSP1.1";
- public static final String TRANSACTION_11_NS = "http://www.wireless-village.org/TRC1.1";
- public static final String PRESENCE_11_NS = "http://www.wireless-village.org/PA1.1";
+ public static final String VERSION_11_NS
+ = "http://www.wireless-village.org/CSP1.1";
+ public static final String TRANSACTION_11_NS
+ = "http://www.wireless-village.org/TRC1.1";
+ public static final String PRESENCE_11_NS
+ = "http://www.wireless-village.org/PA1.1";
- public static final String VERSION_12_NS = "http://www.openmobilealliance.org/DTD/WV-CSP1.2";
- public static final String TRANSACTION_12_NS = "http://www.openmobilealliance.org/DTD/WV-TRC1.2";
- public static final String PRESENCE_12_NS = "http://www.openmobilealliance.org/DTD/WV-PA1.2";
+ public static final String VERSION_12_NS
+ = "http://www.openmobilealliance.org/DTD/WV-CSP1.2";
+ public static final String TRANSACTION_12_NS
+ = "http://www.openmobilealliance.org/DTD/WV-TRC1.2";
+ public static final String PRESENCE_12_NS
+ = "http://www.openmobilealliance.org/DTD/WV-PA1.2";
- public static final String VERSION_13_NS = "http://www.openmobilealliance.org/DTD/IMPS-CSP1.3";
- public static final String TRANSACTION_13_NS = "http://www.openmobilealliance.org/DTD/IMPS-TRC1.3";
- public static final String PRESENCE_13_NS = "http://www.openmobilealliance.org/DTD/IMPS-PA1.3";
+ public static final String VERSION_13_NS
+ = "http://www.openmobilealliance.org/DTD/IMPS-CSP1.3";
+ public static final String TRANSACTION_13_NS
+ = "http://www.openmobilealliance.org/DTD/IMPS-TRC1.3";
+ public static final String PRESENCE_13_NS
+ = "http://www.openmobilealliance.org/DTD/IMPS-PA1.3";
public static final String ADDRESS_PREFIX = "wv:";
private ImpsConnection mConnection;
private String mDefaultDomain;
ImpsTransactionManager mTransactionManager;
+ ImpsConnectionConfig mConfig;
boolean mAllowAutoSubscribe = true;
*/
ImpsContactListManager(ImpsConnection connection) {
mConnection = connection;
- mDefaultDomain = connection.getConfig().getDefaultDomain();
+ mConfig = connection.getConfig();
+ mDefaultDomain = mConfig.getDefaultDomain();
mTransactionManager = connection.getTransactionManager();
- mTransactionManager.setTransactionListener(ImpsTags.PresenceNotification_Request, this);
- mTransactionManager.setTransactionListener(ImpsTags.PresenceAuth_Request, this);
+ mTransactionManager.setTransactionListener(
+ ImpsTags.PresenceNotification_Request, this);
+ mTransactionManager.setTransactionListener(
+ ImpsTags.PresenceAuth_Request, this);
}
@Override
return addresses;
}
+ public void fetchPresence(ImpsAddress[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return;
+ }
+
+ Primitive request = new Primitive(ImpsTags.GetPresence_Request);
+ for (ImpsAddress addr : addresses) {
+ request.addElement(addr.toPrimitiveElement());
+ }
+ AsyncTransaction tx = new AsyncTransaction(mTransactionManager){
+ @Override
+ public void onResponseError(ImpsErrorInfo error) {
+ ImpsLog.logError("Failed to get presence:" + error.toString());
+ }
+
+ @Override
+ public void onResponseOk(Primitive response) {
+ extractAndNotifyPresence(response.getContentElement());
+ }
+ };
+ tx.sendRequest(request);
+ }
+
+ public ImpsAddress[] getAllListAddress() {
+ int count = mContactLists.size();
+ ImpsAddress[] res = new ImpsContactListAddress[count];
+
+ int index = 0;
+ for (ContactList l : mContactLists) {
+ res[index++] = (ImpsContactListAddress) l.getAddress();
+ }
+
+ return res;
+ }
+
// void createDefaultAttributeListAsync() {
// Primitive request = new Primitive(ImpsTags.CreateAttributeList_Request);
//
AsyncTransaction tx = new AsyncTransaction(mTransactionManager) {
@Override
public void onResponseError(ImpsErrorInfo error) {
- if (error.getCode() == ImpsConstants.STATUS_AUTO_SUBSCRIPTION_NOT_SUPPORTED) {
+ if (error.getCode()
+ == ImpsConstants.STATUS_AUTO_SUBSCRIPTION_NOT_SUPPORTED) {
mAllowAutoSubscribe = false;
ArrayList<Contact> contacts = new ArrayList<Contact>();
for (ContactList list : contactLists) {
subscribeToContactsAsync(contacts, completion);
} else {
- completion.onError(error);
+ if (completion != null) {
+ completion.onError(error);
+ }
}
}
@Override
public void onResponseOk(Primitive response) {
- completion.onComplete();
+ if (completion != null) {
+ completion.onComplete();
+ }
}
};
@Override
public void onResponseOk(Primitive response) {
- subscribeToListAsync(list, new AsyncCompletion () {
+ notifyContactListCreated(list);
- public void onComplete() {
- notifyContactListCreated(list);
- }
-
- public void onError(ImErrorInfo error) {
- ImpsLog.log("Error subscribing to newly created list: "
- + error.getDescription() + "; ignored");
- onComplete();
- }
- });
+ if (mConfig.usePrensencePolling()) {
+ getPresencePollingManager().resetPollingContacts();
+ } else {
+ subscribeToListAsync(list, null);
+ }
}
};
@Override
public void onResponseOk(Primitive response) {
notifyContactListDeleted(list);
-
- if (!mAllowAutoSubscribe) {
+ if (mConfig.usePrensencePolling()) {
+ getPresencePollingManager().resetPollingContacts();
+ } else if (!mAllowAutoSubscribe) {
unsubscribeToListAsync(list, new AsyncCompletion(){
public void onComplete() {}
// attributes are desired but the OZ server doens't quite follow the
// spec here. It won't send any PresenceNotification either when we
// don't send PresenceSubList or we request more PA than it supports.
- if(mConnection.getConfig().supportBasicPresenceOnly()){
+ if(mConfig.supportBasicPresenceOnly()){
PrimitiveElement presenceList = request.addElement(ImpsTags.PresenceSubList);
- presenceList.setAttribute(ImpsTags.XMLNS, mConnection.getConfig().getPresenceNs());
+ presenceList.setAttribute(ImpsTags.XMLNS, mConfig.getPresenceNs());
for(String pa : ImpsClientCapability.getBasicPresenceAttributes()) {
presenceList.addChild(pa);
}
if (ImpsTags.PresenceNotification_Request.equals(type)) {
tx.sendStatusResponse(ImpsConstants.SUCCESS_CODE);
- ArrayList<Contact> updated = new ArrayList<Contact>();
- PresenceMapping presenceMapping = mConnection.getConfig().getPresenceMapping();
- for (PrimitiveElement presenceElem : request.getContentElement().getChildren()) {
- String userId = presenceElem.getChildContents(ImpsTags.UserID);
- if (userId == null) {
- continue;
- }
- PrimitiveElement presenceSubList = presenceElem.getChild(ImpsTags.PresenceSubList);
- Presence presence = ImpsPresenceUtils.extractPresence(presenceSubList, presenceMapping);
- // Find out the contact in all lists and update their presence
- for(ContactList list : mContactLists) {
- Contact contact = list.getContact(userId);
- if (contact != null) {
- contact.setPresence(presence);
- updated.add(contact);
- }
- }
- }
- if (updated.size() > 0) {
- notifyContactsPresenceUpdated(updated.toArray(new Contact[updated.size()]));
- }
+ PrimitiveElement content = request.getContentElement();
+ extractAndNotifyPresence(content);
} else if (ImpsTags.PresenceAuth_Request.equals(type)) {
tx.sendStatusResponse(ImpsConstants.SUCCESS_CODE);
}
}
+ private void extractAndNotifyPresence(PrimitiveElement content) {
+ ArrayList<Contact> updated = new ArrayList<Contact>();
+ PresenceMapping presenceMapping = mConfig.getPresenceMapping();
+
+ ArrayList<PrimitiveElement> presenceList = content.getChildren(ImpsTags.Presence);
+ for (PrimitiveElement presenceElem : presenceList) {
+ String userId = presenceElem.getChildContents(ImpsTags.UserID);
+ if (userId == null) {
+ continue;
+ }
+ PrimitiveElement presenceSubList = presenceElem.getChild(ImpsTags.PresenceSubList);
+ Presence presence = ImpsPresenceUtils.extractPresence(presenceSubList, presenceMapping);
+ // Find out the contact in all lists and update their presence
+ for(ContactList list : mContactLists) {
+ Contact contact = list.getContact(userId);
+ if (contact != null) {
+ contact.setPresence(presence);
+ updated.add(contact);
+ }
+ }
+ }
+ if (!updated.isEmpty()) {
+ notifyContactsPresenceUpdated(updated.toArray(new Contact[updated.size()]));
+ }
+ }
+
void loadContactsOfListAsync(final ImpsAddress address, final
AsyncCompletion completion) {
Primitive listManageRequest = new Primitive(ImpsTags.ListManage_Request);
mDefaultContactList = list;
}
- subscribeToListAsync(list, completion);
+ if (mConfig.usePrensencePolling()) {
+ completion.onComplete();
+ } else {
+ subscribeToListAsync(list, completion);
+ }
}
};
private Primitive buildBlockContactReq(String address, boolean block) {
Primitive request = new Primitive(ImpsTags.BlockEntity_Request);
- ImpsVersion version = mConnection.getConfig().getImpsVersion();
+ ImpsVersion version = mConfig.getImpsVersion();
if (version == ImpsVersion.IMPS_VERSION_13) {
request.addElement(ImpsTags.BlockListInUse, true);
}
private final class LoadListCompletion implements AsyncCompletion {
- private int listIndex;
+ private int mListIndex;
LoadListCompletion() {
- listIndex = 0;
+ mListIndex = 0;
}
public void onComplete() {
}
private void processResult(ImErrorInfo error) {
- ImpsAddress addr = mListAddresses.get(listIndex);
+ ImpsAddress addr = mListAddresses.get(mListIndex);
if (error == null) {
notifyContactListLoaded(getContactList(addr));
error, addr.getScreenName(), null);
}
- listIndex++;
- if (listIndex < mListAddresses.size()) {
- loadContactsOfListAsync(mListAddresses.get(listIndex), this);
+ mListIndex++;
+ if (mListIndex < mListAddresses.size()) {
+ loadContactsOfListAsync(mListAddresses.get(mListIndex), this);
} else {
onContactListsLoaded();
}
void onContactListsLoaded() {
notifyContactListsLoaded();
- // notify the pending subscription requests received before contact lists has been loaded.
+ if (mConfig.usePrensencePolling()) {
+ fetchPresence(getAllListAddress());
+ }
+
+ // notify the pending subscription requests received before contact
+ // lists has been loaded.
SubscriptionRequestListener listener = getSubscriptionRequestListener();
if (mSubscriptionRequests != null && listener != null) {
for (Contact c : mSubscriptionRequests) {
notifyContactListUpdated(list,
ContactListListener.LIST_CONTACT_ADDED, contact);
- AsyncCompletion subscribeCompletion = new AsyncCompletion(){
- public void onComplete() {}
+ if (mConfig.usePrensencePolling()) {
+ fetchPresence(new ImpsAddress[]{
+ (ImpsAddress) contact.getAddress()});
+ } else {
+ AsyncCompletion subscribeCompletion = new AsyncCompletion(){
+ public void onComplete() {}
- public void onError(ImErrorInfo error) {
- notifyContactError(
- ContactListListener.ERROR_RETRIEVING_PRESENCE,
- error, list.getName(), contact);
+ public void onError(ImErrorInfo error) {
+ notifyContactError(
+ ContactListListener.ERROR_RETRIEVING_PRESENCE,
+ error, list.getName(), contact);
+ }
+ };
+
+ if (mAllowAutoSubscribe) {
+ // XXX Send subscription again after add contact to make sure we
+ // can get the presence notification. Although the we set
+ // AutoSubscribe True when subscribe presence after load contacts,
+ // the server might not send presence notification.
+ subscribeToListAsync(list, subscribeCompletion);
+ } else {
+ subscribeToContactsAsync(contacts, subscribeCompletion);
}
- };
-
- if (mAllowAutoSubscribe) {
- // XXX Send subscription again after add contact to make sure we
- // can get the presence notification. Although the we set
- // AutoSubscribe True when subscribe presence after load contacts,
- // the server might not send presence notification.
- subscribeToListAsync(list, subscribeCompletion);
- } else {
- subscribeToContactsAsync(contacts, subscribeCompletion);
}
}
public void onError(ImErrorInfo error) {
+ // XXX Workaround to convert 402 error to 531. Some
+ // servers might return 402 - Bad parameter instead of
+ // 531 - Unknown user if the user input an invalid user ID.
+ if (error.getCode() == ImpsErrorInfo.BAD_PARAMETER) {
+ error = new ImErrorInfo(ImpsErrorInfo.UNKNOWN_USER,
+ error.getDescription());
+ }
notifyContactError(ContactListListener.ERROR_ADDING_CONTACT,
error, list.getName(), contact);
}
protected ImConnection getConnection() {
return mConnection;
}
+
+ private PresencePollingManager mPollingMgr;
+ /*package*/PresencePollingManager getPresencePollingManager() {
+ if (mPollingMgr == null) {
+ mPollingMgr = new PresencePollingManager(this,
+ mConfig.getPresencePollInterval());
+ }
+ return mPollingMgr;
+ }
+
}
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.imps;
+
+/**
+ * Manage presence polling from the server. If the server does not support
+ * subscribing presence change or prefer the client polling presence, the client
+ * should send GetPresence-Request periodically.
+ */
+public class PresencePollingManager implements Runnable {
+ private boolean mStopped;
+ private boolean mFinished;
+
+ private long mPollingInterval;
+ private Object mLock = new Object();
+
+ private ImpsAddress[] mPollingAddress;
+ private ImpsAddress[] mContactLists;
+
+ private ImpsContactListManager mManager;
+ private Thread mPollingThread;
+
+ public PresencePollingManager(ImpsContactListManager manager,
+ long pollingIntervalMillis) {
+ mManager = manager;
+ mPollingInterval = pollingIntervalMillis;
+ mStopped = true;
+ mFinished = false;
+ }
+
+ public void resetPollingContacts() {
+ synchronized (mLock) {
+ mContactLists = null;
+ }
+ }
+
+ public void startPolling() {
+ synchronized (mLock) {
+ // Clear the polling address; the polling thread will fetch the
+ // presence of all the contacts in lists.
+ mPollingAddress = null;
+ }
+ doStartPolling();
+ }
+
+ public void startPolling(ImpsUserAddress user){
+ synchronized (mLock) {
+ mPollingAddress = new ImpsAddress[] { user };
+ }
+ doStartPolling();
+ }
+
+ public void stopPolling() {
+ mStopped = true;
+ }
+
+ public void shutdownPolling() {
+ mFinished = true;
+ synchronized (mLock) {
+ mLock.notify();
+ }
+ }
+
+ public void run() {
+ while (!mFinished) {
+ synchronized (mLock) {
+ if (!mStopped) {
+ ImpsAddress[] pollingAddress = mPollingAddress;
+ if (pollingAddress == null) {
+ // Didn't specify of which contacts the presence will
+ // poll. Fetch the presence of all contacts in list.
+ pollingAddress = getContactLists();
+ }
+ mManager.fetchPresence(pollingAddress);
+ }
+
+ try {
+ mLock.wait(mPollingInterval);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ private void doStartPolling() {
+ mStopped = false;
+ if (mPollingThread == null) {
+ mPollingThread = new Thread(this, "PollingThread");
+ mPollingThread.setDaemon(true);
+ mPollingThread.start();
+ } else {
+ synchronized (mLock) {
+ mLock.notify();
+ }
+ }
+ }
+
+ private ImpsAddress[] getContactLists() {
+ if (mContactLists == null) {
+ mContactLists = mManager.getAllListAddress();
+ }
+ return mContactLists;
+ }
+
+}
sTransactionToCode.put(ImpsTags.UpdatePresence_Request, "UP");
sElementToCode.put(ImpsTags.ClientID, ClientID);
+ sElementToCode.put(ImpsTags.DigestSchema, "SH");
+ sElementToCode.put(ImpsTags.DigestBytes, "DB");
sElementToCode.put(ImpsTags.Password, "PW");
sElementToCode.put(ImpsTags.SessionCookie, "SC");
sElementToCode.put(ImpsTags.TimeToLive, "TL");
public Primitive parse(InputStream in) throws ParserException, IOException {
// assuming PTS data is always short
- BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(in, "UTF-8"), 128);
mStringBuf.setLength(0);
mPos = 0;
int len;
private static final Pattern sCharsToBeQuoted = Pattern.compile("[ \",\\(\\)=&]");
public PtsPrimitiveSerializer(ImpsVersion impsVersion) throws SerializerException {
- if (impsVersion == ImpsVersion.IMPS_VERSION_12) {
+ if (impsVersion == ImpsVersion.IMPS_VERSION_11) {
+ mPreampleHead = "WV11";
+ }else if (impsVersion == ImpsVersion.IMPS_VERSION_12) {
mPreampleHead = "WV12";
} else if (impsVersion == ImpsVersion.IMPS_VERSION_13) {
mPreampleHead = "WV13";
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.imps;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.android.im.engine.SmsService.SmsListener;
+
+public class SmsAssembler implements SmsListener {
+ // WVaaBBcccDD
+ // aa - version number; 12 for 1.2, 13 for 1.3; "XX" for version discovery
+ // BB - message type, case insensitive
+ // ccc - transaction id in range 0-999 without preceding zero
+ // DD - multiple SMSes identifier
+ private static final Pattern sPreamplePattern =
+ Pattern.compile("\\AWV(\\d{2})(\\p{Alpha}{2})(\\d{1,3})(\\p{Alpha}{2})?");
+
+ private SmsListener mListener;
+ private HashMap<String, RawPtsData> mPtsCache;
+
+ public SmsAssembler() {
+ mPtsCache = new HashMap<String, RawPtsData>();
+ }
+
+ public void setSmsListener(SmsListener listener) {
+ mListener = listener;
+ }
+
+ public void onIncomingSms(byte[] data) {
+ String preamble = extractPreamble(data);
+ if (preamble == null) {
+ ImpsLog.logError("Received non PTS SMS");
+ return;
+ }
+
+ Matcher m = sPreamplePattern.matcher(preamble);
+ if (!m.matches()) {
+ ImpsLog.logError("Received non PTS SMS");
+ return;
+ }
+ String dd = m.group(4);
+ if (dd == null || dd.length() == 0) {
+ notifyAssembledSms(data);
+ } else {
+ int totalSegmentsCount = dd.charAt(1) - 'a' + 1;
+ int index = dd.charAt(0) - 'a';
+ if (index < 0 || index >= totalSegmentsCount) {
+ ImpsLog.logError("Invalid multiple SMSes identifier");
+ return;
+ }
+
+ String transId = m.group(3);
+ RawPtsData pts = mPtsCache.get(transId);
+ if (pts == null) {
+ pts = new RawPtsData(preamble.length(), totalSegmentsCount);
+ mPtsCache.put(transId, pts);
+ }
+
+ pts.setSegment(index, data);
+ if (pts.isAllSegmentsReceived()) {
+ mPtsCache.remove(transId);
+ notifyAssembledSms(pts.assemble());
+ }
+ }
+ }
+
+ private String extractPreamble(byte[] data) {
+ int N = data.length;
+ int preambleIndex = 0;
+ while (data[preambleIndex] != ' ' && preambleIndex < N) {
+ preambleIndex++;
+ }
+
+ if (preambleIndex >= N) {
+ return null;
+ }
+
+ try {
+ return new String(data, 0, preambleIndex, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // impossible
+ return null;
+ }
+ }
+
+ private void notifyAssembledSms(byte[] data) {
+ if (mListener != null) {
+ mListener.onIncomingSms(data);
+ }
+ }
+
+ private static class RawPtsData {
+ private int mOrigPreambeLen;
+ private byte[][] mSegments;
+
+ public RawPtsData(int origPreambleLen, int totalSegments) {
+ mOrigPreambeLen = origPreambleLen;
+ mSegments = new byte[totalSegments][];
+ }
+
+ public void setSegment(int index, byte[] segment) {
+ mSegments[index] = segment;
+ }
+
+ public boolean isAllSegmentsReceived() {
+ for (byte[] segment : mSegments) {
+ if (segment == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public byte[] assemble() {
+ int len = calculateLength();
+ byte[] res = new byte[len];
+ int index = 0;
+ // copy the preamble
+ System.arraycopy(mSegments[0], 0, res, index, mOrigPreambeLen - 2);
+ index += mOrigPreambeLen - 2;
+ res[index++] = ' ';
+
+ for (byte[] segment : mSegments) {
+ int payloadStart = mOrigPreambeLen + 1;
+ int payloadLen = segment.length - payloadStart;
+ System.arraycopy(segment, payloadStart, res, index, payloadLen);
+ index += payloadLen;
+ }
+ return res;
+ }
+
+ private int calculateLength() {
+ // don't have 'dd' in assembled data
+ int preambleLen = mOrigPreambeLen - 2;
+
+ int total = preambleLen + 1;// a space after preamble
+ for (byte[] segment : mSegments) {
+ int segmentPayload = segment.length - (mOrigPreambeLen + 1);
+ total += segmentPayload;
+ }
+ return total;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.imps;
+
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.engine.ImException;
+import com.android.im.engine.SmsService;
+import com.android.im.engine.SystemService;
+import com.android.im.engine.SmsService.SmsListener;
+import com.android.im.engine.SmsService.SmsSendFailureCallback;
+import com.android.internal.telephony.gsm.EncodeException;
+import com.android.internal.telephony.gsm.GsmAlphabet;
+
+public class SmsCirChannel extends CirChannel
+ implements SmsListener, SmsSendFailureCallback {
+ private String mAddr;
+ private int mPort;
+
+ private SmsService mSmsService;
+
+ protected SmsCirChannel(ImpsConnection connection) {
+ super(connection);
+ ImpsConnectionConfig cfg = connection.getConfig();
+ mAddr = cfg.getSmsCirAddr();
+ mPort = cfg.getSmsCirPort();
+ }
+
+ @Override
+ public void connect() throws ImException {
+ if (mAddr == null || mAddr.length() == 0) {
+ throw new ImException(ImpsErrorInfo.UNKNOWN_SERVER,
+ "Invalid sms addr");
+ }
+ mSmsService = SystemService.getDefault().getSmsService();
+ mSmsService.addSmsListener(mAddr, mPort, this);
+ sendHelo();
+ }
+
+ @Override
+ public void shutdown() {
+ mSmsService.removeSmsListener(this);
+ }
+
+ public void onIncomingSms(byte[] data) {
+ // It's safe to assume that each character is encoded into 7-bit since
+ // all characters in CIR are in gsm 7-bit alphabet.
+ int lengthSeptets = data.length * 8 / 7;
+ int numPaddingBits = data.length * 8 % 7;
+ String s = GsmAlphabet.gsm7BitPackedToString(data, 0,
+ lengthSeptets, numPaddingBits);
+ // CIR format: WVCI <version> <session cookie>
+ if (!s.startsWith("WVCI")) {
+ // not a valid CIR, ignore.
+ return;
+ }
+ String[] fields = s.split(" ");
+ if (fields.length != 3) {
+ // Not a valid CIR, ignore
+ return;
+ }
+ String sessionCookie = mConnection.getSession().getCookie();
+ if (sessionCookie.equalsIgnoreCase(fields[2])) {
+ mConnection.sendPollingRequest();
+ }
+ }
+
+ public void onFailure(int errorCode) {
+ mConnection.shutdownOnError(new ImErrorInfo(ImpsErrorInfo.NETWORK_ERROR,
+ "Could not establish SMS CIR channel"));
+ }
+
+ private void sendHelo() {
+ String data = "HELO " + mConnection.getSession().getID();
+ try {
+ byte[] bytes = GsmAlphabet.stringToGsm7BitPacked(data);
+ mSmsService.sendSms(mAddr, mPort, bytes, this);
+ } catch (EncodeException ignore) {
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.imps;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import android.os.SystemClock;
+
+import com.android.im.engine.HeartbeatService;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.engine.ImException;
+import com.android.im.engine.SmsService;
+import com.android.im.engine.SystemService;
+import com.android.im.engine.SmsService.SmsListener;
+import com.android.im.engine.SmsService.SmsSendFailureCallback;
+
+public class SmsDataChannel extends DataChannel
+ implements SmsListener, HeartbeatService.Callback {
+ private SmsService mSmsService;
+ private String mSmsAddr;
+ private short mSmsPort;
+
+ private long mLastActive;
+
+ private SmsSplitter mSplitter;
+ private SmsAssembler mAssembler;
+
+ private LinkedBlockingQueue<Primitive> mReceiveQueue;
+
+ private ImpsTransactionManager mTxManager;
+ private boolean mConnected;
+ private long mKeepAliveMillis;
+ private Primitive mKeepAlivePrimitive;
+
+ private long mReplyTimeout;
+ private LinkedList<PendingTransaction> mPendingTransactions;
+ private Timer mTimer;
+
+ protected SmsDataChannel(ImpsConnection connection) throws ImException {
+ super(connection);
+
+ mTxManager = connection.getTransactionManager();
+
+ ImpsConnectionConfig config = connection.getConfig();
+ mReplyTimeout = config.getReplyTimeout();
+ mSmsAddr = config.getSmsAddr();
+ mSmsPort = (short) config.getSmsPort();
+ mSmsService = SystemService.getDefault().getSmsService();
+
+ mParser = new PtsPrimitiveParser();
+ try {
+ mSerializer = new PtsPrimitiveSerializer(config.getImpsVersion());
+ } catch (SerializerException e) {
+ throw new ImException(e);
+ }
+ mSplitter = new SmsSplitter(mSmsService.getMaxSmsLength());
+ mAssembler = new SmsAssembler();
+ mAssembler.setSmsListener(this);
+ }
+
+ @Override
+ public void connect() throws ImException {
+ mSmsService.addSmsListener(mSmsAddr, mSmsPort, mAssembler);
+ mReceiveQueue = new LinkedBlockingQueue<Primitive>();
+ mPendingTransactions = new LinkedList<PendingTransaction>();
+ mTimer = new Timer(mReplyTimeout);
+ new Thread(mTimer, "SmsDataChannel timer").start();
+ mConnected = true;
+ }
+
+ @Override
+ public long getLastActiveTime() {
+ return mLastActive;
+ }
+
+ @Override
+ public boolean isSendingQueueEmpty() {
+ // Always true since we don't have a sending queue.
+ return true;
+ }
+
+ @Override
+ public Primitive receivePrimitive() throws InterruptedException {
+ return mReceiveQueue.take();
+ }
+
+ @Override
+ public void sendPrimitive(Primitive p) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ mSerializer.serialize(p, out);
+ mSplitter.split(out.toByteArray());
+ SmsService smsService = SystemService.getDefault().getSmsService();
+ SendFailureCallback sendFailureCallback
+ = new SendFailureCallback(p.getTransactionID());
+ while (mSplitter.hasNext()) {
+ smsService.sendSms(mSmsAddr, mSmsPort, mSplitter.getNext(),
+ sendFailureCallback);
+ }
+ mLastActive = SystemClock.elapsedRealtime();
+ addPendingTransaction(p.getTransactionID());
+ } catch (IOException e) {
+ mTxManager.notifyErrorResponse(p.getTransactionID(),
+ ImpsErrorInfo.SERIALIZER_ERROR, e.getLocalizedMessage());
+ } catch (SerializerException e) {
+ mTxManager.notifyErrorResponse(p.getTransactionID(),
+ ImpsErrorInfo.SERIALIZER_ERROR, e.getLocalizedMessage());
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ mSmsService.removeSmsListener(this);
+ mTimer.stop();
+ mConnected = false;
+ }
+
+ @Override
+ public void startKeepAlive(long interval) {
+ if (!mConnected) {
+ throw new IllegalStateException();
+ }
+
+ if (interval <= 0) {
+ interval = mConnection.getConfig().getDefaultKeepAliveInterval();
+ }
+
+ mKeepAliveMillis = interval * 1000;
+ if (mKeepAliveMillis < 0) {
+ ImpsLog.log("Negative keep alive time. Won't send keep-alive");
+ }
+ mKeepAlivePrimitive = new Primitive(ImpsTags.KeepAlive_Request);
+
+ HeartbeatService heartbeatService
+ = SystemService.getDefault().getHeartbeatService();
+ if (heartbeatService != null) {
+ heartbeatService.startHeartbeat(this, mKeepAliveMillis);
+ }
+ }
+
+ public long sendHeartbeat() {
+ if (!mConnected) {
+ return 0;
+ }
+
+ long inactiveTime = SystemClock.elapsedRealtime() - mLastActive;
+ if (needSendKeepAlive(inactiveTime)) {
+ sendKeepAlive();
+ return mKeepAliveMillis;
+ } else {
+ return mKeepAliveMillis - inactiveTime;
+ }
+ }
+
+ private void sendKeepAlive() {
+ ImpsTransactionManager tm = mConnection.getTransactionManager();
+ AsyncTransaction tx = new AsyncTransaction(tm) {
+ @Override
+ public void onResponseError(ImpsErrorInfo error) {
+ }
+
+ @Override
+ public void onResponseOk(Primitive response) {
+ // Since we never request a new timeout value, the response
+ // can be ignored
+ }
+ };
+ tx.sendRequest(mKeepAlivePrimitive);
+ }
+
+ private boolean needSendKeepAlive(long inactiveTime) {
+ return mKeepAliveMillis - inactiveTime <= 500;
+ }
+
+ @Override
+ public boolean resume() {
+ return true;
+ }
+
+ @Override
+ public void suspend() {
+ // do nothing.
+ }
+
+ public void onIncomingSms(byte[] data) {
+ try {
+ Primitive p = mParser.parse(new ByteArrayInputStream(data));
+ mReceiveQueue.put(p);
+ removePendingTransaction(p.getTransactionID());
+ } catch (ParserException e) {
+ handleError(data, ImpsErrorInfo.PARSER_ERROR, e.getLocalizedMessage());
+ } catch (IOException e) {
+ handleError(data, ImpsErrorInfo.PARSER_ERROR, e.getLocalizedMessage());
+ } catch (InterruptedException e) {
+ handleError(data, ImpsErrorInfo.UNKNOWN_ERROR, e.getLocalizedMessage());
+ }
+ }
+
+ private void handleError(byte[] data, int errCode, String info) {
+ String trId = extractTrId(data);
+ if (trId != null) {
+ mTxManager.notifyErrorResponse(trId, errCode, info);
+ removePendingTransaction(trId);
+ }
+ }
+
+ private String extractTrId(byte[] data) {
+ int transIdStart = 4;
+ int index = transIdStart;
+ while(Character.isDigit(data[index])) {
+ index++;
+ }
+ int transIdLen = index - transIdStart;
+ try {
+ return new String(data, transIdStart, transIdLen, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+
+ private void addPendingTransaction(String transId) {
+ synchronized (mPendingTransactions) {
+ mPendingTransactions.add(new PendingTransaction(transId));
+ }
+ }
+
+ private void removePendingTransaction(String transId) {
+ synchronized (mPendingTransactions) {
+ Iterator<PendingTransaction> iter = mPendingTransactions.iterator();
+ while (iter.hasNext()) {
+ PendingTransaction tx = iter.next();
+ if (tx.mTransId.equals(transId)) {
+ iter.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ /*package*/void checkTimeout() {
+ synchronized (mPendingTransactions) {
+ Iterator<PendingTransaction> iter = mPendingTransactions.iterator();
+ while (iter.hasNext()) {
+ PendingTransaction tx = iter.next();
+ if (tx.isExpired(mReplyTimeout)) {
+ notifyTimeout(tx);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ private void notifyTimeout(PendingTransaction tx) {
+ String transId = tx.mTransId;
+ mTxManager.notifyErrorResponse(transId, ImpsErrorInfo.TIMEOUT,
+ "Timeout");
+ removePendingTransaction(transId);
+ }
+
+ private class SendFailureCallback implements SmsSendFailureCallback {
+ private String mTransId;
+
+ public SendFailureCallback(String transId) {
+ mTransId = transId;
+ }
+
+ public void onFailure(int errorCode) {
+ mTxManager.notifyErrorResponse(mTransId, ImErrorInfo.NETWORK_ERROR, null);
+ }
+ }
+
+ private class Timer implements Runnable {
+ private boolean mStopped;
+ private long mInterval;
+
+ public Timer(long interval) {
+ mInterval = interval;
+ mStopped = false;
+ }
+
+ public void stop() {
+ mStopped = true;
+ }
+
+ public void run() {
+ while (!mStopped) {
+ try {
+ Thread.sleep(mInterval);
+ } catch (InterruptedException e) {
+ continue;
+ }
+ checkTimeout();
+ }
+ }
+ }
+
+ private static class PendingTransaction {
+ private String mTransId;
+ private long mSentTime;
+
+ public PendingTransaction(String transId) {
+ mTransId = transId;
+ }
+
+ public boolean isExpired(long timeout) {
+ return SystemClock.elapsedRealtime() - mSentTime >= timeout;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.imps;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A helper class to split the payload into several segments to meet the size
+ * constraint of the sms.
+ *
+ */
+public class SmsSplitter {
+ private static final int MAX_SEGMENT_COUNT = 26;
+
+ private ByteBuffer mOutBuffer;
+ private int mMaxSegmentLen;
+
+ private byte[] mData;
+ private int mPreambleEnd;
+
+ private int mCurrentSegment;
+ private int mSegmentCount;
+
+ public SmsSplitter(int maxLen) {
+ mMaxSegmentLen = maxLen;
+ mOutBuffer = ByteBuffer.allocate(maxLen);
+ }
+
+ /**
+ * Split the data into several segments to meet the size constraint.
+ *
+ * @param data
+ * The data to split. MUST be a valid PTS primitive.
+ * @return The count of segments of the result or -1 if the data is too long.
+ */
+ public int split(byte[] data) {
+ mData = data;
+ mCurrentSegment = 0;
+ calculateSegments();
+ if (mSegmentCount > MAX_SEGMENT_COUNT) {
+ mSegmentCount = -1;
+ }
+ return mSegmentCount;
+ }
+
+ public boolean hasNext() {
+ return mCurrentSegment < mSegmentCount;
+ }
+
+ /**
+ * Gets the next segment.
+ *
+ * @return The next segment.
+ * @throws IndexOutOfBoundsException
+ */
+ public byte[] getNext() {
+ if (mCurrentSegment >= mSegmentCount) {
+ throw new IndexOutOfBoundsException();
+ }
+ byte[] segment;
+ if (mSegmentCount == 1) {
+ segment = mData;
+ } else {
+ mOutBuffer.clear();
+ // The original preamble
+ mOutBuffer.put(mData, 0, mPreambleEnd);
+ // Two character of DD
+ mOutBuffer.put((byte) ('a' + mCurrentSegment));
+ mOutBuffer.put((byte) ('a' + mSegmentCount - 1));
+ // The space after preamble
+ mOutBuffer.put((byte) ' ');
+
+ // The payload
+ int segmentPayload = mMaxSegmentLen - mPreambleEnd - 3;
+ int offset = mPreambleEnd + 1 + segmentPayload * mCurrentSegment;
+ int len = (offset + segmentPayload > mData.length) ?
+ mData.length - offset : segmentPayload;
+ mOutBuffer.put(mData, offset, len);
+
+ mOutBuffer.flip();
+ segment = new byte[mOutBuffer.limit()];
+ mOutBuffer.get(segment);
+ }
+ mCurrentSegment++;
+ return segment;
+ }
+
+ private void calculateSegments() {
+ int totalLen = mData.length;
+ if (totalLen < mMaxSegmentLen) {
+ mSegmentCount = 1;
+ } else {
+ searchPreambleEnd();
+ int newPreambleLen = mPreambleEnd + 2;
+ int segmentPayload = mMaxSegmentLen - newPreambleLen - 1;
+ int totalPayload = totalLen - mPreambleEnd - 1;
+ mSegmentCount = (totalPayload + segmentPayload -1) / segmentPayload;
+ }
+ }
+
+ private void searchPreambleEnd() {
+ byte[] data = mData;
+ int index = 0;
+ while(index < data.length && data[index] != ' ') {
+ index++;
+ }
+ mPreambleEnd = index;
+ }
+}
\ No newline at end of file
import com.android.im.engine.HeartbeatService;
import com.android.im.engine.ImErrorInfo;
import com.android.im.engine.ImException;
+import com.android.im.engine.SystemService;
import android.os.SystemClock;
import android.util.Log;
mCirThread = new Thread(this, "TcpCirChannel");
mCirThread.setDaemon(true);
mCirThread.start();
- HeartbeatService heartbeatService = mConnection.getHeartBeatService();
+ HeartbeatService heartbeatService
+ = SystemService.getDefault().getHeartbeatService();
if (heartbeatService != null) {
heartbeatService.startHeartbeat(this, PING_INTERVAL);
}
} catch (IOException e) {
// ignore
}
- HeartbeatService heartbeatService = mConnection.getHeartBeatService();
+ HeartbeatService heartbeatService
+ = SystemService.getDefault().getHeartbeatService();
if (heartbeatService != null) {
heartbeatService.stopHeartbeat(this);
}
private void reconnectAndWait() {
reconnect();
+ // in case reconnect() has already been called in another thread, wait
+ // for it to finish
while (!mDone) {
synchronized (mReconnectLock) {
if (mReconnecting) {
private OutputStream mOut;
private int mNativeHandle;
+ private static int PUBLIC_ID_IMPS_11 = 0x10;
private static int PUBLIC_ID_IMPS_12 = 0x11;
private static int PUBLIC_ID_IMPS_13 = 0x12;
public WbxmlSerializer(ImpsVersion impsVersion) {
- if (impsVersion == ImpsVersion.IMPS_VERSION_12) {
+ if (impsVersion == ImpsVersion.IMPS_VERSION_11) {
+ mNativeHandle = nativeCreate(PUBLIC_ID_IMPS_11);
+ } else if (impsVersion == ImpsVersion.IMPS_VERSION_12) {
mNativeHandle = nativeCreate(PUBLIC_ID_IMPS_12);
} else if (impsVersion == ImpsVersion.IMPS_VERSION_13) {
mNativeHandle = nativeCreate(PUBLIC_ID_IMPS_13);
}
}
+ public synchronized void stopAll() {
+ for (int i = 0; i < mAlarms.size(); i++) {
+ Alarm alarm = mAlarms.valueAt(i);
+ cancelAlarm(alarm);
+ }
+ }
+
@Override
public void onReceive(Context context, Intent intent) {
mWakeLock.acquire();
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.service;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.provider.Telephony;
+import static android.provider.Telephony.Sms.Intents.DATA_SMS_RECEIVED_ACTION;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.gsm.SmsManager;
+import android.telephony.gsm.SmsMessage;
+import android.util.Log;
+
+import com.android.im.engine.SmsService;
+
+public class AndroidSmsService implements SmsService {
+ private static final String TAG = RemoteImService.TAG;
+
+ private static final String SMS_STATUS_RECEIVED_ACTION =
+ "com.android.im.SmsService.SMS_STATUS_RECEIVED";
+
+ private static final int sMaxSmsLength =
+ SmsMessage.MAX_USER_DATA_BYTES - 7/* UDH size */;
+
+ private Context mContext;
+ private SmsReceiver mSmsReceiver;
+ private IntentFilter mIntentFilter;
+ /*package*/HashMap<Integer, ListenerList> mListeners;
+ /*package*/HashMap<Long, SmsSendFailureCallback> mFailureCallbacks;
+
+ public AndroidSmsService(Context context) {
+ mContext = context;
+ mSmsReceiver = new SmsReceiver();
+ mIntentFilter = new IntentFilter(
+ Telephony.Sms.Intents.DATA_SMS_RECEIVED_ACTION);
+ mIntentFilter.addDataScheme("sms");
+ mListeners = new HashMap<Integer, ListenerList>();
+ mFailureCallbacks = new HashMap<Long, SmsSendFailureCallback>();
+ }
+
+ public int getMaxSmsLength() {
+ return sMaxSmsLength;
+ }
+
+ public void sendSms(String dest, int port, byte[] data) {
+ sendSms(dest, port, data, null);
+ }
+
+ public void sendSms(String dest, int port, byte[] data,
+ SmsSendFailureCallback callback) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ try {
+ log(dest + ":" + port + " >>> " + new String(data, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ }
+ }
+ if (data.length > sMaxSmsLength) {
+ Log.e(TAG, "SMS data message can only contain " + sMaxSmsLength
+ + " bytes");
+ return;
+ }
+
+ SmsManager smsManager = SmsManager.getDefault();
+ PendingIntent sentIntent;
+ if (callback == null) {
+ sentIntent = null;
+ } else {
+ long msgId = genMsgId();
+ mFailureCallbacks.put(msgId, callback);
+
+ sentIntent = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(
+ SMS_STATUS_RECEIVED_ACTION,
+ Uri.parse("content://sms/" + msgId), /*uri*/
+ mContext, SmsReceiver.class),
+ 0);
+ }
+ smsManager.sendDataMessage(dest, null/*use the default SMSC*/,
+ (short) port, data,
+ sentIntent,
+ null/*do not require delivery report*/);
+ }
+
+ public void addSmsListener(String from, int port, SmsListener listener) {
+ ListenerList l = mListeners.get(port);
+ if (l == null) {
+ l = new ListenerList(port);
+ mListeners.put(port, l);
+
+ // We didn't listen on the port yet, register the receiver with the
+ // additional port.
+ mIntentFilter.addDataAuthority("*", String.valueOf(port));
+ mContext.registerReceiver(mSmsReceiver, mIntentFilter);
+ }
+ l.addListener(from, listener);
+ }
+
+ public void removeSmsListener(SmsListener listener) {
+ Iterator<ListenerList> iter = mListeners.values().iterator();
+ while (iter.hasNext()) {
+ ListenerList l = iter.next();
+ l.removeListener(listener);
+ if (l.isEmpty()) {
+ iter.remove();
+ }
+ }
+ }
+
+ public void stop() {
+ mContext.unregisterReceiver(mSmsReceiver);
+ }
+
+ private static long sNextMsgId = 0;
+ private static synchronized long genMsgId() {
+ return sNextMsgId++;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, "[SmsService]" + msg);
+ }
+
+ private final class SmsReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (SMS_STATUS_RECEIVED_ACTION.equals(intent.getAction())) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ log("send status received");
+ }
+ long id = ContentUris.parseId(intent.getData());
+ SmsSendFailureCallback callback = mFailureCallbacks.get(id);
+ if (callback == null) {
+ return;
+ }
+
+ int resultCode = getResultCode();
+ if (resultCode == SmsManager.RESULT_ERROR_GENERIC_FAILURE) {
+ callback.onFailure(SmsSendFailureCallback.ERROR_GENERIC_FAILURE);
+ } else if (resultCode == SmsManager.RESULT_ERROR_RADIO_OFF) {
+ callback.onFailure(SmsSendFailureCallback.ERROR_RADIO_OFF);
+ }
+ mFailureCallbacks.remove(id);
+ } else if (DATA_SMS_RECEIVED_ACTION.equals(intent.getAction())){
+ Uri uri = intent.getData();
+ int port = uri.getPort();
+ ListenerList listeners = mListeners.get(port);
+ if (listeners == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ log("No listener on port " + port + ", ignore");
+ }
+ return;
+ }
+
+ SmsMessage[] receivedSms
+ = Telephony.Sms.Intents.getMessagesFromIntent(intent);
+
+ for (SmsMessage msg : receivedSms) {
+ String from = msg.getOriginatingAddress();
+ byte[] data = msg.getUserData();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ try {
+ log(from + ":" + port + " <<< " + new String(data, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ }
+ }
+ listeners.notifySms(from, data);
+ }
+ }
+ }
+ }
+
+ private final static class ListenerList {
+ private int mPort;
+ private ArrayList<String> mAddrList;
+ private ArrayList<SmsListener> mListenerList;
+
+ public ListenerList(int port) {
+ mPort = port;
+ mAddrList = new ArrayList<String>();
+ mListenerList = new ArrayList<SmsListener>();
+ }
+
+ public synchronized void addListener(String addr, SmsListener listener) {
+ mAddrList.add(addr);
+ mListenerList.add(listener);
+ }
+
+ public synchronized void removeListener(SmsListener listener) {
+ int index = -1;
+ while ((index = mListenerList.indexOf(listener)) != -1) {
+ mAddrList.remove(index);
+ mListenerList.remove(index);
+ }
+ }
+
+ public void notifySms(String addr, byte[] data) {
+ int N = mListenerList.size();
+ for (int i = 0; i < N; i++) {
+ if (PhoneNumberUtils.compare(addr, mAddrList.get(i))) {
+ mListenerList.get(i).onIncomingSms(data);
+ }
+ }
+ }
+
+ public boolean isEmpty() {
+ return mListenerList.isEmpty();
+ }
+
+ public int getPort() {
+ return mPort;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 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 com.android.im.service;
+
+import android.content.Context;
+
+import com.android.im.engine.HeartbeatService;
+import com.android.im.engine.SmsService;
+import com.android.im.engine.SystemService;
+
+public class AndroidSystemService extends SystemService {
+ private static AndroidSystemService sInstance;
+
+ private AndroidSystemService() {
+ }
+
+ public static AndroidSystemService getInstance() {
+ if (sInstance == null) {
+ sInstance = new AndroidSystemService();
+ }
+ return sInstance;
+ }
+
+ private Context mContext;
+ private AndroidHeartBeatService mHeartbeatServcie;
+ private AndroidSmsService mSmsService;
+
+ public void initialize(Context context) {
+ mContext = context;
+ }
+
+ public void shutdown() {
+ if (mHeartbeatServcie != null) {
+ mHeartbeatServcie.stopAll();
+ }
+ if (mSmsService != null) {
+ mSmsService.stop();
+ }
+ }
+
+ @Override
+ public HeartbeatService getHeartbeatService() {
+ if(mContext == null) {
+ throw new IllegalStateException("Hasn't been initialized yet");
+ }
+ if (mHeartbeatServcie == null) {
+ mHeartbeatServcie = new AndroidHeartBeatService(mContext);
+ }
+ return mHeartbeatServcie;
+ }
+
+ @Override
+ public SmsService getSmsService() {
+ if(mContext == null) {
+ throw new IllegalStateException("Hasn't been initialized yet");
+ }
+ if (mSmsService == null) {
+ mSmsService = new AndroidSmsService(mContext);
+ }
+ return mSmsService;
+ }
+
+}
for (ChatSessionAdapter session : mChatSessionManager.mActiveSessions.values()) {
session.sendPostponedMessages();
}
+ } else if (state == ImConnection.LOGGING_OUT) {
+ // The engine has started to logout the connection, remove it
+ // from the active connection list.
+ mService.removeConnection(ImConnectionAdapter.this);
} else if(state == ImConnection.DISCONNECTED) {
mService.removeConnection(ImConnectionAdapter.this);
final RemoteCallbackList<IConnectionCreationListener> mRemoteListeners
= new RemoteCallbackList<IConnectionCreationListener>();
- private AndroidHeartBeatService mHeartBeatService;
-
private HashMap<Long, ImPluginInfo> mPlugins;
public RemoteImService() {
mNetworkConnectivityListener.startListening(this);
findAvaiablePlugins();
+ AndroidSystemService.getInstance().initialize(this);
}
private void findAvaiablePlugins() {
return !oldVersion.equals(newVersion);
}
- public AndroidHeartBeatService getHeartBeatService() {
- if (mHeartBeatService == null) {
- mHeartBeatService = new AndroidHeartBeatService(this);
- }
- return mHeartBeatService;
- }
-
-
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
// because the mobile network will take care of it.
if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) {
settings.put(ImpsConfigNames.MSISDN, "1231231234");
- } else if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- // 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 = TelephonyManager.getDefault().getLine1Number();
- if (!TextUtils.isEmpty(msisdn)) {
- settings.put(ImpsConfigNames.MSISDN, msisdn);
+ } 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 {
- // TODO: This should be removed. We can't fetch phone number from
- // the test T-Mobile SIMs. Use a fake phone number so that we can
- // work with our test SIMs right now. This can't happen with T-Mobile
- // production SIMs
- Log.w(TAG, "Can not get phone number from SIM, use a fake one");
- settings.put(ImpsConfigNames.MSISDN, "1231231234");
+ // 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 = TelephonyManager.getDefault().getLine1Number();
+ if (!TextUtils.isEmpty(msisdn)) {
+ settings.put(ImpsConfigNames.MSISDN, msisdn);
+ }
}
}
return settings;
for (ImConnectionAdapter conn : mConnections) {
conn.logout();
}
+
+ AndroidSystemService.getInstance().shutdown();
+
mNetworkConnectivityListener.unregisterHandler(mServiceHandler);
mNetworkConnectivityListener.stopListening();
mNetworkConnectivityListener = null;
ConnectionFactory factory = ConnectionFactory.getInstance();
try {
ImConnection conn = factory.createConnection(config);
- conn.setHeartBeatService(getHeartBeatService());
ImConnectionAdapter result = new ImConnectionAdapter(providerId,
conn, this);
mConnections.add(result);