OSDN Git Service

auto import from //branches/cupcake/...@127101
authorThe Android Open Source Project <initial-contribution@android.com>
Tue, 20 Jan 2009 22:04:00 +0000 (14:04 -0800)
committerThe Android Open Source Project <initial-contribution@android.com>
Tue, 20 Jan 2009 22:04:00 +0000 (14:04 -0800)
87 files changed:
AndroidManifest.xml
plugin/com/android/im/plugin/ImpsConfigNames.java
res/drawable/emo_im_angel.png [deleted file]
res/drawable/emo_im_cool.png [deleted file]
res/drawable/emo_im_crying.png [deleted file]
res/drawable/emo_im_embarrased.png [deleted file]
res/drawable/emo_im_foot_in_mouth.png [deleted file]
res/drawable/emo_im_happy.png [deleted file]
res/drawable/emo_im_kissing.png [deleted file]
res/drawable/emo_im_laughing.png [deleted file]
res/drawable/emo_im_lips_are_sealed.png [deleted file]
res/drawable/emo_im_money_mouth.png [deleted file]
res/drawable/emo_im_sad.png [deleted file]
res/drawable/emo_im_surprised.png [deleted file]
res/drawable/emo_im_tongue_sticking_out.png [deleted file]
res/drawable/emo_im_undecided.png [deleted file]
res/drawable/emo_im_winking.png [deleted file]
res/drawable/emo_im_wtf.png [deleted file]
res/drawable/emo_im_yelling.png [deleted file]
res/drawable/ic_menu_account_list.png [deleted file]
res/drawable/ic_menu_block.png [deleted file]
res/drawable/ic_menu_blocked_user.png [deleted file]
res/drawable/ic_menu_chat_dashboard.png [deleted file]
res/drawable/ic_menu_emoticons.png [deleted file]
res/drawable/ic_menu_end_conversation.png [deleted file]
res/drawable/ic_menu_friend_list.png [deleted file]
res/drawable/ic_menu_invite.png [deleted file]
res/drawable/ic_menu_start_conversation.png [deleted file]
res/drawable/status_chat.png [new file with mode: 0644]
res/drawable/status_chat_new.png [new file with mode: 0644]
res/layout/add_contact_activity.xml
res/menu/chat_screen_menu.xml
res/menu/contact_list_menu.xml
res/values/strings.xml
samples/PluginDemo/res/drawable/emo_im_angel.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_cool.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_crying.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_embarrased.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_foot_in_mouth.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_happy.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_kissing.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_laughing.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_lips_are_sealed.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_money_mouth.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_sad.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_surprised.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_tongue_sticking_out.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_undecided.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_winking.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_wtf.png [deleted file]
samples/PluginDemo/res/drawable/emo_im_yelling.png [deleted file]
samples/PluginDemo/src/com/android/im/plugin/demo/DemoImPlugin.java
src/com/android/im/app/AddContactActivity.java
src/com/android/im/app/ContactListActivity.java
src/com/android/im/app/ContactPresenceActivity.java
src/com/android/im/app/Dashboard.java
src/com/android/im/app/ImApp.java
src/com/android/im/app/NewChatActivity.java
src/com/android/im/app/SigningInActivity.java
src/com/android/im/app/SignoutActivity.java
src/com/android/im/engine/ContactListManager.java
src/com/android/im/engine/ImConnection.java
src/com/android/im/engine/SmsService.java [new file with mode: 0644]
src/com/android/im/engine/SystemService.java [new file with mode: 0644]
src/com/android/im/imps/DataChannel.java
src/com/android/im/imps/HttpDataChannel.java
src/com/android/im/imps/ImpsChatSessionManager.java
src/com/android/im/imps/ImpsClientCapability.java
src/com/android/im/imps/ImpsConnection.java
src/com/android/im/imps/ImpsConnectionConfig.java
src/com/android/im/imps/ImpsConstants.java
src/com/android/im/imps/ImpsContactListManager.java
src/com/android/im/imps/PresencePollingManager.java [new file with mode: 0644]
src/com/android/im/imps/PtsCodes.java
src/com/android/im/imps/PtsPrimitiveParser.java
src/com/android/im/imps/PtsPrimitiveSerializer.java
src/com/android/im/imps/SmsAssembler.java [new file with mode: 0644]
src/com/android/im/imps/SmsCirChannel.java [new file with mode: 0644]
src/com/android/im/imps/SmsDataChannel.java [new file with mode: 0644]
src/com/android/im/imps/SmsSplitter.java [new file with mode: 0644]
src/com/android/im/imps/TcpCirChannel.java
src/com/android/im/imps/WbxmlSerializer.java
src/com/android/im/service/AndroidHeartBeatService.java
src/com/android/im/service/AndroidSmsService.java [new file with mode: 0644]
src/com/android/im/service/AndroidSystemService.java [new file with mode: 0644]
src/com/android/im/service/ImConnectionAdapter.java
src/com/android/im/service/RemoteImService.java

