diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:32:43 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:32:43 -0800 |
commit | 97196e67e000586b0db0b4646334f45925b33fff (patch) | |
tree | 586bb7c733c23ef6e76ad1566a449c9d4b45dfe9 | |
parent | 8420f0b2fed10209358ec58b244e16c51fb8cf7d (diff) | |
download | ImProvider-97196e67e000586b0db0b4646334f45925b33fff.tar.gz |
auto import from //depot/cupcake/@135843
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 @@ -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 Binary files differnew file mode 100644 index 0000000..33cb551 --- /dev/null +++ b/res/drawable/default_background.9.png diff --git a/res/drawable/ic_launcher_im.png b/res/drawable/ic_launcher_im.png Binary files differnew file mode 100644 index 0000000..afc35a2 --- /dev/null +++ b/res/drawable/ic_launcher_im.png diff --git a/res/drawable/im_bubble_highlight.9.png b/res/drawable/im_bubble_highlight.9.png Binary files differnew file mode 100644 index 0000000..9b5588a --- /dev/null +++ b/res/drawable/im_bubble_highlight.9.png diff --git a/res/drawable/im_bubble_normal.9.png b/res/drawable/im_bubble_normal.9.png Binary files differnew file mode 100644 index 0000000..a9b327c --- /dev/null +++ b/res/drawable/im_bubble_normal.9.png diff --git a/res/drawable/im_bubble_pressed.9.png b/res/drawable/im_bubble_pressed.9.png Binary files differnew file mode 100644 index 0000000..3933268 --- /dev/null +++ b/res/drawable/im_bubble_pressed.9.png diff --git a/res/drawable/imlogo_s.png b/res/drawable/imlogo_s.png Binary files differnew file mode 100644 index 0000000..b7aa43a --- /dev/null +++ b/res/drawable/imlogo_s.png 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); + } +} |