summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:43 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:43 -0800
commit97196e67e000586b0db0b4646334f45925b33fff (patch)
tree586bb7c733c23ef6e76ad1566a449c9d4b45dfe9
parent8420f0b2fed10209358ec58b244e16c51fb8cf7d (diff)
downloadImProvider-97196e67e000586b0db0b4646334f45925b33fff.tar.gz
auto import from //depot/cupcake/@135843
-rw-r--r--Android.mk16
-rw-r--r--AndroidManifest.xml47
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE190
-rw-r--r--res/color/landing_page_text.xml22
-rw-r--r--res/color/landing_page_text_secondary.xml22
-rw-r--r--res/drawable/bubble.xml25
-rw-r--r--res/drawable/default_background.9.pngbin0 -> 2893 bytes
-rw-r--r--res/drawable/ic_launcher_im.pngbin0 -> 2951 bytes
-rw-r--r--res/drawable/im_bubble_highlight.9.pngbin0 -> 1810 bytes
-rw-r--r--res/drawable/im_bubble_normal.9.pngbin0 -> 1080 bytes
-rw-r--r--res/drawable/im_bubble_pressed.9.pngbin0 -> 1686 bytes
-rw-r--r--res/drawable/imlogo_s.pngbin0 -> 1912 bytes
-rw-r--r--res/layout/account_view.xml92
-rw-r--r--res/values-cs/strings.xml37
-rw-r--r--res/values-de/strings.xml37
-rw-r--r--res/values-es/strings.xml37
-rw-r--r--res/values-fr/strings.xml37
-rw-r--r--res/values-it/strings.xml37
-rw-r--r--res/values-ja/strings.xml37
-rw-r--r--res/values-ko/strings.xml37
-rw-r--r--res/values-nb/strings.xml47
-rw-r--r--res/values-nl/strings.xml37
-rw-r--r--res/values-pl/strings.xml37
-rw-r--r--res/values-ru/strings.xml37
-rw-r--r--res/values-zh-rCN/strings.xml37
-rw-r--r--res/values-zh-rTW/strings.xml37
-rw-r--r--res/values/strings.xml67
-rw-r--r--src/com/android/providers/im/BrandingResources.java160
-rw-r--r--src/com/android/providers/im/ImProvider.java2764
-rw-r--r--src/com/android/providers/im/LandingPage.java710
-rw-r--r--src/com/android/providers/im/ProviderListItem.java211
32 files changed, 4817 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..4c2023a
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_JAVA_LIBRARIES := ext \
+ com.android.im.plugin # TODO: remove this and load this on demand.
+ # (HACK: include this so we can load the
+ # classes defined in this plugin package)
+
+LOCAL_PACKAGE_NAME := ImProvider
+LOCAL_CERTIFICATE := shared
+
+include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..23b4958
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.im"
+ android:sharedUserId="android.uid.shared">
+
+ <permission android:name="com.android.providers.im.permission.READ_ONLY"
+ android:permissionGroup="android.permission-group.MESSAGES"
+ android:protectionLevel="dangerous"
+ android:label="@string/ro_perm_label"
+ android:description="@string/ro_perm_desc" />
+
+ <permission android:name="com.android.providers.im.permission.WRITE_ONLY"
+ android:permissionGroup="android.permission-group.MESSAGES"
+ android:protectionLevel="dangerous"
+ android:label="@string/wo_perm_label"
+ android:description="@string/wo_perm_desc" />
+
+ <uses-permission android:name="com.android.providers.im.permission.READ_ONLY" />
+ <uses-permission android:name="com.android.providers.im.permission.WRITE_ONLY" />
+
+ <application android:process="android.process.acore"
+ android:label="@string/im_label"
+ android:icon="@drawable/ic_launcher_im"
+ android:taskAffinity="android.task.im">
+ <!-- TODO: remove this library include. It's a hack so we can load its classes -->
+ <uses-library android:name="com.android.im.plugin" />
+
+ <provider android:name="ImProvider" android:authorities="im"
+ android:multiprocess="false"
+ android:readPermission="com.android.providers.im.permission.READ_ONLY"
+ android:writePermission="com.android.providers.im.permission.WRITE_ONLY"
+ android:grantUriPermissions="true" />
+
+ <activity android:name=".LandingPage">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="vnd.android.cursor.dir/im-providers" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-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.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/res/color/landing_page_text.xml b/res/color/landing_page_text.xml
new file mode 100644
index 0000000..6e967d9
--- /dev/null
+++ b/res/color/landing_page_text.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:color="#ff000000"/>
+ <item android:state_selected="true" android:color="#ff000000"/>
+ <item android:state_pressed="true" android:color="#ff000000"/>
+ <item android:color="#ffffffff"/>
+</selector>
diff --git a/res/color/landing_page_text_secondary.xml b/res/color/landing_page_text_secondary.xml
new file mode 100644
index 0000000..78cc390
--- /dev/null
+++ b/res/color/landing_page_text_secondary.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:color="#ff000000"/>
+ <item android:state_selected="true" android:color="#ff000000"/>
+ <item android:state_pressed="true" android:color="#ff000000"/>
+ <item android:color="#ffbebebe"/>
+</selector>
diff --git a/res/drawable/bubble.xml b/res/drawable/bubble.xml
new file mode 100644
index 0000000..38cff25
--- /dev/null
+++ b/res/drawable/bubble.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* bubble_with_chats.xml
+**
+** Copyright 2009, Google Inc.
+**
+** 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.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:drawable="@drawable/im_bubble_pressed" />
+ <item android:state_selected="true" android:drawable="@drawable/im_bubble_highlight" />
+ <item android:state_enabled="false" android:drawable="@drawable/im_bubble_normal" />
+ <item android:drawable="@drawable/im_bubble_normal" />
+</selector>
diff --git a/res/drawable/default_background.9.png b/res/drawable/default_background.9.png
new file mode 100644
index 0000000..33cb551
--- /dev/null
+++ b/res/drawable/default_background.9.png
Binary files differ
diff --git a/res/drawable/ic_launcher_im.png b/res/drawable/ic_launcher_im.png
new file mode 100644
index 0000000..afc35a2
--- /dev/null
+++ b/res/drawable/ic_launcher_im.png
Binary files differ
diff --git a/res/drawable/im_bubble_highlight.9.png b/res/drawable/im_bubble_highlight.9.png
new file mode 100644
index 0000000..9b5588a
--- /dev/null
+++ b/res/drawable/im_bubble_highlight.9.png
Binary files differ
diff --git a/res/drawable/im_bubble_normal.9.png b/res/drawable/im_bubble_normal.9.png
new file mode 100644
index 0000000..a9b327c
--- /dev/null
+++ b/res/drawable/im_bubble_normal.9.png
Binary files differ
diff --git a/res/drawable/im_bubble_pressed.9.png b/res/drawable/im_bubble_pressed.9.png
new file mode 100644
index 0000000..3933268
--- /dev/null
+++ b/res/drawable/im_bubble_pressed.9.png
Binary files differ
diff --git a/res/drawable/imlogo_s.png b/res/drawable/imlogo_s.png
new file mode 100644
index 0000000..b7aa43a
--- /dev/null
+++ b/res/drawable/imlogo_s.png
Binary files differ
diff --git a/res/layout/account_view.xml b/res/layout/account_view.xml
new file mode 100644
index 0000000..46bdecf
--- /dev/null
+++ b/res/layout/account_view.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+
+<com.android.providers.im.ProviderListItem
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:id="@+id/providerIcon"
+ android:layout_gravity="center_vertical"
+ android:scaleType="fitXY"
+ android:paddingLeft="5dip"
+ android:paddingRight="2dip"
+ android:layout_width="39dip"
+ android:layout_height="32dip"/>
+
+ <LinearLayout
+ android:id="@+id/underBubble"
+ android:orientation="horizontal"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/providerName"
+ android:textColor="@color/landing_page_text"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textStyle="bold"
+ android:maxLines="1"
+ android:layout_weight="0"
+ android:ellipsize="marquee"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TextView android:id="@+id/conversations"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/landing_page_text_secondary"
+ android:maxLines="1"
+ android:layout_weight="1"
+ android:paddingRight="3dip"
+ android:paddingLeft="5dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <TextView android:id="@+id/loginName"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/landing_page_text"
+ android:ellipsize="marquee"
+ android:maxLines="1"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/statusIcon"
+ android:scaleType="fitXY"
+ android:paddingRight="6dip"
+ android:paddingLeft="6dip"
+ android:layout_weight="0"
+ android:layout_gravity="center_vertical"
+ android:layout_width="30dip"
+ android:layout_height="18dip"/>
+ </LinearLayout>
+</com.android.providers.im.ProviderListItem>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644
index 0000000..bb35717
--- /dev/null
+++ b/res/values-cs/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"číst zprávy chatu"</string>
+ <string name="ro_perm_desc">"Povoluje aplikacím číst data z poskytovatele obsahu chatu."</string>
+ <string name="wo_perm_label">"chatovat"</string>
+ <string name="wo_perm_desc">"Povoluje aplikacím zapisovat data do poskytovatele obsahu chatových zpráv."</string>
+ <string name="im_label">"Chat"</string>
+ <string name="landing_page_title">"Chat – vyberte účet"</string>
+ <string name="menu_add_account">"Přidat účet"</string>
+ <string name="menu_edit_account">"Upravit účet"</string>
+ <string name="menu_remove_account">"Odstranit účet"</string>
+ <string name="sign_in">"Přihlásit se"</string>
+ <string name="menu_sign_out">"Odhlásit se"</string>
+ <string name="menu_settings">"Nastavení"</string>
+ <string name="menu_sign_out_all">"Odhlásit se ze všech služeb"</string>
+ <string name="choose_account_title">"Chat – vyberte účet"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Přidat účet <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
+ <string name="menu_view_contact_list">"Seznam kontaktů"</string>
+ <string name="signing_in_wait">"Přihlašování..."</string>
+</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
new file mode 100644
index 0000000..a0f61fe
--- /dev/null
+++ b/res/values-de/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"Instant Messages lesen"</string>
+ <string name="ro_perm_desc">"Ermöglicht Anwendungen das Lesen von Daten des IM-Content-Providers."</string>
+ <string name="wo_perm_label">"Instant Messages verfassen"</string>
+ <string name="wo_perm_desc">"Ermöglicht Anwendungen das Schreiben von Daten an den IM-Content-Provider."</string>
+ <string name="im_label">"IM"</string>
+ <string name="landing_page_title">"Chat - Konto auswählen"</string>
+ <string name="menu_add_account">"Konto hinzufügen"</string>
+ <string name="menu_edit_account">"Konto bearbeiten"</string>
+ <string name="menu_remove_account">"Konto entfernen"</string>
+ <string name="sign_in">"Anmelden"</string>
+ <string name="menu_sign_out">"Abmelden"</string>
+ <string name="menu_settings">"Einstellungen"</string>
+ <string name="menu_sign_out_all">"Alle abmelden"</string>
+ <string name="choose_account_title">"Chat - Konto auswählen"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"<xliff:g id="ACCOUNT">%1$s</xliff:g>-Konto hinzufügen"</string>
+ <string name="menu_view_contact_list">"Kontaktliste"</string>
+ <string name="signing_in_wait">"Anmeldung..."</string>
+</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
new file mode 100644
index 0000000..128c806
--- /dev/null
+++ b/res/values-es/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"leer mensajes instantáneos"</string>
+ <string name="ro_perm_desc">"Permite que las aplicaciones lean datos del proveedor de contenido de MI."</string>
+ <string name="wo_perm_label">"escribir mensajes instantáneos"</string>
+ <string name="wo_perm_desc">"Permite que las aplicaciones escriban datos en el proveedor de contenido de MI."</string>
+ <string name="im_label">"MI"</string>
+ <string name="landing_page_title">"Chat: seleccionar una cuenta"</string>
+ <string name="menu_add_account">"Añadir cuenta"</string>
+ <string name="menu_edit_account">"Editar cuenta"</string>
+ <string name="menu_remove_account">"Eliminar cuenta"</string>
+ <string name="sign_in">"Acceder"</string>
+ <string name="menu_sign_out">"Salir"</string>
+ <string name="menu_settings">"Configuración"</string>
+ <string name="menu_sign_out_all">"Salir de todo"</string>
+ <string name="choose_account_title">"Chat: seleccionar una cuenta"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Añadir cuenta <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
+ <string name="menu_view_contact_list">"Lista de contactos"</string>
+ <string name="signing_in_wait">"Accediendo..."</string>
+</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
new file mode 100644
index 0000000..3da7c82
--- /dev/null
+++ b/res/values-fr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"Lecture des messages instantanés"</string>
+ <string name="ro_perm_desc">"Permet aux applications de lire les données du prestataire de contenu de messagerie instantanée."</string>
+ <string name="wo_perm_label">"écrire des messages instantanées"</string>
+ <string name="wo_perm_desc">"Permet aux applications d\'écrire les données pour le prestataire de contenu de messagerie instantanée."</string>
+ <string name="im_label">"Chat"</string>
+ <string name="landing_page_title">"Chat : sélectionner un compte"</string>
+ <string name="menu_add_account">"Ajouter un compte"</string>
+ <string name="menu_edit_account">"Modifier un compte"</string>
+ <string name="menu_remove_account">"Supprimer un compte"</string>
+ <string name="sign_in">"Connexion"</string>
+ <string name="menu_sign_out">"Déconnexion"</string>
+ <string name="menu_settings">"Paramètres"</string>
+ <string name="menu_sign_out_all">"Se déconnecter de tout"</string>
+ <string name="choose_account_title">"Chat : sélectionner un compte"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Ajouter le compte <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
+ <string name="menu_view_contact_list">"Liste de contacts"</string>
+ <string name="signing_in_wait">"Connexion..."</string>
+</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
new file mode 100644
index 0000000..70065fb
--- /dev/null
+++ b/res/values-it/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"leggere messaggi immediati"</string>
+ <string name="ro_perm_desc">"Consente alle applicazioni di leggere dati del provider di contenuti IM."</string>
+ <string name="wo_perm_label">"scrivere messaggi immediati"</string>
+ <string name="wo_perm_desc">"Consente alle applicazioni di scrivere dati per il provider di contenuti IM."</string>
+ <string name="im_label">"Chat"</string>
+ <string name="landing_page_title">"Chat - Seleziona un account"</string>
+ <string name="menu_add_account">"Aggiungi account"</string>
+ <string name="menu_edit_account">"Modifica account"</string>
+ <string name="menu_remove_account">"Rimuovi account"</string>
+ <string name="sign_in">"Accedi"</string>
+ <string name="menu_sign_out">"Esci"</string>
+ <string name="menu_settings">"Impostazioni"</string>
+ <string name="menu_sign_out_all">"Esci da tutto"</string>
+ <string name="choose_account_title">"Chat - Seleziona un account"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Aggiungi account <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
+ <string name="menu_view_contact_list">"Elenco contatti"</string>
+ <string name="signing_in_wait">"Accesso..."</string>
+</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
new file mode 100644
index 0000000..89c5700
--- /dev/null
+++ b/res/values-ja/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"インスタントメッセージを表示"</string>
+ <string name="ro_perm_desc">"アプリケーションでIMコンテンツプロバイダからデータを読み取れるようにします。"</string>
+ <string name="wo_perm_label">"インスタントメッセージを作成"</string>
+ <string name="wo_perm_desc">"アプリケーションからIMコンテンツプロバイダにデータを書き込めるようにします。"</string>
+ <string name="im_label">"IM"</string>
+ <string name="landing_page_title">"チャット-アカウントを選択"</string>
+ <string name="menu_add_account">"アカウントを追加"</string>
+ <string name="menu_edit_account">"アカウントを編集"</string>
+ <string name="menu_remove_account">"アカウントを削除"</string>
+ <string name="sign_in">"ログイン"</string>
+ <string name="menu_sign_out">"ログアウト"</string>
+ <string name="menu_settings">"設定"</string>
+ <string name="menu_sign_out_all">"すべてログアウト"</string>
+ <string name="choose_account_title">"チャット-アカウントを選択"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"<xliff:g id="ACCOUNT">%1$s</xliff:g>アカウントを追加"</string>
+ <string name="menu_view_contact_list">"連絡先リスト"</string>
+ <string name="signing_in_wait">"ログイン中..."</string>
+</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
new file mode 100644
index 0000000..b3395e6
--- /dev/null
+++ b/res/values-ko/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"인스턴트 메시지 읽기"</string>
+ <string name="ro_perm_desc">"채팅 콘텐츠 제공업체에서 제공한 데이터를 읽을 수 있는 권한을 응용프로그램에 부여합니다."</string>
+ <string name="wo_perm_label">"인스턴트 메시지 쓰기"</string>
+ <string name="wo_perm_desc">"채팅 콘텐츠 제공업체에 데이터를 쓸 수 있는 권한을 응용프로그램에 부여합니다."</string>
+ <string name="im_label">"채팅"</string>
+ <string name="landing_page_title">"채팅 - 계정 선택"</string>
+ <string name="menu_add_account">"계정 추가"</string>
+ <string name="menu_edit_account">"계정 수정"</string>
+ <string name="menu_remove_account">"계정 삭제"</string>
+ <string name="sign_in">"로그인"</string>
+ <string name="menu_sign_out">"로그아웃"</string>
+ <string name="menu_settings">"설정"</string>
+ <string name="menu_sign_out_all">"모두 로그아웃"</string>
+ <string name="choose_account_title">"채팅 - 계정 선택"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"<xliff:g id="ACCOUNT">%1$s</xliff:g> 계정 추가"</string>
+ <string name="menu_view_contact_list">"연락처 목록"</string>
+ <string name="signing_in_wait">"로그인하는 중..."</string>
+</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
new file mode 100644
index 0000000..1f22b81
--- /dev/null
+++ b/res/values-nb/strings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for ro_perm_label (8923213594676670163) -->
+ <skip />
+ <!-- no translation found for ro_perm_desc (6154703403388711809) -->
+ <skip />
+ <!-- no translation found for wo_perm_label (4771652386754813294) -->
+ <skip />
+ <!-- no translation found for wo_perm_desc (470416777693330370) -->
+ <skip />
+ <string name="im_label">"Nettprat"</string>
+ <!-- no translation found for landing_page_title (2177721703095999384) -->
+ <skip />
+ <!-- no translation found for menu_add_account (4184762314855405486) -->
+ <skip />
+ <!-- no translation found for menu_edit_account (6585077522972888350) -->
+ <skip />
+ <!-- no translation found for menu_remove_account (8013513817623405880) -->
+ <skip />
+ <string name="sign_in">"Logg inn"</string>
+ <string name="menu_sign_out">"Logg ut"</string>
+ <string name="menu_settings">"Innstillinger"</string>
+ <!-- no translation found for menu_sign_out_all (945106187701517280) -->
+ <skip />
+ <!-- no translation found for choose_account_title (3179220583433723296) -->
+ <skip />
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Legg til <xliff:g id="ACCOUNT">%1$s</xliff:g>-konto"</string>
+ <string name="menu_view_contact_list">"Kontaktliste"</string>
+ <string name="signing_in_wait">"Logger inn…"</string>
+</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
new file mode 100644
index 0000000..dbaebbb
--- /dev/null
+++ b/res/values-nl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"chatberichten lezen"</string>
+ <string name="ro_perm_desc">"Toepassingen toestaan gegevens te lezen van de provider van chatinhoud."</string>
+ <string name="wo_perm_label">"chatberichten schrijven"</string>
+ <string name="wo_perm_desc">"Toepassingen toestaan gegevens te schrijven naar de provider van chatinhoud."</string>
+ <string name="im_label">"Chat"</string>
+ <string name="landing_page_title">"Chatten - Een account selecteren"</string>
+ <string name="menu_add_account">"Account toevoegen"</string>
+ <string name="menu_edit_account">"Account bewerken"</string>
+ <string name="menu_remove_account">"Account verwijderen"</string>
+ <string name="sign_in">"Aanmelden"</string>
+ <string name="menu_sign_out">"Afmelden"</string>
+ <string name="menu_settings">"Instellingen"</string>
+ <string name="menu_sign_out_all">"Overal afmelden"</string>
+ <string name="choose_account_title">"Chatten - Een account selecteren"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"<xliff:g id="ACCOUNT">%1$s</xliff:g>-account toevoegen"</string>
+ <string name="menu_view_contact_list">"Lijst met contactpersonen"</string>
+ <string name="signing_in_wait">"Aanmelden..."</string>
+</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
new file mode 100644
index 0000000..95538ba
--- /dev/null
+++ b/res/values-pl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"odczytaj wiadomości komunikatora"</string>
+ <string name="ro_perm_desc">"Zezwalaj aplikacjom na odczytywanie danych dostawcy zawartości komunikatora"</string>
+ <string name="wo_perm_label">"zapisz wiadomości komunikatora"</string>
+ <string name="wo_perm_desc">"Zezwalaj aplikacjom na zapisywanie danych do dostawcy zawartości komunikatora"</string>
+ <string name="im_label">"Komunikator"</string>
+ <string name="landing_page_title">"Czat: Wybierz konto"</string>
+ <string name="menu_add_account">"Dodaj konto"</string>
+ <string name="menu_edit_account">"Edytuj konto"</string>
+ <string name="menu_remove_account">"Usuń konto"</string>
+ <string name="sign_in">"Zaloguj się"</string>
+ <string name="menu_sign_out">"Wyloguj się"</string>
+ <string name="menu_settings">"Ustawienia"</string>
+ <string name="menu_sign_out_all">"Wyloguj wszystkie"</string>
+ <string name="choose_account_title">"Czat: Wybierz konto"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Dodaj konto <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
+ <string name="menu_view_contact_list">"Lista kontaktów"</string>
+ <string name="signing_in_wait">"Trwa logowanie..."</string>
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
new file mode 100644
index 0000000..994b47e
--- /dev/null
+++ b/res/values-ru/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"читать мгновенные сообщения"</string>
+ <string name="ro_perm_desc">"Позволяет приложениям считавать данные через поставщика чата."</string>
+ <string name="wo_perm_label">"писать мгновенные сообщения"</string>
+ <string name="wo_perm_desc">"Позволяет приложениям записывать данные через поставщика чата."</string>
+ <string name="im_label">"Чат"</string>
+ <string name="landing_page_title">"Чат – выберите аккаунт"</string>
+ <string name="menu_add_account">"Добавить аккаунт"</string>
+ <string name="menu_edit_account">"Изменить аккаунт"</string>
+ <string name="menu_remove_account">"Удалить аккаунт"</string>
+ <string name="sign_in">"Вход"</string>
+ <string name="menu_sign_out">"Выход"</string>
+ <string name="menu_settings">"Настройки"</string>
+ <string name="menu_sign_out_all">"Выйти отовсюду"</string>
+ <string name="choose_account_title">"Чат – выберите аккаунт"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"Добавить аккаунт <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
+ <string name="menu_view_contact_list">"Список контактов"</string>
+ <string name="signing_in_wait">"Выполняется вход..."</string>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..255f29f
--- /dev/null
+++ b/res/values-zh-rCN/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"阅读即时消息"</string>
+ <string name="ro_perm_desc">"允许应用程序从即时消息内容提供者处读取数据。"</string>
+ <string name="wo_perm_label">"编写即时消息"</string>
+ <string name="wo_perm_desc">"允许应用程序向即时消息内容提供者写入数据。"</string>
+ <string name="im_label">"即时消息"</string>
+ <string name="landing_page_title">"聊天 - 选择一个帐户"</string>
+ <string name="menu_add_account">"添加帐户"</string>
+ <string name="menu_edit_account">"编辑帐户"</string>
+ <string name="menu_remove_account">"删除帐户"</string>
+ <string name="sign_in">"登录"</string>
+ <string name="menu_sign_out">"登出"</string>
+ <string name="menu_settings">"设置"</string>
+ <string name="menu_sign_out_all">"全部登出"</string>
+ <string name="choose_account_title">"聊天 - 选择一个帐户"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"添加 <xliff:g id="ACCOUNT">%1$s</xliff:g> 帐户"</string>
+ <string name="menu_view_contact_list">"联系人列表"</string>
+ <string name="signing_in_wait">"正在登录..."</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..713fc19
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">"讀取即時訊息"</string>
+ <string name="ro_perm_desc">"允許應用程式讀取來自即時訊息內容提供者的資料。"</string>
+ <string name="wo_perm_label">"撰寫即時訊息"</string>
+ <string name="wo_perm_desc">"允許應用程式將資料寫入即時訊息內容提供者。"</string>
+ <string name="im_label">"即時訊息"</string>
+ <string name="landing_page_title">"即時通訊 - 選取帳戶"</string>
+ <string name="menu_add_account">"新增帳戶"</string>
+ <string name="menu_edit_account">"編輯帳戶"</string>
+ <string name="menu_remove_account">"移除帳戶"</string>
+ <string name="sign_in">"登入"</string>
+ <string name="menu_sign_out">"登出"</string>
+ <string name="menu_settings">"設定"</string>
+ <string name="menu_sign_out_all">"全部登出"</string>
+ <string name="choose_account_title">"即時通訊 - 選取帳戶"</string>
+ <!-- no translation found for conversations (6809253595345281731) -->
+ <skip />
+ <string name="add_account">"新增 <xliff:g id="ACCOUNT">%1$s</xliff:g> 帳戶"</string>
+ <string name="menu_view_contact_list">"通訊錄"</string>
+ <string name="signing_in_wait">"登入中…"</string>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..2519476
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2007, 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ro_perm_label">read instant messages</string>
+ <string name="ro_perm_desc">
+ Allows applications to read data from the IM content provider.
+ </string>
+
+ <string name="wo_perm_label">write instant messages</string>
+ <string name="wo_perm_desc">
+ Allows applications to write data to the IM content provider.
+ </string>
+
+ <!-- The application label -->
+ <string name="im_label">IM</string>
+
+ <!-- These strings displayed on the landing page. -->
+ <!-- The title of the landing page.-->
+ <string name="landing_page_title">Chat - Select an account</string>
+
+ <!-- Landing page screen menu and context menu items. -->
+ <!-- Conext menu item: add a new account.-->
+ <string name="menu_add_account">Add account</string>
+ <!-- Conext menu item: edit an account.-->
+ <string name="menu_edit_account">Edit account</string>
+ <!-- Conext menu item: remove an account.-->
+ <string name="menu_remove_account">Remove account</string>
+ <!-- Context menu item: sign into service. -->
+ <string name="sign_in">Sign in</string>
+ <!-- Context menu item: sign out the service.-->
+ <string name="menu_sign_out">Sign out</string>
+ <!-- Context menu item: go to the setting page.-->
+ <string name="menu_settings">Settings</string>
+ <!-- Screen menu item: sign out all service.-->
+ <string name="menu_sign_out_all">Sign out all</string>
+
+ <!-- These strings displayed on the landing page. -->
+ <!-- The title of the landing page.-->
+ <string name="choose_account_title">Chat - Select an account</string>
+ <!-- Displays the number of ongoing chats on the landing page.-->
+ <string name="conversations">(%1$d)</string>
+ <!-- The add account label on the landing page if there isn't any account.-->
+ <string name="add_account">Add <xliff:g id="account">%1$s</xliff:g> account</string>
+
+ <!-- Screen menu item: go the contact list screen. May be overrided by the plugin.-->
+ <string name="menu_view_contact_list">Contact list</string>
+
+ <!-- Connection status -->
+ <string name="signing_in_wait">Signing in\u2026</string>
+
+</resources>
diff --git a/src/com/android/providers/im/BrandingResources.java b/src/com/android/providers/im/BrandingResources.java
new file mode 100644
index 0000000..e232a96
--- /dev/null
+++ b/src/com/android/providers/im/BrandingResources.java
@@ -0,0 +1,160 @@
+/*
+ * 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.providers.im;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Map;
+
+/**
+ * The provider specific branding resources.
+ */
+public class BrandingResources {
+ private static final String TAG = "IM";
+ private static final boolean LOCAL_DEBUG = false;
+
+ private Map<Integer, Integer> mResMapping;
+ private Resources mPackageRes;
+
+ private BrandingResources mDefaultRes;
+
+ /**
+ * Creates a new BrandingResource of a specific plug-in. The resources will
+ * be retrieved from the plug-in package.
+ *
+ * @param context The current application context.
+ * @param pluginInfo The info about the plug-in.
+ * @param provider the name of the IM service provider.
+ * @param defaultRes The default branding resources. If the resource is not
+ * found in the plug-in, the default resource will be returned.
+ */
+ public BrandingResources(Context context, LandingPage.PluginInfo pluginInfo, String provider,
+ BrandingResources defaultRes) {
+ String packageName = null;
+ mDefaultRes = defaultRes;
+
+ try {
+ mResMapping = pluginInfo.mPlugin.getResourceMapForProvider(provider);
+ packageName = pluginInfo.mPlugin.getResourcePackageNameForProvider(provider);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ }
+
+ if (packageName == null) {
+ packageName = pluginInfo.mPackageName;
+ }
+
+ PackageManager pm = context.getPackageManager();
+ try {
+ if (LOCAL_DEBUG) log("load resources from " + packageName);
+ mPackageRes = pm.getResourcesForApplication(packageName);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Can not load resources from " + packageName);
+ }
+ }
+
+ /**
+ * Creates a BrandingResource with application context and the resource ID map.
+ * The resource will be retrieved from the context directly instead from the plug-in package.
+ *
+ * @param context
+ * @param resMapping
+ */
+ public BrandingResources(Context context, Map<Integer, Integer> resMapping,
+ BrandingResources defaultRes) {
+ mPackageRes = context.getResources();
+ mResMapping = resMapping;
+ mDefaultRes = defaultRes;
+ }
+
+ /**
+ * Gets a drawable object associated with a particular resource ID defined
+ * in {@link com.android.im.plugin.BrandingResourceIDs}
+ *
+ * @param id The ID defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ * @return Drawable An object that can be used to draw this resource.
+ */
+ public Drawable getDrawable(int id) {
+ int resId = getPackageResourceId(id);
+ if (resId != 0) {
+ return mPackageRes.getDrawable(resId);
+ } else if (mDefaultRes != null){
+ return mDefaultRes.getDrawable(id);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the string value associated with a particular resource ID defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ *
+ * @param id The ID of the string resource defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ * @param formatArgs The format arguments that will be used for
+ * substitution.
+ * @return The string data associated with the resource
+ */
+ public String getString(int id, Object... formatArgs) {
+ int resId = getPackageResourceId(id);
+ if (resId != 0) {
+ return mPackageRes.getString(resId, formatArgs);
+ } else if (mDefaultRes != null){
+ return mDefaultRes.getString(id, formatArgs);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the string array associated with a particular resource ID defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ *
+ * @param id The ID of the string resource defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ * @return The string array associated with the resource.
+ */
+ public String[] getStringArray(int id) {
+ int resId = getPackageResourceId(id);
+ if (resId != 0) {
+ return mPackageRes.getStringArray(resId);
+ } else if (mDefaultRes != null){
+ return mDefaultRes.getStringArray(id);
+ } else {
+ return null;
+ }
+ }
+
+ private int getPackageResourceId(int id) {
+ if (mResMapping == null || mPackageRes == null) {
+ return 0;
+ }
+ Integer resId = mResMapping.get(id);
+ return resId == null ? 0 : resId;
+ }
+
+ private void log(String msg) {
+ Log.d(TAG, "[BrandingRes] " + msg);
+ }
+}
diff --git a/src/com/android/providers/im/ImProvider.java b/src/com/android/providers/im/ImProvider.java
new file mode 100644
index 0000000..9a23cc8
--- /dev/null
+++ b/src/com/android/providers/im/ImProvider.java
@@ -0,0 +1,2764 @@
+/*
+ * Copyright (C) 2007 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.providers.im;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.Im;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A content provider for IM
+ */
+public class ImProvider extends ContentProvider {
+ private static final String LOG_TAG = "imProvider";
+ private static final boolean DBG = false;
+
+ private static final String AUTHORITY = "im";
+
+ private static final boolean USE_CONTACT_PRESENCE_TRIGGER = false;
+
+ private static final String TABLE_ACCOUNTS = "accounts";
+ private static final String TABLE_PROVIDERS = "providers";
+ private static final String TABLE_PROVIDER_SETTINGS = "providerSettings";
+
+ private static final String TABLE_CONTACTS = "contacts";
+ private static final String TABLE_CONTACTS_ETAG = "contactsEtag";
+ private static final String TABLE_BLOCKED_LIST = "blockedList";
+ private static final String TABLE_CONTACT_LIST = "contactList";
+ private static final String TABLE_INVITATIONS = "invitations";
+ private static final String TABLE_GROUP_MEMBERS = "groupMembers";
+ private static final String TABLE_GROUP_MESSAGES = "groupMessages";
+ private static final String TABLE_PRESENCE = "presence";
+ private static final String USERNAME = "username";
+ private static final String TABLE_CHATS = "chats";
+ private static final String TABLE_AVATARS = "avatars";
+ private static final String TABLE_SESSION_COOKIES = "sessionCookies";
+ private static final String TABLE_MESSAGES = "messages";
+ private static final String TABLE_OUTGOING_RMQ_MESSAGES = "outgoingRmqMessages";
+ private static final String TABLE_LAST_RMQ_ID = "lastrmqid";
+ private static final String TABLE_ACCOUNT_STATUS = "accountStatus";
+ private static final String TABLE_BRANDING_RESOURCE_MAP_CACHE = "brandingResMapCache";
+
+ private static final String DATABASE_NAME = "im.db";
+ private static final int DATABASE_VERSION = 47;
+
+ protected static final int MATCH_PROVIDERS = 1;
+ protected static final int MATCH_PROVIDERS_BY_ID = 2;
+ protected static final int MATCH_PROVIDERS_WITH_ACCOUNT = 3;
+ protected static final int MATCH_ACCOUNTS = 10;
+ protected static final int MATCH_ACCOUNTS_BY_ID = 11;
+ protected static final int MATCH_CONTACTS = 18;
+ protected static final int MATCH_CONTACTS_JOIN_PRESENCE = 19;
+ protected static final int MATCH_CONTACTS_BAREBONE = 20;
+ protected static final int MATCH_CHATTING_CONTACTS = 21;
+ protected static final int MATCH_CONTACTS_BY_PROVIDER = 22;
+ protected static final int MATCH_CHATTING_CONTACTS_BY_PROVIDER = 23;
+ protected static final int MATCH_NO_CHATTING_CONTACTS_BY_PROVIDER = 24;
+ protected static final int MATCH_ONLINE_CONTACTS_BY_PROVIDER = 25;
+ protected static final int MATCH_OFFLINE_CONTACTS_BY_PROVIDER = 26;
+ protected static final int MATCH_CONTACT = 27;
+ protected static final int MATCH_CONTACTS_BULK = 28;
+ protected static final int MATCH_ONLINE_CONTACT_COUNT = 30;
+ protected static final int MATCH_BLOCKED_CONTACTS = 31;
+ protected static final int MATCH_CONTACTLISTS = 32;
+ protected static final int MATCH_CONTACTLISTS_BY_PROVIDER = 33;
+ protected static final int MATCH_CONTACTLIST = 34;
+ protected static final int MATCH_BLOCKEDLIST = 35;
+ protected static final int MATCH_BLOCKEDLIST_BY_PROVIDER = 36;
+ protected static final int MATCH_CONTACTS_ETAGS = 37;
+ protected static final int MATCH_CONTACTS_ETAG = 38;
+ protected static final int MATCH_PRESENCE = 40;
+ protected static final int MATCH_PRESENCE_ID = 41;
+ protected static final int MATCH_PRESENCE_BY_ACCOUNT = 42;
+ protected static final int MATCH_PRESENCE_SEED_BY_ACCOUNT = 43;
+ protected static final int MATCH_PRESENCE_BULK = 44;
+ protected static final int MATCH_MESSAGES = 50;
+ protected static final int MATCH_MESSAGES_BY_CONTACT = 51;
+ protected static final int MATCH_MESSAGE = 52;
+ protected static final int MATCH_GROUP_MESSAGES = 53;
+ protected static final int MATCH_GROUP_MESSAGE_BY = 54;
+ protected static final int MATCH_GROUP_MESSAGE = 55;
+ protected static final int MATCH_GROUP_MEMBERS = 58;
+ protected static final int MATCH_GROUP_MEMBERS_BY_GROUP = 59;
+ protected static final int MATCH_AVATARS = 60;
+ protected static final int MATCH_AVATAR = 61;
+ protected static final int MATCH_AVATAR_BY_PROVIDER = 62;
+ protected static final int MATCH_CHATS = 70;
+ protected static final int MATCH_CHATS_BY_ACCOUNT = 71;
+ protected static final int MATCH_CHATS_ID = 72;
+ protected static final int MATCH_SESSIONS = 80;
+ protected static final int MATCH_SESSIONS_BY_PROVIDER = 81;
+ protected static final int MATCH_PROVIDER_SETTINGS = 90;
+ protected static final int MATCH_PROVIDER_SETTINGS_BY_ID = 91;
+ protected static final int MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME = 92;
+ protected static final int MATCH_INVITATIONS = 100;
+ protected static final int MATCH_INVITATION = 101;
+ protected static final int MATCH_OUTGOING_RMQ_MESSAGES = 110;
+ protected static final int MATCH_OUTGOING_RMQ_MESSAGE = 111;
+ protected static final int MATCH_OUTGOING_HIGHEST_RMQ_ID = 112;
+ protected static final int MATCH_LAST_RMQ_ID = 113;
+ protected static final int MATCH_ACCOUNTS_STATUS = 114;
+ protected static final int MATCH_ACCOUNT_STATUS = 115;
+ protected static final int MATCH_BRANDING_RESOURCE_MAP_CACHE = 120;
+
+
+ protected final UriMatcher mUrlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ private final String mTransientDbName;
+
+ private static final HashMap<String, String> sProviderAccountsProjectionMap;
+ private static final HashMap<String, String> sContactsProjectionMap;
+ private static final HashMap<String, String> sContactListProjectionMap;
+ private static final HashMap<String, String> sBlockedListProjectionMap;
+
+ private static final String PROVIDER_JOIN_ACCOUNT_TABLE =
+ "providers LEFT OUTER JOIN accounts ON " +
+ "(providers._id = accounts.provider AND accounts.active = 1) " +
+ "LEFT OUTER JOIN accountStatus ON (accounts._id = accountStatus.account)";
+
+
+ private static final String CONTACT_JOIN_PRESENCE_TABLE =
+ "contacts LEFT OUTER JOIN presence ON (contacts._id = presence.contact_id)";
+
+ private static final String CONTACT_JOIN_PRESENCE_CHAT_TABLE =
+ CONTACT_JOIN_PRESENCE_TABLE +
+ " LEFT OUTER JOIN chats ON (contacts._id = chats.contact_id)";
+
+ private static final String CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE =
+ CONTACT_JOIN_PRESENCE_CHAT_TABLE +
+ " LEFT OUTER JOIN avatars ON (contacts.username = avatars.contact" +
+ " AND contacts.account = avatars.account_id)";
+
+ private static final String BLOCKEDLIST_JOIN_AVATAR_TABLE =
+ "blockedList LEFT OUTER JOIN avatars ON (blockedList.username = avatars.contact" +
+ " AND blockedList.account = avatars.account_id)";
+
+ /**
+ * The where clause for filtering out blocked contacts
+ */
+ private static final String NON_BLOCKED_CONTACTS_WHERE_CLAUSE = "("
+ + Im.Contacts.TYPE + " IS NULL OR "
+ + Im.Contacts.TYPE + "!="
+ + String.valueOf(Im.Contacts.TYPE_BLOCKED)
+ + ")";
+
+ private static final String BLOCKED_CONTACTS_WHERE_CLAUSE =
+ "(contacts." + Im.Contacts.TYPE + "=" + Im.Contacts.TYPE_BLOCKED + ")";
+
+ private static final String CONTACT_ID = TABLE_CONTACTS + '.' + Im.Contacts._ID;
+ private static final String PRESENCE_CONTACT_ID = TABLE_PRESENCE + '.' + Im.Presence.CONTACT_ID;
+
+ protected SQLiteOpenHelper mOpenHelper;
+ private final String mDatabaseName;
+ private final int mDatabaseVersion;
+
+ private final String[] BACKFILL_PROJECTION = {
+ Im.Chats._ID, Im.Chats.SHORTCUT, Im.Chats.LAST_MESSAGE_DATE
+ };
+
+ private final String[] FIND_SHORTCUT_PROJECTION = {
+ Im.Chats._ID, Im.Chats.SHORTCUT
+ };
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+
+ DatabaseHelper(Context context) {
+ super(context, mDatabaseName, null, mDatabaseVersion);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+
+ if (DBG) log("##### bootstrapDatabase");
+
+ db.execSQL("CREATE TABLE " + TABLE_PROVIDERS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "name TEXT," + // eg AIM
+ "fullname TEXT," + // eg AOL Instance Messenger
+ "category TEXT," + // a category used for forming intent
+ "signup_url TEXT" + // web url to visit to create a new account
+ ");");
+
+ db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "name TEXT," +
+ "provider INTEGER," +
+ "username TEXT," +
+ "pw TEXT," +
+ "active INTEGER NOT NULL DEFAULT 0," +
+ "locked INTEGER NOT NULL DEFAULT 0," +
+ "keep_signed_in INTEGER NOT NULL DEFAULT 0," +
+ "last_login_state INTEGER NOT NULL DEFAULT 0," +
+ "UNIQUE (provider, username)" +
+ ");");
+
+ createContactsTables(db);
+
+ db.execSQL("CREATE TABLE " + TABLE_AVATARS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "contact TEXT," +
+ "provider_id INTEGER," +
+ "account_id INTEGER," +
+ "hash TEXT," +
+ "data BLOB," + // raw image data
+ "UNIQUE (account_id, contact)" +
+ ");");
+
+ db.execSQL("CREATE TABLE " + TABLE_PROVIDER_SETTINGS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "provider INTEGER," +
+ "name TEXT," +
+ "value TEXT," +
+ "UNIQUE (provider, name)" +
+ ");");
+
+ db.execSQL("create TABLE " + TABLE_OUTGOING_RMQ_MESSAGES + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "rmq_id INTEGER," +
+ "type INTEGER," +
+ "ts INTEGER," +
+ "data TEXT" +
+ ");");
+
+ db.execSQL("create TABLE " + TABLE_LAST_RMQ_ID + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "rmq_id INTEGER" +
+ ");");
+
+ db.execSQL("create TABLE " + TABLE_BRANDING_RESOURCE_MAP_CACHE + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "provider_id INTEGER," +
+ "app_res_id INTEGER," +
+ "plugin_res_id INTEGER" +
+ ");");
+
+ // clean up account specific data when an account is deleted.
+ db.execSQL("CREATE TRIGGER account_cleanup " +
+ "DELETE ON " + TABLE_ACCOUNTS +
+ " BEGIN " +
+ "DELETE FROM " + TABLE_AVATARS + " WHERE account_id= OLD._id;" +
+ "END");
+
+ // add a database trigger to clean up associated provider settings
+ // while deleting a provider
+ db.execSQL("CREATE TRIGGER provider_cleanup " +
+ "DELETE ON " + TABLE_PROVIDERS +
+ " BEGIN " +
+ "DELETE FROM " + TABLE_PROVIDER_SETTINGS + " WHERE provider= OLD._id;" +
+ "END");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.d(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
+
+ switch (oldVersion) {
+ case 43: // this is the db version shipped in Dream 1.0
+ // no-op: no schema changed from 43 to 44. The db version was changed to flush
+ // old provider settings, so new provider setting (including new name/value
+ // pairs) could be inserted by the plugins.
+
+ // follow thru.
+ case 44:
+ if (newVersion <= 44) {
+ return;
+ }
+
+ db.beginTransaction();
+ try {
+ // add category column to the providers table
+ db.execSQL("ALTER TABLE " + TABLE_PROVIDERS + " ADD COLUMN category TEXT;");
+ // add otr column to the contacts table
+ db.execSQL("ALTER TABLE " + TABLE_CONTACTS + " ADD COLUMN otr INTEGER;");
+
+ db.setTransactionSuccessful();
+ } catch (Throwable ex) {
+ Log.e(LOG_TAG, ex.getMessage(), ex);
+ break; // force to destroy all old data;
+ } finally {
+ db.endTransaction();
+ }
+
+ case 45:
+ if (newVersion <= 45) {
+ return;
+ }
+
+ db.beginTransaction();
+ try {
+ // add an otr_etag column to contact etag table
+ db.execSQL(
+ "ALTER TABLE " + TABLE_CONTACTS_ETAG + " ADD COLUMN otr_etag TEXT;");
+ db.setTransactionSuccessful();
+ } catch (Throwable ex) {
+ Log.e(LOG_TAG, ex.getMessage(), ex);
+ break; // force to destroy all old data;
+ } finally {
+ db.endTransaction();
+ }
+
+ case 46:
+ if (newVersion <= 46) {
+ return;
+ }
+
+ db.beginTransaction();
+ try {
+ // add branding resource map cache table
+ db.execSQL("create TABLE " + TABLE_BRANDING_RESOURCE_MAP_CACHE + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "provider_id INTEGER," +
+ "app_res_id INTEGER," +
+ "plugin_res_id INTEGER" +
+ ");");
+ db.setTransactionSuccessful();
+ } catch (Throwable ex) {
+ Log.e(LOG_TAG, ex.getMessage(), ex);
+ break; // force to destroy all old data;
+ } finally {
+ db.endTransaction();
+ }
+
+ return;
+ }
+
+ Log.w(LOG_TAG, "Couldn't upgrade db to " + newVersion + ". Destroying old data.");
+ destroyOldTables(db);
+ onCreate(db);
+ }
+
+ private void destroyOldTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDERS);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_ACCOUNTS);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACT_LIST);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS_ETAG);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_AVATARS);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDER_SETTINGS);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_OUTGOING_RMQ_MESSAGES);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_RMQ_ID);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_BRANDING_RESOURCE_MAP_CACHE);
+ }
+
+ private void createContactsTables(SQLiteDatabase db) {
+ StringBuilder buf = new StringBuilder();
+ String contactsTableName = TABLE_CONTACTS;
+
+ // creating the "contacts" table
+ buf.append("CREATE TABLE IF NOT EXISTS ");
+ buf.append(contactsTableName);
+ buf.append(" (");
+ buf.append("_id INTEGER PRIMARY KEY,");
+ buf.append("username TEXT,");
+ buf.append("nickname TEXT,");
+ buf.append("provider INTEGER,");
+ buf.append("account INTEGER,");
+ buf.append("contactList INTEGER,");
+ buf.append("type INTEGER,");
+ buf.append("subscriptionStatus INTEGER,");
+ buf.append("subscriptionType INTEGER,");
+
+ // the following are derived from Google Contact Extension, we don't include all
+ // the attributes, just the ones we can use.
+ // (see http://code.google.com/apis/talk/jep_extensions/roster_attributes.html)
+ //
+ // qc: quick contact (derived from message count)
+ // rejected: if the contact has ever been rejected by the user
+ buf.append("qc INTEGER,");
+ buf.append("rejected INTEGER,");
+
+ // Off the record status
+ buf.append("otr INTEGER");
+
+ buf.append(");");
+
+ db.execSQL(buf.toString());
+
+ buf.delete(0, buf.length());
+
+ // creating contact etag table
+ buf.append("CREATE TABLE IF NOT EXISTS ");
+ buf.append(TABLE_CONTACTS_ETAG);
+ buf.append(" (");
+ buf.append("_id INTEGER PRIMARY KEY,");
+ buf.append("etag TEXT,");
+ buf.append("otr_etag TEXT,");
+ buf.append("account INTEGER UNIQUE");
+ buf.append(");");
+
+ db.execSQL(buf.toString());
+
+ buf.delete(0, buf.length());
+
+ // creating the "contactList" table
+ buf.append("CREATE TABLE IF NOT EXISTS ");
+ buf.append(TABLE_CONTACT_LIST);
+ buf.append(" (");
+ buf.append("_id INTEGER PRIMARY KEY,");
+ buf.append("name TEXT,");
+ buf.append("provider INTEGER,");
+ buf.append("account INTEGER");
+ buf.append(");");
+
+ db.execSQL(buf.toString());
+
+ buf.delete(0, buf.length());
+
+ // creating the "blockedList" table
+ buf.append("CREATE TABLE IF NOT EXISTS ");
+ buf.append(TABLE_BLOCKED_LIST);
+ buf.append(" (");
+ buf.append("_id INTEGER PRIMARY KEY,");
+ buf.append("username TEXT,");
+ buf.append("nickname TEXT,");
+ buf.append("provider INTEGER,");
+ buf.append("account INTEGER");
+ buf.append(");");
+
+ db.execSQL(buf.toString());
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ if (db.isReadOnly()) {
+ Log.w(LOG_TAG, "ImProvider database opened in read only mode.");
+ Log.w(LOG_TAG, "Transient tables not created.");
+ return;
+ }
+
+ if (DBG) log("##### createTransientTables");
+
+ // Create transient tables
+ String cpDbName;
+ db.execSQL("ATTACH DATABASE ':memory:' AS " + mTransientDbName + ";");
+ cpDbName = mTransientDbName + ".";
+
+ // message table (since the UI currently doesn't require saving message history
+ // across IM sessions, store the message table in memory db only)
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_MESSAGES + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "packet_id TEXT UNIQUE," +
+ "contact TEXT," +
+ "provider INTEGER," +
+ "account INTEGER," +
+ "body TEXT," +
+ "date INTEGER," + // in seconds
+ "type INTEGER," +
+ "err_code INTEGER NOT NULL DEFAULT 0," +
+ "err_msg TEXT" +
+ ");");
+
+ // presence
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_PRESENCE + " ("+
+ "_id INTEGER PRIMARY KEY," +
+ "contact_id INTEGER UNIQUE," +
+ "jid_resource TEXT," + // jid resource for the presence
+ "client_type INTEGER," + // client type
+ "priority INTEGER," + // presence priority (XMPP)
+ "mode INTEGER," + // presence mode
+ "status TEXT" + // custom status
+ ");");
+
+ // group chat invitations
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_INVITATIONS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "providerId INTEGER," +
+ "accountId INTEGER," +
+ "inviteId TEXT," +
+ "sender TEXT," +
+ "groupName TEXT," +
+ "note TEXT," +
+ "status INTEGER" +
+ ");");
+
+ // group chat members
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_GROUP_MEMBERS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "groupId INTEGER," +
+ "username TEXT," +
+ "nickname TEXT" +
+ ");");
+
+ // group chat messages
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_GROUP_MESSAGES + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "packet_id TEXT UNIQUE," +
+ "contact TEXT," +
+ "groupId INTEGER," +
+ "body TEXT," +
+ "date INTEGER," +
+ "type INTEGER," +
+ "err_code INTEGER NOT NULL DEFAULT 0," +
+ "err_msg TEXT" +
+ ");");
+
+ // chat sessions, including single person chats and group chats
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_CHATS + " ("+
+ "_id INTEGER PRIMARY KEY," +
+ "contact_id INTEGER UNIQUE," +
+ "jid_resource TEXT," + // the JID resource for the user, only for non-group chats
+ "groupchat INTEGER," + // 1 if group chat, 0 if not TODO: remove this column
+ "last_unread_message TEXT," + // the last unread message
+ "last_message_date INTEGER," + // in seconds
+ "unsent_composed_message TEXT," + // a composed, but not sent message
+ "shortcut INTEGER" + // which of 10 slots (if any) this chat occupies
+ ");");
+
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_ACCOUNT_STATUS + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "account INTEGER UNIQUE," +
+ "presenceStatus INTEGER," +
+ "connStatus INTEGER" +
+ ");"
+ );
+
+ /* when we moved the contact table out of transient_db and into the main db, the
+ contact_cleanup and group_cleanup triggers don't work anymore. It seems we can't
+ create triggers that reference objects in a different database!
+
+ String contactsTableName = TABLE_CONTACTS;
+
+ if (USE_CONTACT_PRESENCE_TRIGGER) {
+ // Insert a default presence for newly inserted contact
+ db.execSQL("CREATE TRIGGER IF NOT EXISTS " + cpDbName + "contact_create_presence " +
+ "INSERT ON " + cpDbName + contactsTableName +
+ " FOR EACH ROW WHEN NEW.type != " + Im.Contacts.TYPE_GROUP +
+ " OR NEW.type != " + Im.Contacts.TYPE_BLOCKED +
+ " BEGIN " +
+ "INSERT INTO presence (contact_id) VALUES (NEW._id);" +
+ " END");
+ }
+
+ db.execSQL("CREATE TRIGGER IF NOT EXISTS " + cpDbName + "contact_cleanup " +
+ "DELETE ON " + cpDbName + contactsTableName +
+ " BEGIN " +
+ "DELETE FROM presence WHERE contact_id = OLD._id;" +
+ "DELETE FROM chats WHERE contact_id = OLD._id;" +
+ "END");
+
+ // Cleans up group members and group messages when a group chat is deleted
+ db.execSQL("CREATE TRIGGER IF NOT EXISTS " + cpDbName + "group_cleanup " +
+ "DELETE ON " + cpDbName + contactsTableName +
+ " FOR EACH ROW WHEN OLD.type = " + Im.Contacts.TYPE_GROUP +
+ " BEGIN " +
+ "DELETE FROM groupMembers WHERE groupId = OLD._id;" +
+ "DELETE FROM groupMessages WHERE groupId = OLD._id;" +
+ " END");
+ */
+
+ // only store the session cookies in memory right now. This means
+ // that we don't persist them across device reboot
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_SESSION_COOKIES + " ("+
+ "_id INTEGER PRIMARY KEY," +
+ "provider INTEGER," +
+ "account INTEGER," +
+ "name TEXT," +
+ "value TEXT" +
+ ");");
+
+ }
+ }
+
+ static {
+ sProviderAccountsProjectionMap = new HashMap<String, String>();
+ sProviderAccountsProjectionMap.put(Im.Provider._ID,
+ "providers._id AS _id");
+ sProviderAccountsProjectionMap.put(Im.Provider._COUNT,
+ "COUNT(*) AS _account");
+ sProviderAccountsProjectionMap.put(Im.Provider.NAME,
+ "providers.name AS name");
+ sProviderAccountsProjectionMap.put(Im.Provider.FULLNAME,
+ "providers.fullname AS fullname");
+ sProviderAccountsProjectionMap.put(Im.Provider.CATEGORY,
+ "providers.category AS category");
+ sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_ID,
+ "accounts._id AS account_id");
+ sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_USERNAME,
+ "accounts.username AS account_username");
+ sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_PW,
+ "accounts.pw AS account_pw");
+ sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_LOCKED,
+ "accounts.locked AS account_locked");
+ sProviderAccountsProjectionMap.put(Im.Provider.ACCOUNT_PRESENCE_STATUS,
+ "accountStatus.presenceStatus AS account_presenceStatus");
+ sProviderAccountsProjectionMap.put(Im.Provider.ACCOUNT_CONNECTION_STATUS,
+ "accountStatus.connStatus AS account_connStatus");
+
+ // contacts projection map
+ sContactsProjectionMap = new HashMap<String, String>();
+
+ // Base column
+ sContactsProjectionMap.put(Im.Contacts._ID, "contacts._id AS _id");
+ sContactsProjectionMap.put(Im.Contacts._COUNT, "COUNT(*) AS _count");
+
+ // contacts column
+ sContactsProjectionMap.put(Im.Contacts._ID, "contacts._id as _id");
+ sContactsProjectionMap.put(Im.Contacts.USERNAME, "contacts.username as username");
+ sContactsProjectionMap.put(Im.Contacts.NICKNAME, "contacts.nickname as nickname");
+ sContactsProjectionMap.put(Im.Contacts.PROVIDER, "contacts.provider as provider");
+ sContactsProjectionMap.put(Im.Contacts.ACCOUNT, "contacts.account as account");
+ sContactsProjectionMap.put(Im.Contacts.CONTACTLIST, "contacts.contactList as contactList");
+ sContactsProjectionMap.put(Im.Contacts.TYPE, "contacts.type as type");
+ sContactsProjectionMap.put(Im.Contacts.SUBSCRIPTION_STATUS,
+ "contacts.subscriptionStatus as subscriptionStatus");
+ sContactsProjectionMap.put(Im.Contacts.SUBSCRIPTION_TYPE,
+ "contacts.subscriptionType as subscriptionType");
+ sContactsProjectionMap.put(Im.Contacts.QUICK_CONTACT, "contacts.qc as qc");
+ sContactsProjectionMap.put(Im.Contacts.REJECTED, "contacts.rejected as rejected");
+
+ // Presence columns
+ sContactsProjectionMap.put(Im.Presence.CONTACT_ID,
+ "presence.contact_id AS contact_id");
+ sContactsProjectionMap.put(Im.Contacts.PRESENCE_STATUS,
+ "presence.mode AS mode");
+ sContactsProjectionMap.put(Im.Contacts.PRESENCE_CUSTOM_STATUS,
+ "presence.status AS status");
+ sContactsProjectionMap.put(Im.Contacts.CLIENT_TYPE,
+ "presence.client_type AS client_type");
+
+ // Chats columns
+ sContactsProjectionMap.put(Im.Contacts.CHATS_CONTACT,
+ "chats.contact_id AS chats_contact_id");
+ sContactsProjectionMap.put(Im.Chats.JID_RESOURCE,
+ "chats.jid_resource AS jid_resource");
+ sContactsProjectionMap.put(Im.Chats.GROUP_CHAT,
+ "chats.groupchat AS groupchat");
+ sContactsProjectionMap.put(Im.Contacts.LAST_UNREAD_MESSAGE,
+ "chats.last_unread_message AS last_unread_message");
+ sContactsProjectionMap.put(Im.Contacts.LAST_MESSAGE_DATE,
+ "chats.last_message_date AS last_message_date");
+ sContactsProjectionMap.put(Im.Contacts.UNSENT_COMPOSED_MESSAGE,
+ "chats.unsent_composed_message AS unsent_composed_message");
+ sContactsProjectionMap.put(Im.Contacts.SHORTCUT, "chats.SHORTCUT AS shortcut");
+
+ // Avatars columns
+ sContactsProjectionMap.put(Im.Contacts.AVATAR_HASH, "avatars.hash AS avatars_hash");
+ sContactsProjectionMap.put(Im.Contacts.AVATAR_DATA, "avatars.data AS avatars_data");
+
+ // contactList projection map
+ sContactListProjectionMap = new HashMap<String, String>();
+ sContactListProjectionMap.put(Im.ContactList._ID,
+ "contactList._id AS _id");
+ sContactListProjectionMap.put(Im.ContactList._COUNT,
+ "COUNT(*) AS _count");
+ sContactListProjectionMap.put(Im.ContactList.NAME, "name");
+ sContactListProjectionMap.put(Im.ContactList.PROVIDER, "provider");
+ sContactListProjectionMap.put(Im.ContactList.ACCOUNT, "account");
+
+ // blockedList projection map
+ sBlockedListProjectionMap = new HashMap<String, String>();
+ sBlockedListProjectionMap.put(Im.BlockedList._ID,
+ "blockedList._id AS _id");
+ sBlockedListProjectionMap.put(Im.BlockedList._COUNT,
+ "COUNT(*) AS _count");
+ sBlockedListProjectionMap.put(Im.BlockedList.USERNAME, "username");
+ sBlockedListProjectionMap.put(Im.BlockedList.NICKNAME, "nickname");
+ sBlockedListProjectionMap.put(Im.BlockedList.PROVIDER, "provider");
+ sBlockedListProjectionMap.put(Im.BlockedList.ACCOUNT, "account");
+ sBlockedListProjectionMap.put(Im.BlockedList.AVATAR_DATA,
+ "avatars.data AS avatars_data");
+ }
+
+ public ImProvider() {
+ this(AUTHORITY, DATABASE_NAME, DATABASE_VERSION);
+ }
+
+ protected ImProvider(String authority, String dbName, int dbVersion) {
+ mDatabaseName = dbName;
+ mDatabaseVersion = dbVersion;
+
+ mTransientDbName = "transient_" + dbName.replace(".", "_");
+
+ mUrlMatcher.addURI(authority, "providers", MATCH_PROVIDERS);
+ mUrlMatcher.addURI(authority, "providers/#", MATCH_PROVIDERS_BY_ID);
+ mUrlMatcher.addURI(authority, "providers/account", MATCH_PROVIDERS_WITH_ACCOUNT);
+
+ mUrlMatcher.addURI(authority, "accounts", MATCH_ACCOUNTS);
+ mUrlMatcher.addURI(authority, "accounts/#", MATCH_ACCOUNTS_BY_ID);
+
+ mUrlMatcher.addURI(authority, "contacts", MATCH_CONTACTS);
+ mUrlMatcher.addURI(authority, "contactsWithPresence", MATCH_CONTACTS_JOIN_PRESENCE);
+ mUrlMatcher.addURI(authority, "contactsBarebone", MATCH_CONTACTS_BAREBONE);
+ mUrlMatcher.addURI(authority, "contacts/#/#", MATCH_CONTACTS_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "contacts/chatting", MATCH_CHATTING_CONTACTS);
+ mUrlMatcher.addURI(authority, "contacts/chatting/#/#", MATCH_CHATTING_CONTACTS_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "contacts/online/#/#", MATCH_ONLINE_CONTACTS_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "contacts/offline/#/#", MATCH_OFFLINE_CONTACTS_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "contacts/#", MATCH_CONTACT);
+ mUrlMatcher.addURI(authority, "contacts/blocked", MATCH_BLOCKED_CONTACTS);
+ mUrlMatcher.addURI(authority, "bulk_contacts", MATCH_CONTACTS_BULK);
+ mUrlMatcher.addURI(authority, "contacts/onlineCount", MATCH_ONLINE_CONTACT_COUNT);
+
+ mUrlMatcher.addURI(authority, "contactLists", MATCH_CONTACTLISTS);
+ mUrlMatcher.addURI(authority, "contactLists/#/#", MATCH_CONTACTLISTS_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "contactLists/#", MATCH_CONTACTLIST);
+ mUrlMatcher.addURI(authority, "blockedList", MATCH_BLOCKEDLIST);
+ mUrlMatcher.addURI(authority, "blockedList/#/#", MATCH_BLOCKEDLIST_BY_PROVIDER);
+
+ mUrlMatcher.addURI(authority, "contactsEtag", MATCH_CONTACTS_ETAGS);
+ mUrlMatcher.addURI(authority, "contactsEtag/#", MATCH_CONTACTS_ETAG);
+
+ mUrlMatcher.addURI(authority, "presence", MATCH_PRESENCE);
+ mUrlMatcher.addURI(authority, "presence/#", MATCH_PRESENCE_ID);
+ mUrlMatcher.addURI(authority, "presence/account/#", MATCH_PRESENCE_BY_ACCOUNT);
+ mUrlMatcher.addURI(authority, "seed_presence/account/#", MATCH_PRESENCE_SEED_BY_ACCOUNT);
+ mUrlMatcher.addURI(authority, "bulk_presence", MATCH_PRESENCE_BULK);
+
+ mUrlMatcher.addURI(authority, "messages", MATCH_MESSAGES);
+ mUrlMatcher.addURI(authority, "messagesBy/#/#/*", MATCH_MESSAGES_BY_CONTACT);
+ mUrlMatcher.addURI(authority, "messages/#", MATCH_MESSAGE);
+
+ mUrlMatcher.addURI(authority, "groupMessages", MATCH_GROUP_MESSAGES);
+ mUrlMatcher.addURI(authority, "groupMessagesBy/#", MATCH_GROUP_MESSAGE_BY);
+ mUrlMatcher.addURI(authority, "groupMessages/#", MATCH_GROUP_MESSAGE);
+ mUrlMatcher.addURI(authority, "groupMembers", MATCH_GROUP_MEMBERS);
+ mUrlMatcher.addURI(authority, "groupMembers/#", MATCH_GROUP_MEMBERS_BY_GROUP);
+
+ mUrlMatcher.addURI(authority, "avatars", MATCH_AVATARS);
+ mUrlMatcher.addURI(authority, "avatars/#", MATCH_AVATAR);
+ mUrlMatcher.addURI(authority, "avatarsBy/#/#", MATCH_AVATAR_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "chats", MATCH_CHATS);
+ mUrlMatcher.addURI(authority, "chats/account/#", MATCH_CHATS_BY_ACCOUNT);
+ mUrlMatcher.addURI(authority, "chats/#", MATCH_CHATS_ID);
+
+ mUrlMatcher.addURI(authority, "sessionCookies", MATCH_SESSIONS);
+ mUrlMatcher.addURI(authority, "sessionCookiesBy/#/#", MATCH_SESSIONS_BY_PROVIDER);
+ mUrlMatcher.addURI(authority, "providerSettings", MATCH_PROVIDER_SETTINGS);
+ mUrlMatcher.addURI(authority, "providerSettings/#", MATCH_PROVIDER_SETTINGS_BY_ID);
+ mUrlMatcher.addURI(authority, "providerSettings/#/*",
+ MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME);
+
+ mUrlMatcher.addURI(authority, "invitations", MATCH_INVITATIONS);
+ mUrlMatcher.addURI(authority, "invitations/#", MATCH_INVITATION);
+
+ mUrlMatcher.addURI(authority, "outgoingRmqMessages", MATCH_OUTGOING_RMQ_MESSAGES);
+ mUrlMatcher.addURI(authority, "outgoingRmqMessages/#", MATCH_OUTGOING_RMQ_MESSAGE);
+ mUrlMatcher.addURI(authority, "outgoingHighestRmqId", MATCH_OUTGOING_HIGHEST_RMQ_ID);
+ mUrlMatcher.addURI(authority, "lastRmqId", MATCH_LAST_RMQ_ID);
+
+ mUrlMatcher.addURI(authority, "accountStatus", MATCH_ACCOUNTS_STATUS);
+ mUrlMatcher.addURI(authority, "accountStatus/#", MATCH_ACCOUNT_STATUS);
+
+ mUrlMatcher.addURI(authority, "brandingResMapCache", MATCH_BRANDING_RESOURCE_MAP_CACHE);
+ }
+
+ @Override
+ public boolean onCreate() {
+ mOpenHelper = new DatabaseHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public final int update(final Uri url, final ContentValues values,
+ final String selection, final String[] selectionArgs) {
+
+ int result = 0;
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ result = updateInternal(url, values, selection, selectionArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ if (result > 0) {
+ getContext().getContentResolver()
+ .notifyChange(url, null /* observer */, false /* sync */);
+ }
+ return result;
+ }
+
+ @Override
+ public final int delete(final Uri url, final String selection,
+ final String[] selectionArgs) {
+ int result;
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ result = deleteInternal(url, selection, selectionArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ if (result > 0) {
+ getContext().getContentResolver()
+ .notifyChange(url, null /* observer */, false /* sync */);
+ }
+ return result;
+ }
+
+ @Override
+ public final Uri insert(final Uri url, final ContentValues values) {
+ Uri result;
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ result = insertInternal(url, values);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ if (result != null) {
+ getContext().getContentResolver()
+ .notifyChange(url, null /* observer */, false /* sync */);
+ }
+ return result;
+ }
+
+ @Override
+ public final Cursor query(final Uri url, final String[] projection,
+ final String selection, final String[] selectionArgs,
+ final String sortOrder) {
+ return queryInternal(url, projection, selection, selectionArgs, sortOrder);
+ }
+
+ public Cursor queryInternal(Uri url, String[] projectionIn,
+ String selection, String[] selectionArgs, String sort) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ StringBuilder whereClause = new StringBuilder();
+ if(selection != null) {
+ whereClause.append(selection);
+ }
+ String groupBy = null;
+ String limit = null;
+
+ // Generate the body of the query
+ int match = mUrlMatcher.match(url);
+
+ if (DBG) {
+ log("query " + url + ", match " + match + ", where " + selection);
+ if (selectionArgs != null) {
+ for (String selectionArg : selectionArgs) {
+ log(" selectionArg: " + selectionArg);
+ }
+ }
+ }
+
+ switch (match) {
+ case MATCH_PROVIDERS_BY_ID:
+ appendWhere(whereClause, Im.Provider._ID, "=", url.getPathSegments().get(1));
+ // fall thru.
+
+ case MATCH_PROVIDERS:
+ qb.setTables(TABLE_PROVIDERS);
+ break;
+
+ case MATCH_PROVIDERS_WITH_ACCOUNT:
+ qb.setTables(PROVIDER_JOIN_ACCOUNT_TABLE);
+ qb.setProjectionMap(sProviderAccountsProjectionMap);
+ break;
+
+ case MATCH_ACCOUNTS_BY_ID:
+ appendWhere(whereClause, Im.Account._ID, "=", url.getPathSegments().get(1));
+ // falls down
+ case MATCH_ACCOUNTS:
+ qb.setTables(TABLE_ACCOUNTS);
+ break;
+
+ case MATCH_CONTACTS:
+ qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ break;
+
+ case MATCH_CONTACTS_JOIN_PRESENCE:
+ qb.setTables(CONTACT_JOIN_PRESENCE_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ break;
+
+ case MATCH_CONTACTS_BAREBONE:
+ qb.setTables(TABLE_CONTACTS);
+ break;
+
+ case MATCH_CHATTING_CONTACTS:
+ qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ appendWhere(whereClause, "chats.last_message_date IS NOT NULL");
+ // no need to add the non blocked contacts clause because
+ // blocked contacts can't have conversations.
+ break;
+
+ case MATCH_CONTACTS_BY_PROVIDER:
+ buildQueryContactsByProvider(qb, whereClause, url);
+ appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
+ break;
+
+ case MATCH_CHATTING_CONTACTS_BY_PROVIDER:
+ buildQueryContactsByProvider(qb, whereClause, url);
+ appendWhere(whereClause, "chats.last_message_date IS NOT NULL");
+ // no need to add the non blocked contacts clause because
+ // blocked contacts can't have conversations.
+ break;
+
+ case MATCH_NO_CHATTING_CONTACTS_BY_PROVIDER:
+ buildQueryContactsByProvider(qb, whereClause, url);
+ appendWhere(whereClause, "chats.last_message_date IS NULL");
+ appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
+ break;
+
+ case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
+ buildQueryContactsByProvider(qb, whereClause, url);
+ appendWhere(whereClause, Im.Contacts.PRESENCE_STATUS, "!=", Im.Presence.OFFLINE);
+ appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
+ break;
+
+ case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
+ buildQueryContactsByProvider(qb, whereClause, url);
+ appendWhere(whereClause, Im.Contacts.PRESENCE_STATUS, "=", Im.Presence.OFFLINE);
+ appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
+ break;
+
+ case MATCH_BLOCKED_CONTACTS:
+ qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ appendWhere(whereClause, BLOCKED_CONTACTS_WHERE_CLAUSE);
+ break;
+
+ case MATCH_CONTACT:
+ qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ appendWhere(whereClause, "contacts._id", "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_ONLINE_CONTACT_COUNT:
+ qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ appendWhere(whereClause, Im.Contacts.PRESENCE_STATUS, "!=", Im.Presence.OFFLINE);
+ appendWhere(whereClause, "chats.last_message_date IS NULL");
+ appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
+ groupBy = Im.Contacts.CONTACTLIST;
+ break;
+
+ case MATCH_CONTACTLISTS_BY_PROVIDER:
+ appendWhere(whereClause, Im.ContactList.ACCOUNT, "=",
+ url.getPathSegments().get(2));
+ // fall through
+ case MATCH_CONTACTLISTS:
+ qb.setTables(TABLE_CONTACT_LIST);
+ qb.setProjectionMap(sContactListProjectionMap);
+ break;
+
+ case MATCH_CONTACTLIST:
+ qb.setTables(TABLE_CONTACT_LIST);
+ appendWhere(whereClause, Im.ContactList._ID, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_BLOCKEDLIST:
+ qb.setTables(BLOCKEDLIST_JOIN_AVATAR_TABLE);
+ qb.setProjectionMap(sBlockedListProjectionMap);
+ break;
+
+ case MATCH_BLOCKEDLIST_BY_PROVIDER:
+ qb.setTables(BLOCKEDLIST_JOIN_AVATAR_TABLE);
+ qb.setProjectionMap(sBlockedListProjectionMap);
+ appendWhere(whereClause, Im.BlockedList.ACCOUNT, "=",
+ url.getPathSegments().get(2));
+ break;
+
+ case MATCH_CONTACTS_ETAGS:
+ qb.setTables(TABLE_CONTACTS_ETAG);
+ break;
+
+ case MATCH_CONTACTS_ETAG:
+ qb.setTables(TABLE_CONTACTS_ETAG);
+ appendWhere(whereClause, "_id", "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_MESSAGES:
+ qb.setTables(TABLE_MESSAGES);
+ break;
+
+ case MATCH_MESSAGES_BY_CONTACT:
+ // we don't really need the provider id in query. account id
+ // is enough.
+ qb.setTables(TABLE_MESSAGES);
+ appendWhere(whereClause, Im.Messages.ACCOUNT, "=",
+ url.getPathSegments().get(2));
+ appendWhere(whereClause, Im.Messages.CONTACT, "=",
+ decodeURLSegment(url.getPathSegments().get(3)));
+ break;
+
+ case MATCH_MESSAGE:
+ qb.setTables(TABLE_MESSAGES);
+ appendWhere(whereClause, Im.Messages._ID, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_INVITATIONS:
+ qb.setTables(TABLE_INVITATIONS);
+ break;
+
+ case MATCH_INVITATION:
+ qb.setTables(TABLE_INVITATIONS);
+ appendWhere(whereClause, Im.Invitation._ID, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_GROUP_MEMBERS:
+ qb.setTables(TABLE_GROUP_MEMBERS);
+ break;
+
+ case MATCH_GROUP_MEMBERS_BY_GROUP:
+ qb.setTables(TABLE_GROUP_MEMBERS);
+ appendWhere(whereClause, Im.GroupMembers.GROUP, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_GROUP_MESSAGES:
+ qb.setTables(TABLE_GROUP_MESSAGES);
+ break;
+
+ case MATCH_GROUP_MESSAGE_BY:
+ qb.setTables(TABLE_GROUP_MESSAGES);
+ appendWhere(whereClause, Im.GroupMessages.GROUP, "=",
+ url.getPathSegments().get(1));
+ break;
+
+ case MATCH_GROUP_MESSAGE:
+ qb.setTables(TABLE_GROUP_MESSAGES);
+ appendWhere(whereClause, Im.GroupMessages._ID, "=",
+ url.getPathSegments().get(1));
+ break;
+
+ case MATCH_AVATARS:
+ qb.setTables(TABLE_AVATARS);
+ break;
+
+ case MATCH_AVATAR_BY_PROVIDER:
+ qb.setTables(TABLE_AVATARS);
+ appendWhere(whereClause, Im.Avatars.ACCOUNT, "=", url.getPathSegments().get(2));
+ break;
+
+ case MATCH_CHATS:
+ qb.setTables(TABLE_CHATS);
+ break;
+
+ case MATCH_CHATS_ID:
+ qb.setTables(TABLE_CHATS);
+ appendWhere(whereClause, Im.Chats.CONTACT_ID, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_PRESENCE:
+ qb.setTables(TABLE_PRESENCE);
+ break;
+
+ case MATCH_PRESENCE_ID:
+ qb.setTables(TABLE_PRESENCE);
+ appendWhere(whereClause, Im.Presence.CONTACT_ID, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_SESSIONS:
+ qb.setTables(TABLE_SESSION_COOKIES);
+ break;
+
+ case MATCH_SESSIONS_BY_PROVIDER:
+ qb.setTables(TABLE_SESSION_COOKIES);
+ appendWhere(whereClause, Im.SessionCookies.ACCOUNT, "=", url.getPathSegments().get(2));
+ break;
+
+ case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
+ appendWhere(whereClause, Im.ProviderSettings.NAME, "=", url.getPathSegments().get(2));
+ // fall through
+ case MATCH_PROVIDER_SETTINGS_BY_ID:
+ appendWhere(whereClause, Im.ProviderSettings.PROVIDER, "=", url.getPathSegments().get(1));
+ // fall through
+ case MATCH_PROVIDER_SETTINGS:
+ qb.setTables(TABLE_PROVIDER_SETTINGS);
+ break;
+
+ case MATCH_OUTGOING_RMQ_MESSAGES:
+ qb.setTables(TABLE_OUTGOING_RMQ_MESSAGES);
+ break;
+
+ case MATCH_OUTGOING_HIGHEST_RMQ_ID:
+ qb.setTables(TABLE_OUTGOING_RMQ_MESSAGES);
+ sort = "rmq_id DESC";
+ limit = "1";
+ break;
+
+ case MATCH_LAST_RMQ_ID:
+ qb.setTables(TABLE_LAST_RMQ_ID);
+ limit = "1";
+ break;
+
+ case MATCH_ACCOUNTS_STATUS:
+ qb.setTables(TABLE_ACCOUNT_STATUS);
+ break;
+
+ case MATCH_ACCOUNT_STATUS:
+ qb.setTables(TABLE_ACCOUNT_STATUS);
+ appendWhere(whereClause, Im.AccountStatus.ACCOUNT, "=",
+ url.getPathSegments().get(1));
+ break;
+
+ case MATCH_BRANDING_RESOURCE_MAP_CACHE:
+ qb.setTables(TABLE_BRANDING_RESOURCE_MAP_CACHE);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+
+ // run the query
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = null;
+
+ try {
+ c = qb.query(db, projectionIn, whereClause.toString(), selectionArgs,
+ groupBy, null, sort, limit);
+ if (c != null) {
+ switch(match) {
+ case MATCH_CHATTING_CONTACTS:
+ case MATCH_CONTACTS_BY_PROVIDER:
+ case MATCH_CHATTING_CONTACTS_BY_PROVIDER:
+ case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
+ case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
+ case MATCH_CONTACTS_BAREBONE:
+ case MATCH_CONTACTS_JOIN_PRESENCE:
+ case MATCH_ONLINE_CONTACT_COUNT:
+ url = Im.Contacts.CONTENT_URI;
+ break;
+ }
+ if (DBG) log("set notify url " + url);
+ c.setNotificationUri(getContext().getContentResolver(), url);
+ }
+ } catch (Exception ex) {
+ Log.e(LOG_TAG, "query db caught ", ex);
+ }
+
+ return c;
+ }
+
+ private void buildQueryContactsByProvider(SQLiteQueryBuilder qb,
+ StringBuilder whereClause, Uri url) {
+ qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
+ qb.setProjectionMap(sContactsProjectionMap);
+ // we don't really need the provider id in query. account id
+ // is enough.
+ appendWhere(whereClause, Im.Contacts.ACCOUNT, "=", url.getLastPathSegment());
+ }
+
+ @Override
+ public String getType(Uri url) {
+ int match = mUrlMatcher.match(url);
+ switch (match) {
+ case MATCH_PROVIDERS:
+ return Im.Provider.CONTENT_TYPE;
+
+ case MATCH_PROVIDERS_BY_ID:
+ return Im.Provider.CONTENT_ITEM_TYPE;
+
+ case MATCH_ACCOUNTS:
+ return Im.Account.CONTENT_TYPE;
+
+ case MATCH_ACCOUNTS_BY_ID:
+ return Im.Account.CONTENT_ITEM_TYPE;
+
+ case MATCH_CONTACTS:
+ case MATCH_CONTACTS_BY_PROVIDER:
+ case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
+ case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
+ case MATCH_CONTACTS_BULK:
+ case MATCH_CONTACTS_BAREBONE:
+ case MATCH_CONTACTS_JOIN_PRESENCE:
+ return Im.Contacts.CONTENT_TYPE;
+
+ case MATCH_CONTACT:
+ return Im.Contacts.CONTENT_ITEM_TYPE;
+
+ case MATCH_CONTACTLISTS:
+ case MATCH_CONTACTLISTS_BY_PROVIDER:
+ return Im.ContactList.CONTENT_TYPE;
+
+ case MATCH_CONTACTLIST:
+ return Im.ContactList.CONTENT_ITEM_TYPE;
+
+ case MATCH_BLOCKEDLIST:
+ case MATCH_BLOCKEDLIST_BY_PROVIDER:
+ return Im.BlockedList.CONTENT_TYPE;
+
+ case MATCH_CONTACTS_ETAGS:
+ case MATCH_CONTACTS_ETAG:
+ return Im.ContactsEtag.CONTENT_TYPE;
+
+ case MATCH_MESSAGES:
+ case MATCH_MESSAGES_BY_CONTACT:
+ return Im.Messages.CONTENT_TYPE;
+
+ case MATCH_MESSAGE:
+ return Im.Messages.CONTENT_ITEM_TYPE;
+
+ case MATCH_GROUP_MESSAGES:
+ case MATCH_GROUP_MESSAGE_BY:
+ return Im.GroupMessages.CONTENT_TYPE;
+
+ case MATCH_GROUP_MESSAGE:
+ return Im.GroupMessages.CONTENT_ITEM_TYPE;
+
+ case MATCH_PRESENCE:
+ case MATCH_PRESENCE_BULK:
+ return Im.Presence.CONTENT_TYPE;
+
+ case MATCH_AVATARS:
+ return Im.Avatars.CONTENT_TYPE;
+
+ case MATCH_AVATAR:
+ return Im.Avatars.CONTENT_ITEM_TYPE;
+
+ case MATCH_CHATS:
+ return Im.Chats.CONTENT_TYPE;
+
+ case MATCH_CHATS_ID:
+ return Im.Chats.CONTENT_ITEM_TYPE;
+
+ case MATCH_INVITATIONS:
+ return Im.Invitation.CONTENT_TYPE;
+
+ case MATCH_INVITATION:
+ return Im.Invitation.CONTENT_ITEM_TYPE;
+
+ case MATCH_GROUP_MEMBERS:
+ case MATCH_GROUP_MEMBERS_BY_GROUP:
+ return Im.GroupMembers.CONTENT_TYPE;
+
+ case MATCH_SESSIONS:
+ case MATCH_SESSIONS_BY_PROVIDER:
+ return Im.SessionCookies.CONTENT_TYPE;
+
+ case MATCH_PROVIDER_SETTINGS:
+ return Im.ProviderSettings.CONTENT_TYPE;
+
+ case MATCH_ACCOUNTS_STATUS:
+ return Im.AccountStatus.CONTENT_TYPE;
+
+ case MATCH_ACCOUNT_STATUS:
+ return Im.AccountStatus.CONTENT_ITEM_TYPE;
+
+ default:
+ throw new IllegalArgumentException("Unknown URL");
+ }
+ }
+
+ // package scope for testing.
+ boolean insertBulkContacts(ContentValues values) {
+ //if (DBG) log("insertBulkContacts: begin");
+
+ ArrayList<String> usernames = values.getStringArrayList(Im.Contacts.USERNAME);
+ ArrayList<String> nicknames = values.getStringArrayList(Im.Contacts.NICKNAME);
+ int usernameCount = usernames.size();
+ int nicknameCount = nicknames.size();
+
+ if (usernameCount != nicknameCount) {
+ Log.e(LOG_TAG, "[ImProvider] insertBulkContacts: input bundle " +
+ "username & nickname lists have diff. length!");
+ return false;
+ }
+
+ ArrayList<String> contactTypeArray = values.getStringArrayList(Im.Contacts.TYPE);
+ ArrayList<String> subscriptionStatusArray =
+ values.getStringArrayList(Im.Contacts.SUBSCRIPTION_STATUS);
+ ArrayList<String> subscriptionTypeArray =
+ values.getStringArrayList(Im.Contacts.SUBSCRIPTION_TYPE);
+ ArrayList<String> quickContactArray = values.getStringArrayList(Im.Contacts.QUICK_CONTACT);
+ ArrayList<String> rejectedArray = values.getStringArrayList(Im.Contacts.REJECTED);
+ int sum = 0;
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ db.beginTransaction();
+ try {
+ Long provider = values.getAsLong(Im.Contacts.PROVIDER);
+ Long account = values.getAsLong(Im.Contacts.ACCOUNT);
+ Long listId = values.getAsLong(Im.Contacts.CONTACTLIST);
+
+ ContentValues contactValues = new ContentValues();
+ contactValues.put(Im.Contacts.PROVIDER, provider);
+ contactValues.put(Im.Contacts.ACCOUNT, account);
+ contactValues.put(Im.Contacts.CONTACTLIST, listId);
+ ContentValues presenceValues = new ContentValues();
+ presenceValues.put(Im.Presence.PRESENCE_STATUS,
+ Im.Presence.OFFLINE);
+
+ for (int i=0; i<usernameCount; i++) {
+ String username = usernames.get(i);
+ String nickname = nicknames.get(i);
+ int type = 0;
+ int subscriptionStatus = 0;
+ int subscriptionType = 0;
+ int quickContact = 0;
+ int rejected = 0;
+
+ try {
+ type = Integer.parseInt(contactTypeArray.get(i));
+ if (subscriptionStatusArray != null) {
+ subscriptionStatus = Integer.parseInt(subscriptionStatusArray.get(i));
+ }
+ if (subscriptionTypeArray != null) {
+ subscriptionType = Integer.parseInt(subscriptionTypeArray.get(i));
+ }
+ if (quickContactArray != null) {
+ quickContact = Integer.parseInt(quickContactArray.get(i));
+ }
+ if (rejectedArray != null) {
+ rejected = Integer.parseInt(rejectedArray.get(i));
+ }
+ } catch (NumberFormatException ex) {
+ Log.e(LOG_TAG, "insertBulkContacts: caught " + ex);
+ }
+
+ /*
+ if (DBG) log("insertBulkContacts[" + i + "] username=" +
+ username + ", nickname=" + nickname + ", type=" + type +
+ ", subscriptionStatus=" + subscriptionStatus + ", subscriptionType=" +
+ subscriptionType + ", qc=" + quickContact);
+ */
+
+ contactValues.put(Im.Contacts.USERNAME, username);
+ contactValues.put(Im.Contacts.NICKNAME, nickname);
+ contactValues.put(Im.Contacts.TYPE, type);
+ if (subscriptionStatusArray != null) {
+ contactValues.put(Im.Contacts.SUBSCRIPTION_STATUS, subscriptionStatus);
+ }
+ if (subscriptionTypeArray != null) {
+ contactValues.put(Im.Contacts.SUBSCRIPTION_TYPE, subscriptionType);
+ }
+ if (quickContactArray != null) {
+ contactValues.put(Im.Contacts.QUICK_CONTACT, quickContact);
+ }
+ if (rejectedArray != null) {
+ contactValues.put(Im.Contacts.REJECTED, rejected);
+ }
+
+ long rowId = 0;
+
+ /* save this code for when we add constraint (account, username) to the contacts
+ table
+ try {
+ rowId = db.insertOrThrow(TABLE_CONTACTS, USERNAME, contactValues);
+ } catch (android.database.sqlite.SQLiteConstraintException ex) {
+ if (DBG) log("insertBulkContacts: insert " + username + " caught " + ex);
+
+ // append username to the selection clause
+ updateSelection.delete(0, updateSelection.length());
+ updateSelection.append(Im.Contacts.USERNAME);
+ updateSelection.append("=?");
+ updateSelectionArgs[0] = username;
+
+ int updated = db.update(TABLE_CONTACTS, contactValues,
+ updateSelection.toString(), updateSelectionArgs);
+
+ if (DBG && updated != 1) {
+ log("insertBulkContacts: update " + username + " failed!");
+ }
+ }
+ */
+
+ rowId = db.insert(TABLE_CONTACTS, USERNAME, contactValues);
+ if (rowId > 0) {
+ sum++;
+ if (!USE_CONTACT_PRESENCE_TRIGGER) {
+ // seed the presence for the new contact
+ //if (DBG) log("seedPresence for pid " + rowId);
+ presenceValues.put(Im.Presence.CONTACT_ID, rowId);
+ db.insert(TABLE_PRESENCE, null, presenceValues);
+ }
+ }
+
+ // yield the lock if anyone else is trying to
+ // perform a db operation here.
+ db.yieldIfContended();
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ // We know that we succeeded becuase endTransaction throws if the transaction failed.
+ if (DBG) log("insertBulkContacts: added " + sum + " contacts!");
+ return true;
+ }
+
+ // package scope for testing.
+ int updateBulkContacts(ContentValues values, String userWhere) {
+ ArrayList<String> usernames = values.getStringArrayList(Im.Contacts.USERNAME);
+ ArrayList<String> nicknames = values.getStringArrayList(Im.Contacts.NICKNAME);
+
+ int usernameCount = usernames.size();
+ int nicknameCount = nicknames.size();
+
+ if (usernameCount != nicknameCount) {
+ Log.e(LOG_TAG, "[ImProvider] updateBulkContacts: input bundle " +
+ "username & nickname lists have diff. length!");
+ return 0;
+ }
+
+ ArrayList<String> contactTypeArray = values.getStringArrayList(Im.Contacts.TYPE);
+ ArrayList<String> subscriptionStatusArray =
+ values.getStringArrayList(Im.Contacts.SUBSCRIPTION_STATUS);
+ ArrayList<String> subscriptionTypeArray =
+ values.getStringArrayList(Im.Contacts.SUBSCRIPTION_TYPE);
+ ArrayList<String> quickContactArray = values.getStringArrayList(Im.Contacts.QUICK_CONTACT);
+ ArrayList<String> rejectedArray = values.getStringArrayList(Im.Contacts.REJECTED);
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ db.beginTransaction();
+ int sum = 0;
+
+ try {
+ Long provider = values.getAsLong(Im.Contacts.PROVIDER);
+ Long account = values.getAsLong(Im.Contacts.ACCOUNT);
+
+ ContentValues contactValues = new ContentValues();
+ contactValues.put(Im.Contacts.PROVIDER, provider);
+ contactValues.put(Im.Contacts.ACCOUNT, account);
+
+ StringBuilder updateSelection = new StringBuilder();
+ String[] updateSelectionArgs = new String[1];
+
+ for (int i=0; i<usernameCount; i++) {
+ String username = usernames.get(i);
+ String nickname = nicknames.get(i);
+ int type = 0;
+ int subscriptionStatus = 0;
+ int subscriptionType = 0;
+ int quickContact = 0;
+ int rejected = 0;
+
+ try {
+ type = Integer.parseInt(contactTypeArray.get(i));
+ subscriptionStatus = Integer.parseInt(subscriptionStatusArray.get(i));
+ subscriptionType = Integer.parseInt(subscriptionTypeArray.get(i));
+ quickContact = Integer.parseInt(quickContactArray.get(i));
+ rejected = Integer.parseInt(rejectedArray.get(i));
+ } catch (NumberFormatException ex) {
+ Log.e(LOG_TAG, "insertBulkContacts: caught " + ex);
+ }
+
+ if (DBG) log("updateBulkContacts[" + i + "] username=" +
+ username + ", nickname=" + nickname + ", type=" + type +
+ ", subscriptionStatus=" + subscriptionStatus + ", subscriptionType=" +
+ subscriptionType + ", qc=" + quickContact);
+
+ contactValues.put(Im.Contacts.USERNAME, username);
+ contactValues.put(Im.Contacts.NICKNAME, nickname);
+ contactValues.put(Im.Contacts.TYPE, type);
+ contactValues.put(Im.Contacts.SUBSCRIPTION_STATUS, subscriptionStatus);
+ contactValues.put(Im.Contacts.SUBSCRIPTION_TYPE, subscriptionType);
+ contactValues.put(Im.Contacts.QUICK_CONTACT, quickContact);
+ contactValues.put(Im.Contacts.REJECTED, rejected);
+
+ // append username to the selection clause
+ updateSelection.delete(0, updateSelection.length());
+ updateSelection.append(userWhere);
+ updateSelection.append(" AND ");
+ updateSelection.append(Im.Contacts.USERNAME);
+ updateSelection.append("=?");
+
+ updateSelectionArgs[0] = username;
+
+ int numUpdated = db.update(TABLE_CONTACTS, contactValues,
+ updateSelection.toString(), updateSelectionArgs);
+ if (numUpdated == 0) {
+ Log.e(LOG_TAG, "[ImProvider] updateBulkContacts: " +
+ " update failed for selection = " + updateSelection);
+ } else {
+ sum += numUpdated;
+ }
+
+ // yield the lock if anyone else is trying to
+ // perform a db operation here.
+ db.yieldIfContended();
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (DBG) log("updateBulkContacts: " + sum + " entries updated");
+ return sum;
+ }
+
+ // constants definitions use for the query in seedInitialPresenceByAccount()
+ private static final String[] CONTACT_ID_PROJECTION = new String[] {
+ Im.Contacts._ID, // 0
+ };
+
+ private static final int COLUMN_ID = 0;
+
+ private static final String CONTACTS_WITH_NO_PRESENCE_SELECTION =
+ Im.Contacts.ACCOUNT + "=?" + " AND " + Im.Contacts._ID +
+ " in (select " + CONTACT_ID + " from " + TABLE_CONTACTS +
+ " left outer join " + TABLE_PRESENCE + " on " + CONTACT_ID + '=' +
+ PRESENCE_CONTACT_ID + " where " + PRESENCE_CONTACT_ID + " IS NULL)";
+
+ // selection args for the query.
+ private String[] mQueryContactPresenceSelectionArgs = new String[1];
+
+ /**
+ * This method first performs a query for all the contacts (for the given account) that
+ * don't have a presence entry in the presence table. Then for each of those contacts,
+ * the method creates a presence row. The whole thing is done inside one database transaction
+ * to increase performance.
+ *
+ * @param account the account of the contacts for which we want to create seed presence rows.
+ */
+ private void seedInitialPresenceByAccount(long account) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(TABLE_CONTACTS);
+ qb.setProjectionMap(sContactsProjectionMap);
+
+ mQueryContactPresenceSelectionArgs[0] = String.valueOf(account);
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+
+ Cursor c = null;
+
+ try {
+ ContentValues presenceValues = new ContentValues();
+ presenceValues.put(Im.Presence.PRESENCE_STATUS, Im.Presence.OFFLINE);
+ presenceValues.put(Im.Presence.PRESENCE_CUSTOM_STATUS, "");
+
+ // First: update all the presence for the account so they are offline
+ StringBuilder buf = new StringBuilder();
+ buf.append(Im.Presence.CONTACT_ID);
+ buf.append(" in (select ");
+ buf.append(Im.Contacts._ID);
+ buf.append(" from ");
+ buf.append(TABLE_CONTACTS);
+ buf.append(" where ");
+ buf.append(Im.Contacts.ACCOUNT);
+ buf.append("=?) ");
+
+ String selection = buf.toString();
+ if (DBG) log("seedInitialPresence: reset presence selection=" + selection);
+
+ int count = db.update(TABLE_PRESENCE, presenceValues, selection,
+ mQueryContactPresenceSelectionArgs);
+ if (DBG) log("seedInitialPresence: reset " + count + " presence rows to OFFLINE");
+
+ // second: add a presence row for each contact that doesn't have a presence
+ if (DBG) {
+ log("seedInitialPresence: contacts_with_no_presence_selection => " +
+ CONTACTS_WITH_NO_PRESENCE_SELECTION);
+ }
+
+ c = qb.query(db,
+ CONTACT_ID_PROJECTION,
+ CONTACTS_WITH_NO_PRESENCE_SELECTION,
+ mQueryContactPresenceSelectionArgs,
+ null, null, null, null);
+
+ if (DBG) log("seedInitialPresence: found " + c.getCount() + " contacts w/o presence");
+
+ count = 0;
+
+ while (c.moveToNext()) {
+ long id = c.getLong(COLUMN_ID);
+ presenceValues.put(Im.Presence.CONTACT_ID, id);
+
+ try {
+ if (db.insert(TABLE_PRESENCE, null, presenceValues) > 0) {
+ count++;
+ }
+ } catch (SQLiteConstraintException ex) {
+ // we could possibly catch this exception, since there could be a presence
+ // row with the same contact_id. That's fine, just ignore the error
+ if (DBG) log("seedInitialPresence: insert presence for contact_id " + id +
+ " failed, caught " + ex);
+ }
+ }
+
+ db.setTransactionSuccessful();
+
+ if (DBG) log("seedInitialPresence: added " + count + " new presence rows");
+ } finally {
+ c.close();
+ db.endTransaction();
+ }
+ }
+
+ private int updateBulkPresence(ContentValues values, String userWhere, String[] whereArgs) {
+ ArrayList<String> usernames = values.getStringArrayList(Im.Contacts.USERNAME);
+ int count = usernames.size();
+ Long account = values.getAsLong(Im.Contacts.ACCOUNT);
+
+ ArrayList<String> priorityArray = values.getStringArrayList(Im.Presence.PRIORITY);
+ ArrayList<String> modeArray = values.getStringArrayList(Im.Presence.PRESENCE_STATUS);
+ ArrayList<String> statusArray = values.getStringArrayList(
+ Im.Presence.PRESENCE_CUSTOM_STATUS);
+ ArrayList<String> clientTypeArray = values.getStringArrayList(Im.Presence.CLIENT_TYPE);
+ ArrayList<String> resourceArray = values.getStringArrayList(Im.Presence.JID_RESOURCE);
+
+ // append username to the selection clause
+ StringBuilder buf = new StringBuilder();
+
+ if (!TextUtils.isEmpty(userWhere)) {
+ buf.append(userWhere);
+ buf.append(" AND ");
+ }
+
+ buf.append(Im.Presence.CONTACT_ID);
+ buf.append(" in (select ");
+ buf.append(Im.Contacts._ID);
+ buf.append(" from ");
+ buf.append(TABLE_CONTACTS);
+ buf.append(" where ");
+ buf.append(Im.Contacts.ACCOUNT);
+ buf.append("=? AND ");
+
+ // use username LIKE ? for case insensitive comparison
+ buf.append(Im.Contacts.USERNAME);
+ buf.append(" LIKE ?) AND (");
+
+ buf.append(Im.Presence.PRIORITY);
+ buf.append("<=? OR ");
+ buf.append(Im.Presence.PRIORITY);
+ buf.append(" IS NULL OR ");
+ buf.append(Im.Presence.JID_RESOURCE);
+ buf.append("=?)");
+
+ String selection = buf.toString();
+
+ if (DBG) log("updateBulkPresence: selection => " + selection);
+
+ int numArgs = (whereArgs != null ? whereArgs.length + 4 : 4);
+ String[] selectionArgs = new String[numArgs];
+ int selArgsIndex = 0;
+
+ if (whereArgs != null) {
+ for (selArgsIndex=0; selArgsIndex<numArgs-1; selArgsIndex++) {
+ selectionArgs[selArgsIndex] = whereArgs[selArgsIndex];
+ }
+ }
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ db.beginTransaction();
+ int sum = 0;
+
+ try {
+ ContentValues presenceValues = new ContentValues();
+
+ for (int i=0; i<count; i++) {
+ String username = usernames.get(i);
+ int priority = 0;
+ int mode = 0;
+ String status = statusArray.get(i);
+ String jidResource = resourceArray == null ? "" : resourceArray.get(i);
+ int clientType = Im.Presence.CLIENT_TYPE_DEFAULT;
+
+ try {
+ if (priorityArray != null) {
+ priority = Integer.parseInt(priorityArray.get(i));
+ }
+ if (modeArray != null) {
+ mode = Integer.parseInt(modeArray.get(i));
+ }
+ if (clientTypeArray != null) {
+ clientType = Integer.parseInt(clientTypeArray.get(i));
+ }
+ } catch (NumberFormatException ex) {
+ Log.e(LOG_TAG, "[ImProvider] updateBulkPresence: caught " + ex);
+ }
+
+ /*
+ if (DBG) {
+ log("updateBulkPresence[" + i + "] username=" + username + ", priority=" +
+ priority + ", mode=" + mode + ", status=" + status + ", resource=" +
+ jidResource + ", clientType=" + clientType);
+ }
+ */
+
+ if (modeArray != null) {
+ presenceValues.put(Im.Presence.PRESENCE_STATUS, mode);
+ }
+ if (priorityArray != null) {
+ presenceValues.put(Im.Presence.PRIORITY, priority);
+ }
+ presenceValues.put(Im.Presence.PRESENCE_CUSTOM_STATUS, status);
+ if (clientTypeArray != null) {
+ presenceValues.put(Im.Presence.CLIENT_TYPE, clientType);
+ }
+
+ if (!TextUtils.isEmpty(jidResource)) {
+ presenceValues.put(Im.Presence.JID_RESOURCE, jidResource);
+ }
+
+ // fill in the selection args
+ int idx = selArgsIndex;
+ selectionArgs[idx++] = String.valueOf(account);
+ selectionArgs[idx++] = username;
+ selectionArgs[idx++] = String.valueOf(priority);
+ selectionArgs[idx] = jidResource;
+
+ int numUpdated = db.update(TABLE_PRESENCE,
+ presenceValues, selection, selectionArgs);
+ if (numUpdated == 0) {
+ Log.e(LOG_TAG, "[ImProvider] updateBulkPresence: failed for " + username);
+ } else {
+ sum += numUpdated;
+ }
+
+ // yield the lock if anyone else is trying to
+ // perform a db operation here.
+ db.yieldIfContended();
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (DBG) log("updateBulkPresence: " + sum + " entries updated");
+ return sum;
+ }
+
+ public Uri insertInternal(Uri url, ContentValues initialValues) {
+ Uri resultUri = null;
+ long rowID = 0;
+ boolean notifyContactListContentUri = false;
+ boolean notifyContactContentUri = false;
+ boolean notifyMessagesContentUri = false;
+ boolean notifyGroupMessagesContentUri = false;
+ boolean notifyProviderAccountContentUri = false;
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int match = mUrlMatcher.match(url);
+
+ if (DBG) log("insert to " + url + ", match " + match);
+ switch (match) {
+ case MATCH_PROVIDERS:
+ // Insert into the providers table
+ rowID = db.insert(TABLE_PROVIDERS, "name", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Provider.CONTENT_URI + "/" + rowID);
+ }
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_ACCOUNTS:
+ // Insert into the accounts table
+ rowID = db.insert(TABLE_ACCOUNTS, "name", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Account.CONTENT_URI + "/" + rowID);
+ }
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_CONTACTS_BY_PROVIDER:
+ appendValuesFromUrl(initialValues, url, Im.Contacts.PROVIDER,
+ Im.Contacts.ACCOUNT);
+ // fall through
+ case MATCH_CONTACTS:
+ case MATCH_CONTACTS_BAREBONE:
+ // Insert into the contacts table
+ rowID = db.insert(TABLE_CONTACTS, "username", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Contacts.CONTENT_URI + "/" + rowID);
+ }
+
+ notifyContactContentUri = true;
+ break;
+
+ case MATCH_CONTACTS_BULK:
+ if (insertBulkContacts(initialValues)) {
+ // notify change using the "content://im/contacts" url,
+ // so the change will be observed by listeners interested
+ // in contacts changes.
+ resultUri = Im.Contacts.CONTENT_URI;
+ }
+ notifyContactContentUri = true;
+ break;
+
+ case MATCH_CONTACTLISTS_BY_PROVIDER:
+ appendValuesFromUrl(initialValues, url, Im.ContactList.PROVIDER,
+ Im.ContactList.ACCOUNT);
+ // fall through
+ case MATCH_CONTACTLISTS:
+ // Insert into the contactList table
+ rowID = db.insert(TABLE_CONTACT_LIST, "name", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.ContactList.CONTENT_URI + "/" + rowID);
+ }
+ notifyContactListContentUri = true;
+ break;
+
+ case MATCH_BLOCKEDLIST_BY_PROVIDER:
+ appendValuesFromUrl(initialValues, url, Im.BlockedList.PROVIDER,
+ Im.BlockedList.ACCOUNT);
+ // fall through
+ case MATCH_BLOCKEDLIST:
+ // Insert into the blockedList table
+ rowID = db.insert(TABLE_BLOCKED_LIST, "username", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.BlockedList.CONTENT_URI + "/" + rowID);
+ }
+
+ break;
+
+ case MATCH_CONTACTS_ETAGS:
+ rowID = db.replace(TABLE_CONTACTS_ETAG, Im.ContactsEtag.ETAG, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.ContactsEtag.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_MESSAGES_BY_CONTACT:
+ appendValuesFromUrl(initialValues, url, Im.Messages.PROVIDER,
+ Im.Messages.ACCOUNT, Im.Messages.CONTACT);
+ notifyMessagesContentUri = true;
+ // fall through
+ case MATCH_MESSAGES:
+ // Insert into the messages table.
+ rowID = db.insert(TABLE_MESSAGES, "contact", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Messages.CONTENT_URI + "/" + rowID);
+ }
+
+ break;
+
+ case MATCH_INVITATIONS:
+ rowID = db.insert(TABLE_INVITATIONS, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Invitation.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_GROUP_MEMBERS:
+ rowID = db.insert(TABLE_GROUP_MEMBERS, "nickname", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.GroupMembers.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_GROUP_MEMBERS_BY_GROUP:
+ appendValuesFromUrl(initialValues, url, Im.GroupMembers.GROUP);
+ rowID = db.insert(TABLE_GROUP_MEMBERS, "nickname", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.GroupMembers.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_GROUP_MESSAGE_BY:
+ appendValuesFromUrl(initialValues, url, Im.GroupMembers.GROUP);
+ notifyGroupMessagesContentUri = true;
+ // fall through
+ case MATCH_GROUP_MESSAGES:
+ rowID = db.insert(TABLE_GROUP_MESSAGES, "group", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.GroupMessages.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_AVATAR_BY_PROVIDER:
+ appendValuesFromUrl(initialValues, url, Im.Avatars.PROVIDER, Im.Avatars.ACCOUNT);
+ // fall through
+ case MATCH_AVATARS:
+ // Insert into the avatars table
+ rowID = db.replace(TABLE_AVATARS, "contact", initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Avatars.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_CHATS_ID:
+ appendValuesFromUrl(initialValues, url, Im.Chats.CONTACT_ID);
+ // fall through
+ case MATCH_CHATS:
+ // Insert into the chats table
+ initialValues.put(Im.Chats.SHORTCUT, -1);
+ rowID = db.replace(TABLE_CHATS, Im.Chats.CONTACT_ID, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Chats.CONTENT_URI + "/" + rowID);
+ addToQuickSwitch(rowID);
+ }
+ notifyContactContentUri = true;
+ break;
+
+ case MATCH_PRESENCE:
+ rowID = db.replace(TABLE_PRESENCE, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.Presence.CONTENT_URI + "/" + rowID);
+ }
+ notifyContactContentUri = true;
+ break;
+
+ case MATCH_PRESENCE_SEED_BY_ACCOUNT:
+ try {
+ seedInitialPresenceByAccount(Long.parseLong(url.getLastPathSegment()));
+ resultUri = Im.Presence.CONTENT_URI;
+ } catch (NumberFormatException ex) {
+ throw new IllegalArgumentException();
+ }
+ break;
+
+ case MATCH_SESSIONS_BY_PROVIDER:
+ appendValuesFromUrl(initialValues, url, Im.SessionCookies.PROVIDER,
+ Im.SessionCookies.ACCOUNT);
+ // fall through
+ case MATCH_SESSIONS:
+ rowID = db.insert(TABLE_SESSION_COOKIES, null, initialValues);
+ if(rowID > 0) {
+ resultUri = Uri.parse(Im.SessionCookies.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_PROVIDER_SETTINGS:
+ rowID = db.replace(TABLE_PROVIDER_SETTINGS, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.ProviderSettings.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_OUTGOING_RMQ_MESSAGES:
+ rowID = db.insert(TABLE_OUTGOING_RMQ_MESSAGES, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.OutgoingRmq.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_LAST_RMQ_ID:
+ rowID = db.replace(TABLE_LAST_RMQ_ID, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.LastRmqId.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ case MATCH_ACCOUNTS_STATUS:
+ rowID = db.replace(TABLE_ACCOUNT_STATUS, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.AccountStatus.CONTENT_URI + "/" + rowID);
+ }
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_BRANDING_RESOURCE_MAP_CACHE:
+ rowID = db.insert(TABLE_BRANDING_RESOURCE_MAP_CACHE, null, initialValues);
+ if (rowID > 0) {
+ resultUri = Uri.parse(Im.BrandingResourceMapCache.CONTENT_URI + "/" + rowID);
+ }
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Cannot insert into URL: " + url);
+ }
+ // TODO: notify the data change observer?
+
+ if (resultUri != null) {
+ ContentResolver resolver = getContext().getContentResolver();
+
+ // In most case, we query contacts with presence and chats joined, thus
+ // we should also notify that contacts changes when presence or chats changed.
+ if (notifyContactContentUri) {
+ resolver.notifyChange(Im.Contacts.CONTENT_URI, null);
+ }
+
+ if (notifyContactListContentUri) {
+ resolver.notifyChange(Im.ContactList.CONTENT_URI, null);
+ }
+
+ if (notifyMessagesContentUri) {
+ resolver.notifyChange(Im.Messages.CONTENT_URI, null);
+ }
+
+ if (notifyGroupMessagesContentUri) {
+ resolver.notifyChange(Im.GroupMessages.CONTENT_URI, null);
+ }
+
+ if (notifyProviderAccountContentUri) {
+ if (DBG) log("notify insert for " + Im.Provider.CONTENT_URI_WITH_ACCOUNT);
+ resolver.notifyChange(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
+ null);
+ }
+ }
+ return resultUri;
+ }
+
+ private void appendValuesFromUrl(ContentValues values, Uri url, String...columns){
+ if(url.getPathSegments().size() <= columns.length) {
+ throw new IllegalArgumentException("Not enough values in url");
+ }
+ for(int i = 0; i < columns.length; i++){
+ if(values.containsKey(columns[i])){
+ throw new UnsupportedOperationException("Cannot override the value for " + columns[i]);
+ }
+ values.put(columns[i], decodeURLSegment(url.getPathSegments().get(i + 1)));
+ }
+ }
+
+ // Quick-switch management
+ // The chat UI provides slots (0, 9, .., 1) for the first 10 chats. This allows you to
+ // quickly switch between these chats by chording menu+#. We number from the right end of
+ // the number row and move leftward to make an easier two-hand chord with the menu button
+ // on the left side of the keyboard.
+ private void addToQuickSwitch(long newRow) {
+ // Since there are fewer than 10, there must be an empty slot. Let's find it.
+ int slot = findEmptyQuickSwitchSlot();
+
+ if (slot == -1) {
+ return;
+ }
+
+ updateSlotForChat(newRow, slot);
+ }
+
+ // If there are more than 10 chats and one with a quick switch slot ends then pick a chat
+ // that doesn't have a slot and have it inhabit the newly emptied slot.
+ private void backfillQuickSwitchSlots() {
+ // Find all the chats without a quick switch slot, and order
+ Cursor c = query(Im.Chats.CONTENT_URI,
+ BACKFILL_PROJECTION,
+ Im.Chats.SHORTCUT + "=-1", null, Im.Chats.LAST_MESSAGE_DATE + " DESC");
+
+ try {
+ if (c.getCount() < 1) {
+ return;
+ }
+
+ int slot = findEmptyQuickSwitchSlot();
+
+ if (slot != -1) {
+ c.moveToFirst();
+
+ long id = c.getLong(c.getColumnIndex(Im.Chats._ID));
+
+ updateSlotForChat(id, slot);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private int updateSlotForChat(long chatId, int slot) {
+ ContentValues values = new ContentValues();
+
+ values.put(Im.Chats.SHORTCUT, slot);
+
+ return update(Im.Chats.CONTENT_URI, values, Im.Chats._ID + "=?",
+ new String[] { Long.toString(chatId) });
+ }
+
+ private int findEmptyQuickSwitchSlot() {
+ Cursor c = queryInternal(Im.Chats.CONTENT_URI, FIND_SHORTCUT_PROJECTION, null, null, null);
+ final int N = c.getCount();
+
+ try {
+ // If there are 10 or more chats then all the quick switch slots are already filled
+ if (N >= 10) {
+ return -1;
+ }
+
+ int slots = 0;
+ int column = c.getColumnIndex(Im.Chats.SHORTCUT);
+
+ // The map is here because numbers go from 0-9, but we want to assign slots in
+ // 0, 9, 8, ..., 1 order to match the right-to-left reading of the number row
+ // on the keyboard.
+ int[] map = new int[] { 0, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
+
+ // Mark all the slots that are in use
+ // The shortcuts represent actual keyboard number row keys, and not ordinals.
+ // So 7 would mean the shortcut is the 7 key on the keyboard and NOT the 7th
+ // shortcut. The passing of slot through map[] below maps these keyboard key
+ // shortcuts into an ordinal bit position in the 'slots' bitfield.
+ for (c.moveToFirst(); ! c.isAfterLast(); c.moveToNext()) {
+ int slot = c.getInt(column);
+
+ if (slot != -1) {
+ slots |= (1 << map[slot]);
+ }
+ }
+
+ // Try to find an empty one
+ // As we exit this, the push of i through map[] maps the ordinal bit position
+ // in the 'slots' bitfield onto a key on the number row of the device keyboard.
+ // The keyboard key is what is used to designate the shortcut.
+ for (int i = 0; i < 10; i++) {
+ if ((slots & (1 << i)) == 0) {
+ return map[i];
+ }
+ }
+
+ return -1;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * manual trigger for deleting contacts
+ */
+ private static final String DELETE_PRESENCE_SELECTION =
+ Im.Presence.CONTACT_ID + " in (select " +
+ PRESENCE_CONTACT_ID + " from " + TABLE_PRESENCE + " left outer join " + TABLE_CONTACTS +
+ " on " + PRESENCE_CONTACT_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
+
+ private static final String CHATS_CONTACT_ID = TABLE_CHATS + '.' + Im.Chats.CONTACT_ID;
+ private static final String DELETE_CHATS_SELECTION = Im.Chats.CONTACT_ID + " in (select "+
+ CHATS_CONTACT_ID + " from " + TABLE_CHATS + " left outer join " + TABLE_CONTACTS +
+ " on " + CHATS_CONTACT_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
+
+ private static final String GROUP_MEMBER_ID = TABLE_GROUP_MEMBERS + '.' + Im.GroupMembers.GROUP;
+ private static final String DELETE_GROUP_MEMBER_SELECTION =
+ Im.GroupMembers.GROUP + " in (select "+
+ GROUP_MEMBER_ID + " from " + TABLE_GROUP_MEMBERS + " left outer join " + TABLE_CONTACTS +
+ " on " + GROUP_MEMBER_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
+
+ private static final String GROUP_MESSAGES_ID =
+ TABLE_GROUP_MESSAGES + '.' + Im.GroupMessages.GROUP;
+ private static final String DELETE_GROUP_MESSAGES_SELECTION =
+ Im.GroupMessages.GROUP + " in (select "+ GROUP_MESSAGES_ID + " from " +
+ TABLE_GROUP_MESSAGES + " left outer join " + TABLE_CONTACTS + " on " +
+ GROUP_MESSAGES_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
+
+ private void performContactRemovalCleanup(long contactId) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ if (contactId > 0) {
+ deleteWithContactId(db, contactId, TABLE_PRESENCE, Im.Presence.CONTACT_ID);
+ deleteWithContactId(db, contactId, TABLE_CHATS, Im.Chats.CONTACT_ID);
+ deleteWithContactId(db, contactId, TABLE_GROUP_MEMBERS, Im.GroupMembers.GROUP);
+ deleteWithContactId(db, contactId, TABLE_GROUP_MESSAGES, Im.GroupMessages.GROUP);
+ } else {
+ performComplexDelete(db, TABLE_PRESENCE, DELETE_PRESENCE_SELECTION, null);
+ performComplexDelete(db, TABLE_CHATS, DELETE_CHATS_SELECTION, null);
+ performComplexDelete(db, TABLE_GROUP_MEMBERS, DELETE_GROUP_MEMBER_SELECTION, null);
+ performComplexDelete(db, TABLE_GROUP_MESSAGES, DELETE_GROUP_MESSAGES_SELECTION, null);
+ }
+ }
+
+ private void deleteWithContactId(SQLiteDatabase db, long contactId,
+ String tableName, String columnName) {
+ db.delete(tableName, columnName + '=' + contactId, null /* selection args */);
+ }
+
+ private void performComplexDelete(SQLiteDatabase db, String tableName,
+ String selection, String[] selectionArgs) {
+ if (DBG) log("performComplexDelete for table " + tableName + ", selection => " + selection);
+ int count = db.delete(tableName, selection, selectionArgs);
+ if (DBG) log("performComplexDelete: deleted " + count + " rows");
+ }
+
+ public int deleteInternal(Uri url, String userWhere,
+ String[] whereArgs) {
+ String tableToChange;
+ String idColumnName = null;
+ String changedItemId = null;
+
+ StringBuilder whereClause = new StringBuilder();
+ if(userWhere != null) {
+ whereClause.append(userWhere);
+ }
+
+ boolean notifyMessagesContentUri = false;
+ boolean notifyGroupMessagesContentUri = false;
+ boolean notifyContactListContentUri = false;
+ boolean notifyProviderAccountContentUri = false;
+ int match = mUrlMatcher.match(url);
+
+ boolean contactDeleted = false;
+ long deletedContactId = 0;
+
+ boolean backfillQuickSwitchSlots = false;
+
+ switch (match) {
+ case MATCH_PROVIDERS:
+ tableToChange = TABLE_PROVIDERS;
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_ACCOUNTS_BY_ID:
+ changedItemId = url.getPathSegments().get(1);
+ // fall through
+ case MATCH_ACCOUNTS:
+ tableToChange = TABLE_ACCOUNTS;
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_ACCOUNT_STATUS:
+ changedItemId = url.getPathSegments().get(1);
+ // fall through
+ case MATCH_ACCOUNTS_STATUS:
+ tableToChange = TABLE_ACCOUNT_STATUS;
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_CONTACTS:
+ case MATCH_CONTACTS_BAREBONE:
+ tableToChange = TABLE_CONTACTS;
+ contactDeleted = true;
+ break;
+
+ case MATCH_CONTACT:
+ tableToChange = TABLE_CONTACTS;
+ changedItemId = url.getPathSegments().get(1);
+
+ try {
+ deletedContactId = Long.parseLong(changedItemId);
+ } catch (NumberFormatException ex) {
+ }
+
+ contactDeleted = true;
+ break;
+
+ case MATCH_CONTACTS_BY_PROVIDER:
+ tableToChange = TABLE_CONTACTS;
+ appendWhere(whereClause, Im.Contacts.ACCOUNT, "=", url.getPathSegments().get(2));
+ contactDeleted = true;
+ break;
+
+ case MATCH_CONTACTLISTS_BY_PROVIDER:
+ appendWhere(whereClause, Im.ContactList.ACCOUNT, "=",
+ url.getPathSegments().get(2));
+ // fall through
+ case MATCH_CONTACTLISTS:
+ tableToChange = TABLE_CONTACT_LIST;
+ notifyContactListContentUri = true;
+ break;
+
+ case MATCH_CONTACTLIST:
+ tableToChange = TABLE_CONTACT_LIST;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_BLOCKEDLIST:
+ tableToChange = TABLE_BLOCKED_LIST;
+ break;
+
+ case MATCH_BLOCKEDLIST_BY_PROVIDER:
+ tableToChange = TABLE_BLOCKED_LIST;
+ appendWhere(whereClause, Im.BlockedList.ACCOUNT, "=", url.getPathSegments().get(2));
+ break;
+
+ case MATCH_CONTACTS_ETAGS:
+ tableToChange = TABLE_CONTACTS_ETAG;
+ break;
+
+ case MATCH_CONTACTS_ETAG:
+ tableToChange = TABLE_CONTACTS_ETAG;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_MESSAGES:
+ tableToChange = TABLE_MESSAGES;
+ break;
+
+ case MATCH_MESSAGES_BY_CONTACT:
+ tableToChange = TABLE_MESSAGES;
+ appendWhere(whereClause, Im.Messages.ACCOUNT, "=",
+ url.getPathSegments().get(2));
+ appendWhere(whereClause, Im.Messages.CONTACT, "=",
+ decodeURLSegment(url.getPathSegments().get(3)));
+ notifyMessagesContentUri = true;
+ break;
+
+ case MATCH_MESSAGE:
+ tableToChange = TABLE_MESSAGES;
+ changedItemId = url.getPathSegments().get(1);
+ notifyMessagesContentUri = true;
+ break;
+
+ case MATCH_GROUP_MEMBERS:
+ tableToChange = TABLE_GROUP_MEMBERS;
+ break;
+
+ case MATCH_GROUP_MEMBERS_BY_GROUP:
+ tableToChange = TABLE_GROUP_MEMBERS;
+ appendWhere(whereClause, Im.GroupMembers.GROUP, "=", url.getPathSegments().get(1));
+ break;
+
+ case MATCH_GROUP_MESSAGES:
+ tableToChange = TABLE_GROUP_MESSAGES;
+ break;
+
+ case MATCH_GROUP_MESSAGE_BY:
+ tableToChange = TABLE_GROUP_MESSAGES;
+ changedItemId = url.getPathSegments().get(1);
+ idColumnName = Im.GroupMessages.GROUP;
+ notifyGroupMessagesContentUri = true;
+ break;
+
+ case MATCH_GROUP_MESSAGE:
+ tableToChange = TABLE_GROUP_MESSAGES;
+ changedItemId = url.getPathSegments().get(1);
+ notifyGroupMessagesContentUri = true;
+ break;
+
+ case MATCH_INVITATIONS:
+ tableToChange = TABLE_INVITATIONS;
+ break;
+
+ case MATCH_INVITATION:
+ tableToChange = TABLE_INVITATIONS;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_AVATARS:
+ tableToChange = TABLE_AVATARS;
+ break;
+
+ case MATCH_AVATAR:
+ tableToChange = TABLE_AVATARS;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_AVATAR_BY_PROVIDER:
+ tableToChange = TABLE_AVATARS;
+ changedItemId = url.getPathSegments().get(2);
+ idColumnName = Im.Avatars.ACCOUNT;
+ break;
+
+ case MATCH_CHATS:
+ tableToChange = TABLE_CHATS;
+ backfillQuickSwitchSlots = true;
+ break;
+
+ case MATCH_CHATS_BY_ACCOUNT:
+ tableToChange = TABLE_CHATS;
+
+ if (whereClause.length() > 0) {
+ whereClause.append(" AND ");
+ }
+ whereClause.append(Im.Chats.CONTACT_ID);
+ whereClause.append(" in (select ");
+ whereClause.append(Im.Contacts._ID);
+ whereClause.append(" from ");
+ whereClause.append(TABLE_CONTACTS);
+ whereClause.append(" where ");
+ whereClause.append(Im.Contacts.ACCOUNT);
+ whereClause.append("='");
+ whereClause.append(url.getLastPathSegment());
+ whereClause.append("')");
+
+ if (DBG) log("deleteInternal (MATCH_CHATS_BY_ACCOUNT): sel => " +
+ whereClause.toString());
+
+ changedItemId = null;
+ break;
+
+ case MATCH_CHATS_ID:
+ tableToChange = TABLE_CHATS;
+ changedItemId = url.getPathSegments().get(1);
+ idColumnName = Im.Chats.CONTACT_ID;
+ break;
+
+ case MATCH_PRESENCE:
+ tableToChange = TABLE_PRESENCE;
+ break;
+
+ case MATCH_PRESENCE_ID:
+ tableToChange = TABLE_PRESENCE;
+ changedItemId = url.getPathSegments().get(1);
+ idColumnName = Im.Presence.CONTACT_ID;
+ break;
+
+ case MATCH_PRESENCE_BY_ACCOUNT:
+ tableToChange = TABLE_PRESENCE;
+
+ if (whereClause.length() > 0) {
+ whereClause.append(" AND ");
+ }
+ whereClause.append(Im.Presence.CONTACT_ID);
+ whereClause.append(" in (select ");
+ whereClause.append(Im.Contacts._ID);
+ whereClause.append(" from ");
+ whereClause.append(TABLE_CONTACTS);
+ whereClause.append(" where ");
+ whereClause.append(Im.Contacts.ACCOUNT);
+ whereClause.append("='");
+ whereClause.append(url.getLastPathSegment());
+ whereClause.append("')");
+
+ if (DBG) log("deleteInternal (MATCH_PRESENCE_BY_ACCOUNT): sel => " +
+ whereClause.toString());
+
+ changedItemId = null;
+ break;
+
+ case MATCH_SESSIONS:
+ tableToChange = TABLE_SESSION_COOKIES;
+ break;
+
+ case MATCH_SESSIONS_BY_PROVIDER:
+ tableToChange = TABLE_SESSION_COOKIES;
+ changedItemId = url.getPathSegments().get(2);
+ idColumnName = Im.SessionCookies.ACCOUNT;
+ break;
+
+ case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
+ tableToChange = TABLE_PROVIDER_SETTINGS;
+
+ String providerId = url.getPathSegments().get(1);
+ String name = url.getPathSegments().get(2);
+
+ appendWhere(whereClause, Im.ProviderSettings.PROVIDER, "=", providerId);
+ appendWhere(whereClause, Im.ProviderSettings.NAME, "=", name);
+ break;
+
+ case MATCH_OUTGOING_RMQ_MESSAGES:
+ tableToChange = TABLE_OUTGOING_RMQ_MESSAGES;
+ break;
+
+ case MATCH_LAST_RMQ_ID:
+ tableToChange = TABLE_LAST_RMQ_ID;
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Cannot delete that URL: " + url);
+ }
+
+ if (idColumnName == null) {
+ idColumnName = "_id";
+ }
+
+ if (changedItemId != null) {
+ appendWhere(whereClause, idColumnName, "=", changedItemId);
+ }
+
+ if (DBG) log("delete from " + url + " WHERE " + whereClause);
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = db.delete(tableToChange, whereClause.toString(), whereArgs);
+
+ if (contactDeleted && count > 0) {
+ // since the contact cleanup triggers no longer work for cross database tables,
+ // we have to do it by hand here.
+ performContactRemovalCleanup(deletedContactId);
+ }
+
+ if (count > 0) {
+ // In most case, we query contacts with presence and chats joined, thus
+ // we should also notify that contacts changes when presence or chats changed.
+ if (match == MATCH_CHATS || match == MATCH_CHATS_ID
+ || match == MATCH_PRESENCE || match == MATCH_PRESENCE_ID
+ || match == MATCH_CONTACTS_BAREBONE) {
+ getContext().getContentResolver().notifyChange(Im.Contacts.CONTENT_URI, null);
+ } else if (notifyMessagesContentUri) {
+ getContext().getContentResolver().notifyChange(Im.Messages.CONTENT_URI, null);
+ } else if (notifyGroupMessagesContentUri) {
+ getContext().getContentResolver().notifyChange(Im.GroupMessages.CONTENT_URI, null);
+ } else if (notifyContactListContentUri) {
+ getContext().getContentResolver().notifyChange(Im.ContactList.CONTENT_URI, null);
+ } else if (notifyProviderAccountContentUri) {
+ if (DBG) log("notify delete for " + Im.Provider.CONTENT_URI_WITH_ACCOUNT);
+ getContext().getContentResolver().notifyChange(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
+ null);
+ }
+
+ if (backfillQuickSwitchSlots) {
+ backfillQuickSwitchSlots();
+ }
+ }
+
+ return count;
+ }
+
+ public int updateInternal(Uri url, ContentValues values, String userWhere,
+ String[] whereArgs) {
+ String tableToChange;
+ String idColumnName = null;
+ String changedItemId = null;
+ int count;
+
+ StringBuilder whereClause = new StringBuilder();
+ if(userWhere != null) {
+ whereClause.append(userWhere);
+ }
+
+ boolean notifyMessagesContentUri = false;
+ boolean notifyGroupMessagesContentUri = false;
+ boolean notifyContactListContentUri = false;
+ boolean notifyProviderAccountContentUri = false;
+
+ int match = mUrlMatcher.match(url);
+ switch (match) {
+ case MATCH_PROVIDERS_BY_ID:
+ changedItemId = url.getPathSegments().get(1);
+ // fall through
+ case MATCH_PROVIDERS:
+ tableToChange = TABLE_PROVIDERS;
+ break;
+
+ case MATCH_ACCOUNTS_BY_ID:
+ changedItemId = url.getPathSegments().get(1);
+ // fall through
+ case MATCH_ACCOUNTS:
+ tableToChange = TABLE_ACCOUNTS;
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_ACCOUNT_STATUS:
+ changedItemId = url.getPathSegments().get(1);
+ // fall through
+ case MATCH_ACCOUNTS_STATUS:
+ tableToChange = TABLE_ACCOUNT_STATUS;
+ notifyProviderAccountContentUri = true;
+ break;
+
+ case MATCH_CONTACTS:
+ case MATCH_CONTACTS_BAREBONE:
+ tableToChange = TABLE_CONTACTS;
+ break;
+
+ case MATCH_CONTACTS_BY_PROVIDER:
+ tableToChange = TABLE_CONTACTS;
+ changedItemId = url.getPathSegments().get(2);
+ idColumnName = Im.Contacts.ACCOUNT;
+ break;
+
+ case MATCH_CONTACT:
+ tableToChange = TABLE_CONTACTS;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_CONTACTS_BULK:
+ count = updateBulkContacts(values, userWhere);
+ // notify change using the "content://im/contacts" url,
+ // so the change will be observed by listeners interested
+ // in contacts changes.
+ if (count > 0) {
+ getContext().getContentResolver().notifyChange(
+ Im.Contacts.CONTENT_URI, null);
+ }
+ return count;
+
+ case MATCH_CONTACTLIST:
+ tableToChange = TABLE_CONTACT_LIST;
+ changedItemId = url.getPathSegments().get(1);
+ notifyContactListContentUri = true;
+ break;
+
+ case MATCH_CONTACTS_ETAGS:
+ tableToChange = TABLE_CONTACTS_ETAG;
+ break;
+
+ case MATCH_CONTACTS_ETAG:
+ tableToChange = TABLE_CONTACTS_ETAG;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_MESSAGES:
+ tableToChange = TABLE_MESSAGES;
+ break;
+
+ case MATCH_MESSAGES_BY_CONTACT:
+ tableToChange = TABLE_MESSAGES;
+ appendWhere(whereClause, Im.Messages.ACCOUNT, "=",
+ url.getPathSegments().get(2));
+ appendWhere(whereClause, Im.Messages.CONTACT, "=",
+ decodeURLSegment(url.getPathSegments().get(3)));
+ notifyMessagesContentUri = true;
+ break;
+
+ case MATCH_MESSAGE:
+ tableToChange = TABLE_MESSAGES;
+ changedItemId = url.getPathSegments().get(1);
+ notifyMessagesContentUri = true;
+ break;
+
+ case MATCH_GROUP_MESSAGES:
+ tableToChange = TABLE_GROUP_MESSAGES;
+ break;
+
+ case MATCH_GROUP_MESSAGE_BY:
+ tableToChange = TABLE_GROUP_MESSAGES;
+ changedItemId = url.getPathSegments().get(1);
+ idColumnName = Im.GroupMessages.GROUP;
+ notifyGroupMessagesContentUri = true;
+ break;
+
+ case MATCH_GROUP_MESSAGE:
+ tableToChange = TABLE_GROUP_MESSAGES;
+ changedItemId = url.getPathSegments().get(1);
+ notifyGroupMessagesContentUri = true;
+ break;
+
+ case MATCH_AVATARS:
+ tableToChange = TABLE_AVATARS;
+ break;
+
+ case MATCH_AVATAR:
+ tableToChange = TABLE_AVATARS;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_AVATAR_BY_PROVIDER:
+ tableToChange = TABLE_AVATARS;
+ changedItemId = url.getPathSegments().get(2);
+ idColumnName = Im.Avatars.ACCOUNT;
+ break;
+
+ case MATCH_CHATS:
+ tableToChange = TABLE_CHATS;
+ break;
+
+ case MATCH_CHATS_ID:
+ tableToChange = TABLE_CHATS;
+ changedItemId = url.getPathSegments().get(1);
+ idColumnName = Im.Chats.CONTACT_ID;
+ break;
+
+ case MATCH_PRESENCE:
+ //if (DBG) log("update presence: where='" + userWhere + "'");
+ tableToChange = TABLE_PRESENCE;
+ break;
+
+ case MATCH_PRESENCE_ID:
+ tableToChange = TABLE_PRESENCE;
+ changedItemId = url.getPathSegments().get(1);
+ idColumnName = Im.Presence.CONTACT_ID;
+ break;
+
+ case MATCH_PRESENCE_BULK:
+ count = updateBulkPresence(values, userWhere, whereArgs);
+ // notify change using the "content://im/contacts" url,
+ // so the change will be observed by listeners interested
+ // in contacts changes.
+ if (count > 0) {
+ getContext().getContentResolver().notifyChange(Im.Contacts.CONTENT_URI, null);
+ }
+
+ return count;
+
+ case MATCH_INVITATION:
+ tableToChange = TABLE_INVITATIONS;
+ changedItemId = url.getPathSegments().get(1);
+ break;
+
+ case MATCH_SESSIONS:
+ tableToChange = TABLE_SESSION_COOKIES;
+ break;
+
+ case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
+ tableToChange = TABLE_PROVIDER_SETTINGS;
+
+ String providerId = url.getPathSegments().get(1);
+ String name = url.getPathSegments().get(2);
+
+ if (values.containsKey(Im.ProviderSettings.PROVIDER) ||
+ values.containsKey(Im.ProviderSettings.NAME)) {
+ throw new SecurityException("Cannot override the value for provider|name");
+ }
+
+ appendWhere(whereClause, Im.ProviderSettings.PROVIDER, "=", providerId);
+ appendWhere(whereClause, Im.ProviderSettings.NAME, "=", name);
+
+ break;
+
+ case MATCH_OUTGOING_RMQ_MESSAGES:
+ tableToChange = TABLE_OUTGOING_RMQ_MESSAGES;
+ break;
+
+ case MATCH_LAST_RMQ_ID:
+ tableToChange = TABLE_LAST_RMQ_ID;
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Cannot update URL: " + url);
+ }
+
+ if (idColumnName == null) {
+ idColumnName = "_id";
+ }
+ if(changedItemId != null) {
+ appendWhere(whereClause, idColumnName, "=", changedItemId);
+ }
+
+ if (DBG) log("update " + url + " WHERE " + whereClause);
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ count = db.update(tableToChange, values, whereClause.toString(), whereArgs);
+
+ if (count > 0) {
+ // In most case, we query contacts with presence and chats joined, thus
+ // we should also notify that contacts changes when presence or chats changed.
+ if (match == MATCH_CHATS || match == MATCH_CHATS_ID
+ || match == MATCH_PRESENCE || match == MATCH_PRESENCE_ID
+ || match == MATCH_CONTACTS_BAREBONE) {
+ getContext().getContentResolver().notifyChange(Im.Contacts.CONTENT_URI, null);
+ } else if (notifyMessagesContentUri) {
+ if (DBG) log("notify change for " + Im.Messages.CONTENT_URI);
+ getContext().getContentResolver().notifyChange(Im.Messages.CONTENT_URI, null);
+ } else if (notifyGroupMessagesContentUri) {
+ getContext().getContentResolver().notifyChange(Im.GroupMessages.CONTENT_URI, null);
+ } else if (notifyContactListContentUri) {
+ getContext().getContentResolver().notifyChange(Im.ContactList.CONTENT_URI, null);
+ } else if (notifyProviderAccountContentUri) {
+ if (DBG) log("notify change for " + Im.Provider.CONTENT_URI_WITH_ACCOUNT);
+ getContext().getContentResolver().notifyChange(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
+ null);
+ }
+ }
+
+ return count;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ return openFileHelper(uri, mode);
+ }
+
+ private static void appendWhere(StringBuilder where, String columnName,
+ String condition, Object value) {
+ if (where.length() > 0) {
+ where.append(" AND ");
+ }
+ where.append(columnName).append(condition);
+ if(value != null) {
+ DatabaseUtils.appendValueToSql(where, value);
+ }
+ }
+
+ private static void appendWhere(StringBuilder where, String clause) {
+ if (where.length() > 0) {
+ where.append(" AND ");
+ }
+ where.append(clause);
+ }
+
+ private static String decodeURLSegment(String segment) {
+ try {
+ return URLDecoder.decode(segment, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // impossible
+ return segment;
+ }
+ }
+
+ static void log(String message) {
+ Log.d(LOG_TAG, message);
+ }
+}
diff --git a/src/com/android/providers/im/LandingPage.java b/src/com/android/providers/im/LandingPage.java
new file mode 100644
index 0000000..2c09b02
--- /dev/null
+++ b/src/com/android/providers/im/LandingPage.java
@@ -0,0 +1,710 @@
+/*
+ * 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.providers.im;
+
+import android.app.ListActivity;
+import android.app.ActivityManagerNative;
+import android.app.ActivityThread;
+import android.app.Application;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.database.Cursor;
+import android.im.IImPlugin;
+import android.im.ImPluginConsts;
+import android.im.BrandingResourceIDs;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.provider.Im;
+import android.util.Log;
+import android.util.AttributeSet;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.CursorAdapter;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+
+import com.android.providers.im.R;
+
+import dalvik.system.PathClassLoader;
+
+public class LandingPage extends ListActivity implements View.OnCreateContextMenuListener {
+ private static final String TAG = "IM";
+ private final static boolean LOCAL_DEBUG = false;
+
+ private static final int ID_SIGN_IN = Menu.FIRST + 1;
+ private static final int ID_SIGN_OUT = Menu.FIRST + 2;
+ private static final int ID_EDIT_ACCOUNT = Menu.FIRST + 3;
+ private static final int ID_REMOVE_ACCOUNT = Menu.FIRST + 4;
+ private static final int ID_SIGN_OUT_ALL = Menu.FIRST + 5;
+ private static final int ID_ADD_ACCOUNT = Menu.FIRST + 6;
+ private static final int ID_VIEW_CONTACT_LIST = Menu.FIRST + 7;
+ private static final int ID_SETTINGS = Menu.FIRST + 8;
+
+ private ProviderAdapter mAdapter;
+ private Cursor mProviderCursor;
+
+ private static final String[] PROVIDER_PROJECTION = {
+ Im.Provider._ID,
+ Im.Provider.NAME,
+ Im.Provider.FULLNAME,
+ Im.Provider.CATEGORY,
+ Im.Provider.ACTIVE_ACCOUNT_ID,
+ Im.Provider.ACTIVE_ACCOUNT_USERNAME,
+ Im.Provider.ACTIVE_ACCOUNT_PW,
+ Im.Provider.ACTIVE_ACCOUNT_LOCKED,
+ Im.Provider.ACCOUNT_PRESENCE_STATUS,
+ Im.Provider.ACCOUNT_CONNECTION_STATUS,
+ };
+
+ private static final int PROVIDER_ID_COLUMN = 0;
+ private static final int PROVIDER_NAME_COLUMN = 1;
+ private static final int PROVIDER_FULLNAME_COLUMN = 2;
+ private static final int PROVIDER_CATEGORY_COLUMN = 3;
+ private static final int ACTIVE_ACCOUNT_ID_COLUMN = 4;
+ private static final int ACTIVE_ACCOUNT_USERNAME_COLUMN = 5;
+ private static final int ACTIVE_ACCOUNT_PW_COLUMN = 6;
+ private static final int ACTIVE_ACCOUNT_LOCKED = 7;
+ private static final int ACCOUNT_PRESENCE_STATUS = 8;
+ private static final int ACCOUNT_CONNECTION_STATUS = 9;
+
+ private static final String PROVIDER_SELECTION = "providers.name!=?";
+
+ private HashMap<String, PluginInfo> mProviderToPluginMap;
+ private HashMap<Long, PluginInfo> mAccountToPluginMap;
+ private HashMap<Long, BrandingResources> mBrandingResources;
+ private BrandingResources mDefaultBrandingResources;
+
+ private String[] mProviderSelectionArgs = new String[1];
+
+ public class PluginInfo {
+ public IImPlugin mPlugin;
+ /**
+ * The name of the package that the plugin is in.
+ */
+ public String mPackageName;
+
+ /**
+ * The name of the class that implements {@link @ImFrontDoorPlugin} in this plugin.
+ */
+ public String mClassName;
+
+ /**
+ * The full path to the location of the package that the plugin is in.
+ */
+ public String mSrcPath;
+
+ public PluginInfo(IImPlugin plugin, String packageName, String className,
+ String srcPath) {
+ mPackageName = packageName;
+ mClassName = className;
+ mSrcPath = srcPath;
+ mPlugin = plugin;
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setTitle(R.string.landing_page_title);
+
+ if (!loadPlugins()) {
+ Log.e(TAG, "[onCreate] load plugin failed, no plugin found!");
+ finish();
+ return;
+ }
+
+ startPlugins();
+
+ // get everything except for Google Talk.
+ mProviderSelectionArgs[0] = Im.ProviderNames.GTALK;
+ mProviderCursor = managedQuery(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
+ PROVIDER_PROJECTION,
+ PROVIDER_SELECTION /* selection */,
+ mProviderSelectionArgs /* selection args */,
+ Im.Provider.DEFAULT_SORT_ORDER);
+ mAdapter = new ProviderAdapter(this, mProviderCursor);
+ setListAdapter(mAdapter);
+
+ rebuildAccountToPluginMap();
+
+ mBrandingResources = new HashMap<Long, BrandingResources>();
+ loadDefaultBrandingRes();
+ loadBrandingResources();
+
+ registerForContextMenu(getListView());
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+
+ // refresh the accountToPlugin map after mProviderCursor is requeried
+ if (!rebuildAccountToPluginMap()) {
+ Log.w(TAG, "[onRestart] rebuiltAccountToPluginMap failed, reload plugins...");
+
+ if (!loadPlugins()) {
+ Log.e(TAG, "[onRestart] load plugin failed, no plugin found!");
+ finish();
+ return;
+ }
+ rebuildAccountToPluginMap();
+ }
+
+ startPlugins();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ stopPlugins();
+ }
+
+ private boolean loadPlugins() {
+ mProviderToPluginMap = new HashMap<String, PluginInfo>();
+
+ PackageManager pm = getPackageManager();
+ List<ResolveInfo> plugins = pm.queryIntentServices(
+ new Intent(ImPluginConsts.PLUGIN_ACTION_NAME),
+ PackageManager.GET_META_DATA);
+ for (ResolveInfo info : plugins) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) log("loadPlugins: found plugin " + info);
+
+ ServiceInfo serviceInfo = info.serviceInfo;
+ if (serviceInfo == null) {
+ Log.e(TAG, "Ignore bad IM frontdoor plugin: " + info);
+ continue;
+ }
+
+ IImPlugin plugin = null;
+
+ // Load the plug-in directly from the apk instead of binding the service
+ // and calling through the IPC binder API. It's more effective in this way
+ // and we can avoid the async behaviors of binding service.
+ PathClassLoader classLoader = new PathClassLoader(serviceInfo.applicationInfo.sourceDir,
+ getClassLoader());
+ try {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ log("loadPlugin: load class " + serviceInfo.name);
+ }
+ Class cls = classLoader.loadClass(serviceInfo.name);
+ Object newInstance = cls.newInstance();
+ Method m;
+
+ // call "attach" method, so the plugin will get initialized with the proper context
+ m = cls.getMethod("attach", Context.class, ActivityThread.class, String.class,
+ IBinder.class, Application.class, Object.class);
+ m.invoke(newInstance,
+ new Object[] {this, null, serviceInfo.name, null, getApplication(),
+ ActivityManagerNative.getDefault()});
+
+ // call "bind" to get the plugin object
+ m = cls.getMethod("onBind", Intent.class);
+ plugin = (IImPlugin)m.invoke(newInstance, new Object[]{null});
+ } catch (ClassNotFoundException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ } catch (InstantiationException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Failed load the plugin", e);
+ }
+
+ if (plugin != null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) log("loadPlugin: plugin " + plugin + " loaded");
+ ArrayList<String> providers = getSupportedProviders(plugin);
+
+ if (providers == null || providers.size() == 0) {
+ Log.e(TAG, "Ignore bad IM frontdoor plugin: " + info + ". No providers found");
+ continue;
+ }
+
+ PluginInfo pluginInfo = new PluginInfo(plugin,
+ serviceInfo.packageName,
+ serviceInfo.name,
+ serviceInfo.applicationInfo.sourceDir);
+
+ for (String providerName : providers) {
+ mProviderToPluginMap.put(providerName, pluginInfo);
+ }
+ }
+ }
+
+ return mProviderToPluginMap.size() > 0;
+ }
+
+ private void startPlugins() {
+ Iterator<PluginInfo> itor = mProviderToPluginMap.values().iterator();
+
+ while (itor.hasNext()) {
+ PluginInfo pluginInfo = itor.next();
+ try {
+ pluginInfo.mPlugin.onStart();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not start plugin " + pluginInfo.mPackageName, e);
+ }
+ }
+ }
+
+ private void stopPlugins() {
+ Iterator<PluginInfo> itor = mProviderToPluginMap.values().iterator();
+
+ while (itor.hasNext()) {
+ PluginInfo pluginInfo = itor.next();
+ try {
+ pluginInfo.mPlugin.onStop();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not stop plugin " + pluginInfo.mPackageName, e);
+ }
+ }
+ }
+
+ private ArrayList<String> getSupportedProviders(IImPlugin plugin) {
+ ArrayList<String> providers = null;
+
+ try {
+ providers = (ArrayList<String>) plugin.getSupportedProviders();
+ } catch (RemoteException ex) {
+ Log.e(TAG, "getSupportedProviders caught ", ex);
+ }
+
+ return providers;
+ }
+
+ private void loadDefaultBrandingRes() {
+ HashMap<Integer, Integer> resMapping = new HashMap<Integer, Integer>();
+
+ resMapping.put(BrandingResourceIDs.DRAWABLE_LOGO, R.drawable.imlogo_s);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_ONLINE,
+ android.R.drawable.presence_online);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_AWAY,
+ android.R.drawable.presence_away);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_BUSY,
+ android.R.drawable.presence_busy);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_INVISIBLE,
+ android.R.drawable.presence_invisible);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE,
+ android.R.drawable.presence_offline);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_CONTACT_LIST,
+ R.string.menu_view_contact_list);
+
+ mDefaultBrandingResources = new BrandingResources(this, resMapping, null /* default res */);
+ }
+
+ private void loadBrandingResources() {
+ mProviderCursor.moveToFirst();
+ do {
+ long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN);
+ String providerName = mProviderCursor.getString(PROVIDER_NAME_COLUMN);
+ PluginInfo pluginInfo = mProviderToPluginMap.get(providerName);
+
+ if (pluginInfo == null) {
+ Log.w(TAG, "[LandingPage] loadBrandingResources: no plugin found for " + providerName);
+ continue;
+ }
+
+ if (!mBrandingResources.containsKey(providerId)) {
+ BrandingResources res = new BrandingResources(this, pluginInfo, providerName,
+ mDefaultBrandingResources);
+ mBrandingResources.put(providerId, res);
+ }
+ } while (mProviderCursor.moveToNext()) ;
+ }
+
+ public BrandingResources getBrandingResource(long providerId) {
+ BrandingResources res = mBrandingResources.get(providerId);
+ return res == null ? mDefaultBrandingResources : res;
+ }
+
+ private boolean rebuildAccountToPluginMap() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ log("rebuildAccountToPluginMap");
+ }
+
+ if (mAccountToPluginMap != null) {
+ mAccountToPluginMap.clear();
+ }
+
+ mAccountToPluginMap = new HashMap<Long, PluginInfo>();
+
+ mProviderCursor.moveToFirst();
+
+ boolean retVal = true;
+
+ do {
+ long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+
+ if (accountId == 0) {
+ continue;
+ }
+
+ String name = mProviderCursor.getString(PROVIDER_NAME_COLUMN);
+ PluginInfo pluginInfo = mProviderToPluginMap.get(name);
+ if (pluginInfo != null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ log("rebuildAccountToPluginMap: add plugin for acct=" + accountId + ", provider=" + name);
+ }
+ mAccountToPluginMap.put(accountId, pluginInfo);
+ } else {
+ Log.w(TAG, "[LandingPage] no plugin found for " + name);
+ retVal = false;
+ }
+ } while (mProviderCursor.moveToNext()) ;
+
+ return retVal;
+ }
+
+ private void signIn(long accountId) {
+ if (accountId == 0) {
+ Log.w(TAG, "signIn: account id is 0, bail");
+ return;
+ }
+
+ boolean isAccountEditible = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
+ if (isAccountEditible && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) {
+ // no password, edit the account
+ if (Log.isLoggable(TAG, Log.DEBUG)) log("no pw for account " + accountId);
+ Intent intent = getEditAccountIntent();
+ startActivity(intent);
+ return;
+ }
+
+
+ PluginInfo pluginInfo = mAccountToPluginMap.get(accountId);
+ if (pluginInfo == null) {
+ Log.e(TAG, "signIn: cannot find plugin for account " + accountId);
+ return;
+ }
+
+ try {
+ if (Log.isLoggable(TAG, Log.DEBUG)) log("sign in for account " + accountId);
+ pluginInfo.mPlugin.signIn(accountId);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "signIn failed", ex);
+ }
+ }
+
+ boolean isSigningIn(Cursor cursor) {
+ int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS);
+ return connectionStatus == Im.ConnectionStatus.CONNECTING;
+ }
+
+ boolean isSignedIn(Cursor cursor) {
+ int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS);
+ return connectionStatus == Im.ConnectionStatus.ONLINE;
+ }
+
+ private boolean allAccountsSignedOut() {
+ mProviderCursor.moveToFirst();
+ do {
+ if (isSignedIn(mProviderCursor)) {
+ return false;
+ }
+ } while (mProviderCursor.moveToNext()) ;
+
+ return true;
+ }
+
+ private void signoutAll() {
+ do {
+ long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+ signOut(accountId);
+ } while (mProviderCursor.moveToNext()) ;
+ }
+
+ private void signOut(long accountId) {
+ if (accountId == 0) {
+ Log.w(TAG, "signOut: account id is 0, bail");
+ return;
+ }
+
+ PluginInfo pluginInfo = mAccountToPluginMap.get(accountId);
+ if (pluginInfo == null) {
+ Log.e(TAG, "signOut: cannot find plugin for account " + accountId);
+ return;
+ }
+
+ try {
+ if (Log.isLoggable(TAG, Log.DEBUG)) log("sign out for account " + accountId);
+ pluginInfo.mPlugin.signOut(accountId);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "signOut failed", ex);
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(ID_SIGN_OUT_ALL).setVisible(!allAccountsSignedOut());
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, ID_SIGN_OUT_ALL, 0, R.string.menu_sign_out_all)
+ .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case ID_SIGN_OUT_ALL:
+ signoutAll();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return;
+ }
+
+ Cursor providerCursor = (Cursor) getListAdapter().getItem(info.position);
+ menu.setHeaderTitle(providerCursor.getString(PROVIDER_FULLNAME_COLUMN));
+
+ if (providerCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
+ menu.add(0, ID_ADD_ACCOUNT, 0, R.string.menu_add_account);
+ return;
+ }
+
+ long providerId = providerCursor.getLong(PROVIDER_ID_COLUMN);
+ boolean isLoggingIn = isSigningIn(providerCursor);
+ boolean isLoggedIn = isSignedIn(providerCursor);
+
+ if (!isLoggedIn) {
+ menu.add(0, ID_SIGN_IN, 0, R.string.sign_in).setIcon(com.android.internal.R.drawable.ic_menu_login);
+ } else {
+ BrandingResources brandingRes = getBrandingResource(providerId);
+ menu.add(0, ID_VIEW_CONTACT_LIST, 0,
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_CONTACT_LIST));
+ menu.add(0, ID_SIGN_OUT, 0, R.string.menu_sign_out)
+ .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ }
+
+ boolean isAccountEditible = providerCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
+ if (isAccountEditible && !isLoggingIn && !isLoggedIn) {
+ menu.add(0, ID_EDIT_ACCOUNT, 0, R.string.menu_edit_account)
+ .setIcon(android.R.drawable.ic_menu_edit);
+ menu.add(0, ID_REMOVE_ACCOUNT, 0, R.string.menu_remove_account)
+ .setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ // always add a settings menu item
+ menu.add(0, ID_SETTINGS, 0, R.string.menu_settings);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return false;
+ }
+ long providerId = info.id;
+ Cursor providerCursor = (Cursor) getListAdapter().getItem(info.position);
+ long accountId = providerCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+
+ switch (item.getItemId()) {
+ case ID_EDIT_ACCOUNT:
+ {
+ startActivity(getEditAccountIntent());
+ return true;
+ }
+
+ case ID_REMOVE_ACCOUNT:
+ {
+ Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+ getContentResolver().delete(accountUri, null, null);
+ // Requery the cursor to force refreshing screen
+ providerCursor.requery();
+ return true;
+ }
+
+ case ID_VIEW_CONTACT_LIST:
+ {
+ Intent intent = getViewContactsIntent();
+ startActivity(intent);
+ return true;
+ }
+ case ID_ADD_ACCOUNT:
+ {
+ startActivity(getCreateAccountIntent());
+ return true;
+ }
+
+ case ID_SIGN_IN:
+ {
+ signIn(accountId);
+ return true;
+ }
+
+ case ID_SIGN_OUT:
+ {
+ // TODO: progress bar
+ signOut(accountId);
+ return true;
+ }
+
+ case ID_SETTINGS:
+ {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Im.ProviderSettings.CONTENT_URI);
+ intent.addCategory(getProviderCategory(providerCursor));
+ intent.putExtra("providerId", providerId);
+ startActivity(intent);
+ return true;
+ }
+
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ Intent intent = null;
+ mProviderCursor.moveToPosition(position);
+
+ if (mProviderCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
+ // add account
+ intent = getCreateAccountIntent();
+ } else {
+ int state = mProviderCursor.getInt(ACCOUNT_CONNECTION_STATUS);
+
+ if (state == Im.ConnectionStatus.OFFLINE || state == Im.ConnectionStatus.CONNECTING) {
+ boolean isAccountEditible = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
+ if (isAccountEditible && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) {
+ // no password, edit the account
+ intent = getEditAccountIntent();
+ } else {
+ long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+ signIn(accountId);
+ }
+ } else {
+ intent = getViewContactsIntent();
+ }
+ }
+
+ if (intent != null) {
+ startActivity(intent);
+ }
+ }
+
+ Intent getCreateAccountIntent() {
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_INSERT);
+
+ long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN);
+ intent.setData(ContentUris.withAppendedId(Im.Provider.CONTENT_URI, providerId));
+ intent.addCategory(getProviderCategory(mProviderCursor));
+ return intent;
+ }
+
+ Intent getEditAccountIntent() {
+ Intent intent = new Intent(Intent.ACTION_EDIT,
+ ContentUris.withAppendedId(Im.Account.CONTENT_URI,
+ mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN)));
+ intent.addCategory(getProviderCategory(mProviderCursor));
+ return intent;
+ }
+
+ Intent getViewContactsIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Im.Contacts.CONTENT_URI);
+ intent.addCategory(getProviderCategory(mProviderCursor));
+ intent.putExtra("accountId", mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN));
+ return intent;
+ }
+
+ private String getProviderCategory(Cursor cursor) {
+ return cursor.getString(PROVIDER_CATEGORY_COLUMN);
+ }
+
+
+ static void log(String msg) {
+ Log.d(TAG, "[LandingPage]" + msg);
+ }
+
+ private class ProviderListItemFactory implements LayoutInflater.Factory {
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ if (name != null && name.equals(ProviderListItem.class.getName())) {
+ return new ProviderListItem(context, LandingPage.this);
+ }
+ return null;
+ }
+ }
+
+ private final class ProviderAdapter extends CursorAdapter {
+ private LayoutInflater mInflater;
+
+ public ProviderAdapter(Context context, Cursor c) {
+ super(context, c);
+ mInflater = LayoutInflater.from(context).cloneInContext(context);
+ mInflater.setFactory(new ProviderListItemFactory());
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ // create a custom view, so we can manage it ourselves. Mainly, we want to
+ // initialize the widget views (by calling getViewById()) in newView() instead of in
+ // bindView(), which can be called more often.
+ ProviderListItem view = (ProviderListItem) mInflater.inflate(
+ R.layout.account_view, parent, false);
+ view.init(cursor);
+ return view;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ((ProviderListItem) view).bindView(cursor);
+ }
+ }
+
+}
diff --git a/src/com/android/providers/im/ProviderListItem.java b/src/com/android/providers/im/ProviderListItem.java
new file mode 100644
index 0000000..f2f0da8
--- /dev/null
+++ b/src/com/android/providers/im/ProviderListItem.java
@@ -0,0 +1,211 @@
+/*
+ * 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.providers.im;
+
+import android.graphics.drawable.Drawable;
+import android.widget.LinearLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.im.BrandingResourceIDs;
+import android.provider.Im;
+import android.content.res.ColorStateList;
+import android.view.View;
+import android.util.Log;
+import com.android.providers.im.R;
+
+public class ProviderListItem extends LinearLayout {
+ private static final String TAG = "IM";
+ private static final boolean LOCAL_DEBUG = false;
+
+ private LandingPage mActivity;
+ private ImageView mProviderIcon;
+ private ImageView mStatusIcon;
+ private TextView mProviderName;
+ private TextView mLoginName;
+ private TextView mChatView;
+ private View mUnderBubble;
+ private Drawable mBubbleDrawable, mDefaultBackground;
+
+ private int mProviderIdColumn;
+ private int mProviderFullnameColumn;
+ private int mActiveAccountIdColumn;
+ private int mActiveAccountUserNameColumn;
+ private int mAccountPresenceStatusColumn;
+ private int mAccountConnectionStatusColumn;
+
+ private ColorStateList mProviderNameColors;
+ private ColorStateList mLoginNameColors;
+ private ColorStateList mChatViewColors;
+
+ public ProviderListItem(Context context, LandingPage activity) {
+ super(context);
+ mActivity = activity;
+ }
+
+ public void init(Cursor c) {
+ mProviderIcon = (ImageView) findViewById(R.id.providerIcon);
+ mStatusIcon = (ImageView) findViewById(R.id.statusIcon);
+ mProviderName = (TextView) findViewById(R.id.providerName);
+ mLoginName = (TextView) findViewById(R.id.loginName);
+ mChatView = (TextView) findViewById(R.id.conversations);
+ mUnderBubble = (View) findViewById(R.id.underBubble);
+ mBubbleDrawable = getResources().getDrawable(R.drawable.bubble);
+ mDefaultBackground = getResources().getDrawable(R.drawable.default_background);
+
+ mProviderIdColumn = c.getColumnIndexOrThrow(Im.Provider._ID);
+ mProviderFullnameColumn = c.getColumnIndexOrThrow(Im.Provider.FULLNAME);
+ mActiveAccountIdColumn = c.getColumnIndexOrThrow(
+ Im.Provider.ACTIVE_ACCOUNT_ID);
+ mActiveAccountUserNameColumn = c.getColumnIndexOrThrow(
+ Im.Provider.ACTIVE_ACCOUNT_USERNAME);
+ mAccountPresenceStatusColumn = c.getColumnIndexOrThrow(
+ Im.Provider.ACCOUNT_PRESENCE_STATUS);
+ mAccountConnectionStatusColumn = c.getColumnIndexOrThrow(
+ Im.Provider.ACCOUNT_CONNECTION_STATUS);
+
+ mProviderNameColors = mProviderName.getTextColors();
+ mLoginNameColors = mLoginName.getTextColors();
+ mChatViewColors = mChatView.getTextColors();
+ }
+
+ public void bindView(Cursor cursor) {
+ Resources r = getResources();
+ ImageView providerIcon = mProviderIcon;
+ ImageView statusIcon = mStatusIcon;
+ TextView providerName = mProviderName;
+ TextView loginName = mLoginName;
+ TextView chatView = mChatView;
+
+ int providerId = cursor.getInt(mProviderIdColumn);
+ String providerDisplayName = cursor.getString(mProviderFullnameColumn);
+
+ BrandingResources brandingRes = mActivity.getBrandingResource(providerId);
+ providerIcon.setImageDrawable(
+ brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_LOGO));
+
+ mUnderBubble.setBackgroundDrawable(mDefaultBackground);
+ statusIcon.setVisibility(View.GONE);
+
+ providerName.setTextColor(mProviderNameColors);
+ loginName.setTextColor(mLoginNameColors);
+ chatView.setTextColor(mChatViewColors);
+
+ if (!cursor.isNull(mActiveAccountIdColumn)) {
+ mLoginName.setVisibility(View.VISIBLE);
+ providerName.setVisibility(View.VISIBLE);
+ providerName.setText(providerDisplayName);
+
+ long accountId = cursor.getLong(mActiveAccountIdColumn);
+ int connectionStatus = cursor.getInt(mAccountConnectionStatusColumn);
+
+ String secondRowText;
+
+ chatView.setVisibility(View.GONE);
+
+ switch (connectionStatus) {
+ case Im.ConnectionStatus.CONNECTING:
+ secondRowText = r.getString(R.string.signing_in_wait);
+ break;
+
+ case Im.ConnectionStatus.ONLINE:
+ int presenceIconId = getPresenceIconId(cursor);
+ statusIcon.setImageDrawable(
+ brandingRes.getDrawable(presenceIconId));
+ statusIcon.setVisibility(View.VISIBLE);
+ ContentResolver cr = mActivity.getContentResolver();
+
+ int count = getConversationCount(cr, accountId);
+ if (count > 0) {
+ mUnderBubble.setBackgroundDrawable(mBubbleDrawable);
+ chatView.setVisibility(View.VISIBLE);
+ chatView.setText(r.getString(R.string.conversations, count));
+
+ providerName.setTextColor(0xff000000);
+ loginName.setTextColor(0xff000000);
+ chatView.setTextColor(0xff000000);
+ }
+
+ secondRowText = cursor.getString(mActiveAccountUserNameColumn);
+ break;
+
+ default:
+ secondRowText = cursor.getString(mActiveAccountUserNameColumn);
+ break;
+ }
+
+ loginName.setText(secondRowText);
+
+ } else {
+ // No active account, show add account
+ mLoginName.setVisibility(View.GONE);
+
+ mProviderName.setText(providerDisplayName);
+ }
+ }
+
+ private int getConversationCount(ContentResolver cr, long accountId) {
+ // TODO: this is code used to get Google Talk's chat count. Not sure if this will work
+ // for IMPS chat count.
+ StringBuilder where = new StringBuilder();
+ where.append(Im.Chats.CONTACT_ID);
+ where.append(" in (select _id from contacts where ");
+ where.append(Im.Contacts.ACCOUNT);
+ where.append("=");
+ where.append(accountId);
+ where.append(")");
+
+ Cursor cursor = cr.query(Im.Chats.CONTENT_URI, null, where.toString(), null, null);
+
+ try {
+ return cursor.getCount();
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private int getPresenceIconId(Cursor cursor) {
+ int presenceStatus = cursor.getInt(mAccountPresenceStatusColumn);
+
+ if (LOCAL_DEBUG) log("getPresenceIconId: presenceStatus=" + presenceStatus);
+
+ switch (presenceStatus) {
+ case Im.Presence.AVAILABLE:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_ONLINE;
+
+ case Im.Presence.IDLE:
+ case Im.Presence.AWAY:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_AWAY;
+
+ case Im.Presence.DO_NOT_DISTURB:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_BUSY;
+
+ case Im.Presence.INVISIBLE:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_INVISIBLE;
+
+ default:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE;
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}