index ee5fb3c..eb26284 100644 (file)
@@ -26,6 +26,8 @@
     <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" />
index a7d1766..de64cd4 100644 (file)
@@ -42,11 +42,36 @@ public interface ImpsConfigNames extends ImConfigNames {
     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";
@@ -62,11 +87,28 @@ public interface ImpsConfigNames extends ImConfigNames {
     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.
      */
diff --git a/res/drawable/emo_im_angel.png b/res/drawable/emo_im_angel.png
deleted file mode 100644 (file)
index c34dfa6..0000000
Binary files a/res/drawable/emo_im_angel.png and /dev/null differ
diff --git a/res/drawable/emo_im_cool.png b/res/drawable/emo_im_cool.png
deleted file mode 100644 (file)
index d8eeb34..0000000
Binary files a/res/drawable/emo_im_cool.png and /dev/null differ
diff --git a/res/drawable/emo_im_crying.png b/res/drawable/emo_im_crying.png
deleted file mode 100644 (file)
index 1cafdb3..0000000
Binary files a/res/drawable/emo_im_crying.png and /dev/null differ
diff --git a/res/drawable/emo_im_embarrased.png b/res/drawable/emo_im_embarrased.png
deleted file mode 100644 (file)
index e4db963..0000000
Binary files a/res/drawable/emo_im_embarrased.png and /dev/null differ
diff --git a/res/drawable/emo_im_foot_in_mouth.png b/res/drawable/emo_im_foot_in_mouth.png
deleted file mode 100644 (file)
index 09d1fba..0000000
Binary files a/res/drawable/emo_im_foot_in_mouth.png and /dev/null differ
diff --git a/res/drawable/emo_im_happy.png b/res/drawable/emo_im_happy.png
deleted file mode 100644 (file)
index b86602a..0000000
Binary files a/res/drawable/emo_im_happy.png and /dev/null differ
diff --git a/res/drawable/emo_im_kissing.png b/res/drawable/emo_im_kissing.png
deleted file mode 100644 (file)
index 56378f6..0000000
Binary files a/res/drawable/emo_im_kissing.png and /dev/null differ
diff --git a/res/drawable/emo_im_laughing.png b/res/drawable/emo_im_laughing.png
deleted file mode 100644 (file)
index 980bf28..0000000
Binary files a/res/drawable/emo_im_laughing.png and /dev/null differ
diff --git a/res/drawable/emo_im_lips_are_sealed.png b/res/drawable/emo_im_lips_are_sealed.png
deleted file mode 100644 (file)
index f2de993..0000000
Binary files a/res/drawable/emo_im_lips_are_sealed.png and /dev/null differ
diff --git a/res/drawable/emo_im_money_mouth.png b/res/drawable/emo_im_money_mouth.png
deleted file mode 100644 (file)
index 08c53fd..0000000
Binary files a/res/drawable/emo_im_money_mouth.png and /dev/null differ
diff --git a/res/drawable/emo_im_sad.png b/res/drawable/emo_im_sad.png
deleted file mode 100644 (file)
index 31c08d0..0000000
Binary files a/res/drawable/emo_im_sad.png and /dev/null differ
diff --git a/res/drawable/emo_im_surprised.png b/res/drawable/emo_im_surprised.png
deleted file mode 100644 (file)
index abe8c7a..0000000
Binary files a/res/drawable/emo_im_surprised.png and /dev/null differ
diff --git a/res/drawable/emo_im_tongue_sticking_out.png b/res/drawable/emo_im_tongue_sticking_out.png
deleted file mode 100644 (file)
index 6f0f47b..0000000
Binary files a/res/drawable/emo_im_tongue_sticking_out.png and /dev/null differ
diff --git a/res/drawable/emo_im_undecided.png b/res/drawable/emo_im_undecided.png
deleted file mode 100644 (file)
index eb4f8c5..0000000
Binary files a/res/drawable/emo_im_undecided.png and /dev/null differ
diff --git a/res/drawable/emo_im_winking.png b/res/drawable/emo_im_winking.png
deleted file mode 100644 (file)
index 568562a..0000000
Binary files a/res/drawable/emo_im_winking.png and /dev/null differ
diff --git a/res/drawable/emo_im_wtf.png b/res/drawable/emo_im_wtf.png
deleted file mode 100644 (file)
index 41dd47f..0000000
Binary files a/res/drawable/emo_im_wtf.png and /dev/null differ
diff --git a/res/drawable/emo_im_yelling.png b/res/drawable/emo_im_yelling.png
deleted file mode 100644 (file)
index c3c8612..0000000
Binary files a/res/drawable/emo_im_yelling.png and /dev/null differ
diff --git a/res/drawable/ic_menu_account_list.png b/res/drawable/ic_menu_account_list.png
deleted file mode 100644 (file)
index f0945b2..0000000
Binary files a/res/drawable/ic_menu_account_list.png and /dev/null differ
diff --git a/res/drawable/ic_menu_block.png b/res/drawable/ic_menu_block.png
deleted file mode 100644 (file)
index 422eeb1..0000000
Binary files a/res/drawable/ic_menu_block.png and /dev/null differ
diff --git a/res/drawable/ic_menu_blocked_user.png b/res/drawable/ic_menu_blocked_user.png
deleted file mode 100644 (file)
index 5a5619b..0000000
Binary files a/res/drawable/ic_menu_blocked_user.png and /dev/null differ
diff --git a/res/drawable/ic_menu_chat_dashboard.png b/res/drawable/ic_menu_chat_dashboard.png
deleted file mode 100644 (file)
index 37fd3cb..0000000
Binary files a/res/drawable/ic_menu_chat_dashboard.png and /dev/null differ
diff --git a/res/drawable/ic_menu_emoticons.png b/res/drawable/ic_menu_emoticons.png
deleted file mode 100644 (file)
index e8c4e47..0000000
Binary files a/res/drawable/ic_menu_emoticons.png and /dev/null differ
diff --git a/res/drawable/ic_menu_end_conversation.png b/res/drawable/ic_menu_end_conversation.png
deleted file mode 100644 (file)
index 0ea0fcb..0000000
Binary files a/res/drawable/ic_menu_end_conversation.png and /dev/null differ
diff --git a/res/drawable/ic_menu_friend_list.png b/res/drawable/ic_menu_friend_list.png
deleted file mode 100644 (file)
index 4876021..0000000
Binary files a/res/drawable/ic_menu_friend_list.png and /dev/null differ
diff --git a/res/drawable/ic_menu_invite.png b/res/drawable/ic_menu_invite.png
deleted file mode 100644 (file)
index 7577e6d..0000000
Binary files a/res/drawable/ic_menu_invite.png and /dev/null differ
diff --git a/res/drawable/ic_menu_start_conversation.png b/res/drawable/ic_menu_start_conversation.png
deleted file mode 100644 (file)
index aadcc2f..0000000
Binary files a/res/drawable/ic_menu_start_conversation.png and /dev/null differ
diff --git a/res/drawable/status_chat.png b/res/drawable/status_chat.png
new file mode 100644 (file)
index 0000000..abfb6fa
Binary files /dev/null and b/res/drawable/status_chat.png differ
diff --git a/res/drawable/status_chat_new.png b/res/drawable/status_chat_new.png
new file mode 100644 (file)
index 0000000..564b38b
Binary files /dev/null and b/res/drawable/status_chat_new.png differ
index c4fd5ac..d7aa87c 100644 (file)
 
     <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"
index b2af807..74714d4 100644 (file)
 
     <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>
index 7e35eaf..cf15b4a 100644 (file)
 
     <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"
index 1e92403..0ec0d02 100644 (file)
@@ -81,7 +81,7 @@
 
     <!-- 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. -->
diff --git a/samples/PluginDemo/res/drawable/emo_im_angel.png b/samples/PluginDemo/res/drawable/emo_im_angel.png
deleted file mode 100644 (file)
index c34dfa6..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_angel.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_cool.png b/samples/PluginDemo/res/drawable/emo_im_cool.png
deleted file mode 100644 (file)
index d8eeb34..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_cool.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_crying.png b/samples/PluginDemo/res/drawable/emo_im_crying.png
deleted file mode 100644 (file)
index 1cafdb3..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_crying.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_embarrased.png b/samples/PluginDemo/res/drawable/emo_im_embarrased.png
deleted file mode 100644 (file)
index e4db963..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_embarrased.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_foot_in_mouth.png b/samples/PluginDemo/res/drawable/emo_im_foot_in_mouth.png
deleted file mode 100644 (file)
index 09d1fba..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_foot_in_mouth.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_happy.png b/samples/PluginDemo/res/drawable/emo_im_happy.png
deleted file mode 100644 (file)
index b86602a..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_happy.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_kissing.png b/samples/PluginDemo/res/drawable/emo_im_kissing.png
deleted file mode 100644 (file)
index 56378f6..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_kissing.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_laughing.png b/samples/PluginDemo/res/drawable/emo_im_laughing.png
deleted file mode 100644 (file)
index 980bf28..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_laughing.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_lips_are_sealed.png b/samples/PluginDemo/res/drawable/emo_im_lips_are_sealed.png
deleted file mode 100644 (file)
index f2de993..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_lips_are_sealed.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_money_mouth.png b/samples/PluginDemo/res/drawable/emo_im_money_mouth.png
deleted file mode 100644 (file)
index 08c53fd..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_money_mouth.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_sad.png b/samples/PluginDemo/res/drawable/emo_im_sad.png
deleted file mode 100644 (file)
index 31c08d0..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_sad.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_surprised.png b/samples/PluginDemo/res/drawable/emo_im_surprised.png
deleted file mode 100644 (file)
index abe8c7a..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_surprised.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_tongue_sticking_out.png b/samples/PluginDemo/res/drawable/emo_im_tongue_sticking_out.png
deleted file mode 100644 (file)
index 6f0f47b..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_tongue_sticking_out.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_undecided.png b/samples/PluginDemo/res/drawable/emo_im_undecided.png
deleted file mode 100644 (file)
index eb4f8c5..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_undecided.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_winking.png b/samples/PluginDemo/res/drawable/emo_im_winking.png
deleted file mode 100644 (file)
index 568562a..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_winking.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_wtf.png b/samples/PluginDemo/res/drawable/emo_im_wtf.png
deleted file mode 100644 (file)
index 41dd47f..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_wtf.png and /dev/null differ
diff --git a/samples/PluginDemo/res/drawable/emo_im_yelling.png b/samples/PluginDemo/res/drawable/emo_im_yelling.png
deleted file mode 100644 (file)
index c3c8612..0000000
Binary files a/samples/PluginDemo/res/drawable/emo_im_yelling.png and /dev/null differ
index c01d832..9975d38 100644 (file)
@@ -136,23 +136,23 @@ public class DemoImPlugin extends Service {
      * 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
     };
 
 }
index a2b490e..6e8a92d 100644 (file)
@@ -20,10 +20,12 @@ package com.android.im.app;
 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;
@@ -39,6 +41,8 @@ import android.view.View;
 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;
@@ -53,13 +57,20 @@ import java.util.List;
 
 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
@@ -82,15 +93,54 @@ public class AddContactActivity extends Activity {
         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);
     }
@@ -101,7 +151,8 @@ public class AddContactActivity extends Activity {
             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;
@@ -137,8 +188,9 @@ public class AddContactActivity extends Activity {
 
         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();
@@ -160,6 +212,11 @@ public class AddContactActivity extends Activity {
         }
     }
 
+    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() {
index 32b602a..a102108 100644 (file)
@@ -159,6 +159,7 @@ public class ContactListActivity extends Activity implements View.OnCreateContex
             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);
@@ -355,26 +356,26 @@ public class ContactListActivity extends Activity implements View.OnCreateContex
 
         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)
index 36e09a9..6531503 100644 (file)
@@ -96,9 +96,12 @@ public class ContactPresenceActivity extends Activity {
             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));
index 27b4c8e..84a0af7 100644 (file)
@@ -18,6 +18,7 @@
 package com.android.im.app;
 
 import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
 
 import android.app.Activity;
 import android.content.ContentResolver;
@@ -28,8 +29,8 @@ import android.content.res.Resources;
 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;
@@ -55,6 +56,14 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
     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);
     }
@@ -88,7 +97,18 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
 
         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);
@@ -101,11 +121,19 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
 
     @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);
     }
 
@@ -121,13 +149,10 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
             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();
             }
         }
@@ -135,25 +160,45 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
     }
 
     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);
         }
@@ -180,9 +225,9 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
 
     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();
 
@@ -217,7 +262,40 @@ public class Dashboard extends LinearLayout implements Gallery.OnItemClickListen
         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));
index 3ec371b..a2768dd 100644 (file)
@@ -321,6 +321,10 @@ public class ImApp extends Application {
                 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);
 
@@ -635,21 +639,6 @@ public class ImApp extends Application {
                 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:
index 3736ea8..569d2a9 100644 (file)
@@ -24,6 +24,7 @@ import com.android.im.service.ImServiceConstants;
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -213,6 +214,29 @@ public class NewChatActivity extends Activity {
             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);
index d527a59..870bd44 100644 (file)
@@ -31,6 +31,7 @@ import android.app.Activity;
 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;
@@ -91,12 +92,13 @@ public class SigningInActivity extends Activity {
             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);
@@ -119,6 +121,9 @@ public class SigningInActivity extends Activity {
         mApp.callWhenServiceConnected(mHandler, new Runnable() {
             public void run() {
                 if (mApp.serviceConnected()) {
+                    if (!isActive) {
+                        activateAccount(providerId, accountId);
+                    }
                     signInAccount(provider, accountId, username, pw);
                 }
             }
@@ -148,12 +153,29 @@ public class SigningInActivity extends Activity {
                 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) {
@@ -174,7 +196,8 @@ public class SigningInActivity extends Activity {
 
     @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;
     }
index 45d7208..2cb3bcb 100644 (file)
@@ -21,6 +21,7 @@ import android.provider.Im;
 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;
@@ -32,6 +33,7 @@ import com.android.im.IImConnection;
 public class SignoutActivity extends Activity {
 
     private String[] ACCOUNT_SELECTION = new String[] {
+            Im.Account._ID,
             Im.Account.PROVIDER,
     };
 
@@ -58,6 +60,7 @@ public class SignoutActivity extends Activity {
                 null /* selection args */,
                 null /* sort order */);
         final long providerId;
+        final long accountId;
 
         try {
             if (!c.moveToFirst()) {
@@ -67,6 +70,7 @@ public class SignoutActivity extends Activity {
             }
 
             providerId = c.getLong(c.getColumnIndexOrThrow(Im.Account.PROVIDER));
+            accountId = c.getLong(c.getColumnIndexOrThrow(Im.Account._ID));
         } finally {
             c.close();
         }
@@ -74,16 +78,30 @@ public class SignoutActivity extends Activity {
         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);
index b433060..7b3d92d 100644 (file)
@@ -59,6 +59,17 @@ public abstract class ContactListManager {
     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.
@@ -68,6 +79,9 @@ public abstract class ContactListManager {
         mContactListListeners = new CopyOnWriteArrayList<ContactListListener>();
         mBlockedList = new Vector<Contact>();
 
+        mBlockPending = new Vector<String>(4);
+        mDeletePending = new Vector<Contact>(4);
+
         mState = LISTS_NOT_LOADED;
     }
 
@@ -332,6 +346,9 @@ public abstract class ContactListManager {
             return;
         }
 
+        if (mBlockPending.contains(address)) {
+            return;
+        }
         doBlockContactAsync(address, true);
     }
 
@@ -371,6 +388,10 @@ public abstract class ContactListManager {
             throws ImException {
         checkState();
 
+        if (mDeletePending.contains(contact)) {
+            return;
+        }
+
         doRemoveContactFromListAsync(contact, list);
     }
 
@@ -482,6 +503,11 @@ public abstract class ContactListManager {
      */
     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);
         }
@@ -523,6 +549,7 @@ public abstract class ContactListManager {
                 list.insertToCache(contact);
             } else if (type == ContactListListener.LIST_CONTACT_REMOVED) {
                 list.removeFromCache(contact);
+                mDeletePending.remove(contact);
             }
         }
 
@@ -596,6 +623,8 @@ public abstract class ContactListManager {
         synchronized (this) {
             if (blocked) {
                 mBlockedList.add(contact);
+                String addr = contact.getAddress().getFullName();
+                mBlockPending.remove(addr);
             } else {
                 mBlockedList.remove(contact);
             }
index 781e937..bd0f2ab 100644 (file)
@@ -71,7 +71,6 @@ public abstract class ImConnection {
 
     protected CopyOnWriteArrayList<ConnectionListener> mConnectionListeners;
     protected Presence mUserPresence;
-    protected HeartbeatService mHeartbeatService;
 
     protected ImConnection() {
         mConnectionListeners = new CopyOnWriteArrayList<ConnectionListener>();
@@ -134,14 +133,6 @@ public abstract class ImConnection {
     public void networkTypeChanged(){
     }
 
-    public void setHeartBeatService(HeartbeatService service) {
-        mHeartbeatService = service;
-    }
-
-    public HeartbeatService getHeartBeatService() {
-        return mHeartbeatService;
-    }
-
     /**
      * Tells the current state of the connection.
      */
diff --git a/src/com/android/im/engine/SmsService.java b/src/com/android/im/engine/SmsService.java
new file mode 100644 (file)
index 0000000..5ba170b
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * 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);
+}
diff --git a/src/com/android/im/engine/SystemService.java b/src/com/android/im/engine/SystemService.java
new file mode 100644 (file)
index 0000000..26edc2a
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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();
+}
index f999c02..a244677 100644 (file)
@@ -32,8 +32,6 @@ abstract class DataChannel {
 
     protected DataChannel(ImpsConnection connection) throws ImException {
         mConnection = connection;
-        mParser = mConnection.getConfig().createPrimitiveParser();
-        mSerializer = mConnection.getConfig().createPrimitiveSerializer();
     }
 
     /**
index f656c3f..8f49751 100644 (file)
@@ -44,6 +44,7 @@ import android.util.Log;
 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;
 
 /**
@@ -116,6 +117,9 @@ class HttpDataChannel extends DataChannel implements Runnable, HeartbeatService.
         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
@@ -155,6 +159,7 @@ class HttpDataChannel extends DataChannel implements Runnable, HeartbeatService.
             Primitive polling = new Primitive(ImpsTags.Polling_Request);
             polling.setSession(mConnection.getSession().getID());
             sendPrimitive(polling);
+            startHeartbeat();
 
             return true;
         }
@@ -162,7 +167,8 @@ class HttpDataChannel extends DataChannel implements Runnable, HeartbeatService.
 
     @Override
     public void shutdown() {
-        HeartbeatService heartbeatService = mConnection.getHeartBeatService();
+        HeartbeatService heartbeatService
+            = SystemService.getDefault().getHeartbeatService();
         if (heartbeatService != null) {
             heartbeatService.stopHeartbeat(this);
         }
@@ -222,13 +228,22 @@ class HttpDataChannel extends DataChannel implements Runnable, HeartbeatService.
             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();
index 711abb5..b2c49db 100644 (file)
@@ -208,11 +208,10 @@ public class ImpsChatSessionManager extends ChatSessionManager
      * @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);
index 75a6d05..f075846 100644 (file)
@@ -99,6 +99,7 @@ final class ImpsClientCapability {
     public static CirMethod[] getSupportedCirMethods() {
         return new CirMethod[] {
                 CirMethod.STCP,
+                CirMethod.SSMS,
                 CirMethod.SHTTP,
         };
     }
index 44e5120..8f84cdb 100644 (file)
@@ -32,6 +32,7 @@ import com.android.im.engine.LoginInfo;
 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.
@@ -166,7 +167,11 @@ public class ImpsConnection extends ImConnection {
 
     private void doLogin() {
         try {
-            initDataChannel();
+            if (mConfig.useSmsAuth()) {
+                mDataChannel = new SmsDataChannel(this);
+            } else {
+                mDataChannel = createDataChannel();
+            }
             mDataChannel.connect();
         } catch (ImException e) {
             ImErrorInfo error = e.getImError();
@@ -296,6 +301,34 @@ public class ImpsConnection extends ImConnection {
         }
 
         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() {
@@ -365,26 +398,31 @@ public class ImpsConnection extends ImConnection {
             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() {
@@ -444,10 +482,12 @@ public class ImpsConnection extends ImConnection {
         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");
         }
@@ -473,6 +513,8 @@ public class ImpsConnection extends ImConnection {
             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 {
@@ -495,6 +537,11 @@ public class ImpsConnection extends ImConnection {
             mChannel = channel;
         }
 
+        public void changeDataChannel(DataChannel channel) {
+            mChannel = channel;
+            interrupt();
+        }
+
         @Override
         public void run() {
             Primitive primitive = null;
@@ -556,6 +603,19 @@ public class ImpsConnection extends ImConnection {
             }
         }
 
+        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) {
index e35bbad..0986dbc 100644 (file)
@@ -31,6 +31,10 @@ public class ImpsConnectionConfig extends ConnectionConfig {
     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.
@@ -65,6 +69,8 @@ public class ImpsConnectionConfig extends ConnectionConfig {
 
     private String mPluginPath;
 
+    private Map<String, String> mOthers;
+
     public ImpsConnectionConfig() {
         setupVersionStrings();
     }
@@ -104,11 +110,14 @@ public class ImpsConnectionConfig extends ConnectionConfig {
             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();
 
@@ -117,6 +126,8 @@ public class ImpsConnectionConfig extends ConnectionConfig {
         mPluginPath = map.get(ImpsConfigNames.PLUGIN_PATH);
         mCustomPasswordDigest = map.get(ImpsConfigNames.CUSTOM_PASSWORD_DIGEST);
         mCustomPresenceMapping = map.get(ImpsConfigNames.CUSTOM_PRESENCE_MAPPING);
+
+        mOthers = map;
     }
 
     @Override
@@ -125,7 +136,18 @@ public class ImpsConnectionConfig extends ConnectionConfig {
     }
 
     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) {
@@ -164,6 +186,10 @@ public class ImpsConnectionConfig extends ConnectionConfig {
         return mSecureLogin;
     }
 
+    public boolean useSmsAuth() {
+        return isTrue(mOthers.get(ImpsConfigNames.SMS_AUTH));
+    }
+
     public boolean needDeliveryReport() {
         return NEED_DELIVERY_REPORT;
     }
@@ -281,6 +307,55 @@ public class ImpsConnectionConfig extends ConnectionConfig {
         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;
     }
@@ -332,6 +407,10 @@ public class ImpsConnectionConfig extends ConnectionConfig {
         return mDefaultDomain;
     }
 
+    private boolean isTrue(String value) {
+        return "true".equalsIgnoreCase(value);
+    }
+
     /**
      * Represents the type of protocol binding for data channel.
      */
index 4ed6365..0bc8378 100644 (file)
@@ -20,25 +20,48 @@ package com.android.im.imps;
 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:";
 
index 4792736..cc6e369 100644 (file)
@@ -41,6 +41,7 @@ public class ImpsContactListManager extends ContactListManager
     private ImpsConnection mConnection;
     private String mDefaultDomain;
     ImpsTransactionManager mTransactionManager;
+    ImpsConnectionConfig mConfig;
 
     boolean mAllowAutoSubscribe = true;
 
@@ -53,11 +54,14 @@ public class ImpsContactListManager extends ContactListManager
      */
     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
@@ -148,6 +152,41 @@ public class ImpsContactListManager extends ContactListManager
         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);
 //
@@ -254,7 +293,8 @@ public class ImpsContactListManager extends ContactListManager
         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) {
@@ -263,13 +303,17 @@ public class ImpsContactListManager extends ContactListManager
 
                     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();
+                }
             }
 
         };
@@ -340,18 +384,13 @@ public class ImpsContactListManager extends ContactListManager
 
             @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);
+                }
             }
         };
 
@@ -403,8 +442,9 @@ public class ImpsContactListManager extends ContactListManager
             @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() {}
 
@@ -487,9 +527,9 @@ public class ImpsContactListManager extends ContactListManager
         // 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);
             }
@@ -508,27 +548,8 @@ public class ImpsContactListManager extends ContactListManager
         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);
 
@@ -552,6 +573,32 @@ public class ImpsContactListManager extends ContactListManager
         }
     }
 
+    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);
@@ -574,7 +621,11 @@ public class ImpsContactListManager extends ContactListManager
                     mDefaultContactList = list;
                 }
 
-                subscribeToListAsync(list, completion);
+                if (mConfig.usePrensencePolling()) {
+                    completion.onComplete();
+                } else {
+                    subscribeToListAsync(list, completion);
+                }
             }
         };
 
@@ -583,7 +634,7 @@ public class ImpsContactListManager extends ContactListManager
 
     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);
@@ -757,9 +808,9 @@ public class ImpsContactListManager extends ContactListManager
         }
 
         private final class LoadListCompletion implements AsyncCompletion {
-            private int listIndex;
+            private int mListIndex;
             LoadListCompletion() {
-                listIndex = 0;
+                mListIndex = 0;
             }
 
             public void onComplete() {
@@ -771,7 +822,7 @@ public class ImpsContactListManager extends ContactListManager
             }
 
             private void processResult(ImErrorInfo error) {
-                ImpsAddress addr = mListAddresses.get(listIndex);
+                ImpsAddress addr = mListAddresses.get(mListIndex);
 
                 if (error == null) {
                     notifyContactListLoaded(getContactList(addr));
@@ -780,9 +831,9 @@ public class ImpsContactListManager extends ContactListManager
                             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();
                 }
@@ -793,7 +844,12 @@ public class ImpsContactListManager extends ContactListManager
     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) {
@@ -833,28 +889,40 @@ public class ImpsContactListManager extends ContactListManager
                 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);
             }
@@ -935,4 +1003,14 @@ public class ImpsContactListManager extends ContactListManager
     protected ImConnection getConnection() {
         return mConnection;
     }
+
+    private PresencePollingManager mPollingMgr;
+    /*package*/PresencePollingManager getPresencePollingManager() {
+        if (mPollingMgr == null) {
+            mPollingMgr = new PresencePollingManager(this,
+                    mConfig.getPresencePollInterval());
+        }
+        return mPollingMgr;
+    }
+
 }
diff --git a/src/com/android/im/imps/PresencePollingManager.java b/src/com/android/im/imps/PresencePollingManager.java
new file mode 100644 (file)
index 0000000..e7d917c
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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;
+    }
+
+}
index 5c7b716..1ee46e4 100644 (file)
@@ -189,6 +189,8 @@ public class PtsCodes {
         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");
index b5b95a5..dbaabc7 100644 (file)
@@ -51,7 +51,8 @@ public class PtsPrimitiveParser implements PrimitiveParser {
 
     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;
index ed1bcaf..1e9982a 100644 (file)
@@ -41,7 +41,9 @@ public class PtsPrimitiveSerializer implements PrimitiveSerializer {
     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";
diff --git a/src/com/android/im/imps/SmsAssembler.java b/src/com/android/im/imps/SmsAssembler.java
new file mode 100644 (file)
index 0000000..644b517
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * 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;
+        }
+    }
+
+}
diff --git a/src/com/android/im/imps/SmsCirChannel.java b/src/com/android/im/imps/SmsCirChannel.java
new file mode 100644 (file)
index 0000000..84f01ee
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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) {
+        }
+    }
+
+}
diff --git a/src/com/android/im/imps/SmsDataChannel.java b/src/com/android/im/imps/SmsDataChannel.java
new file mode 100644 (file)
index 0000000..a5f8d6b
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/src/com/android/im/imps/SmsSplitter.java b/src/com/android/im/imps/SmsSplitter.java
new file mode 100644 (file)
index 0000000..5c262fa
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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
index fd6ef24..6679a0a 100644 (file)
@@ -25,6 +25,7 @@ import java.net.UnknownHostException;
 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;
@@ -65,7 +66,8 @@ class TcpCirChannel extends CirChannel implements Runnable, HeartbeatService.Cal
             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);
             }
@@ -97,7 +99,8 @@ class TcpCirChannel extends CirChannel implements Runnable, HeartbeatService.Cal
         } catch (IOException e) {
             // ignore
         }
-        HeartbeatService heartbeatService = mConnection.getHeartBeatService();
+        HeartbeatService heartbeatService
+                = SystemService.getDefault().getHeartbeatService();
         if (heartbeatService != null) {
             heartbeatService.stopHeartbeat(this);
         }
@@ -206,6 +209,8 @@ class TcpCirChannel extends CirChannel implements Runnable, HeartbeatService.Cal
 
     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) {
index 3ed87a6..55e53a9 100644 (file)
@@ -29,11 +29,14 @@ final class WbxmlSerializer {
     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);
index 85f4704..fe7aa29 100644 (file)
@@ -90,6 +90,13 @@ public class AndroidHeartBeatService extends BroadcastReceiver
         }
     }
 
+    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();
diff --git a/src/com/android/im/service/AndroidSmsService.java b/src/com/android/im/service/AndroidSmsService.java
new file mode 100644 (file)
index 0000000..d1dda41
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/src/com/android/im/service/AndroidSystemService.java b/src/com/android/im/service/AndroidSystemService.java
new file mode 100644 (file)
index 0000000..e908924
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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;
+    }
+
+}
index c7f1cfd..7b97ac6 100644 (file)
@@ -373,6 +373,10 @@ public class ImConnectionAdapter extends IImConnection.Stub {
                 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);
 
index 9ce16a8..41bc9e8 100644 (file)
@@ -94,8 +94,6 @@ public class RemoteImService extends Service {
     final RemoteCallbackList<IConnectionCreationListener> mRemoteListeners
             = new RemoteCallbackList<IConnectionCreationListener>();
 
-    private AndroidHeartBeatService mHeartBeatService;
-
     private HashMap<Long, ImPluginInfo> mPlugins;
 
     public RemoteImService() {
@@ -113,6 +111,7 @@ public class RemoteImService extends Service {
         mNetworkConnectivityListener.startListening(this);
 
         findAvaiablePlugins();
+        AndroidSystemService.getInstance().initialize(this);
     }
 
     private void findAvaiablePlugins() {
@@ -266,14 +265,6 @@ public class RemoteImService extends Service {
         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);
@@ -328,21 +319,22 @@ public class RemoteImService extends Service {
         // 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;
@@ -354,6 +346,9 @@ public class RemoteImService extends Service {
         for (ImConnectionAdapter conn : mConnections) {
             conn.logout();
         }
+
+        AndroidSystemService.getInstance().shutdown();
+
         mNetworkConnectivityListener.unregisterHandler(mServiceHandler);
         mNetworkConnectivityListener.stopListening();
         mNetworkConnectivityListener = null;
@@ -397,7 +392,6 @@ public class RemoteImService extends Service {
         ConnectionFactory factory = ConnectionFactory.getInstance();
         try {
             ImConnection conn = factory.createConnection(config);
-            conn.setHeartBeatService(getHeartBeatService());
             ImConnectionAdapter result = new ImConnectionAdapter(providerId,
                     conn, this);
             mConnections.add(result);