diff options
author | Chiao Cheng <chiaocheng@google.com> | 2012-11-13 18:38:05 -0800 |
---|---|---|
committer | Chiao Cheng <chiaocheng@google.com> | 2012-11-13 18:38:05 -0800 |
commit | 273399bf829133a8385332ad43add3c34c889102 (patch) | |
tree | ee74b9e3d8d4269818d3db900a70615d2a8d9942 | |
parent | edefd0315fe9c8e5bfd25f0abb538f861b3d9e39 (diff) | |
download | android_packages_apps_ContactsCommon-273399bf829133a8385332ad43add3c34c889102.tar.gz android_packages_apps_ContactsCommon-273399bf829133a8385332ad43add3c34c889102.tar.bz2 android_packages_apps_ContactsCommon-273399bf829133a8385332ad43add3c34c889102.zip |
Move dependencies of AccountTypeManager into ContactsCommon.
Moving dependencies in preparation to move AccountTypeManager.
Bug: 6993891
Change-Id: I10893209986efd288315dc6b51c7971838ac3923
69 files changed, 6024 insertions, 2 deletions
diff --git a/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/res/drawable-hdpi/ic_menu_remove_field_holo_light.png Binary files differnew file mode 100644 index 00000000..03fd2fb1 --- /dev/null +++ b/res/drawable-hdpi/ic_menu_remove_field_holo_light.png diff --git a/res/drawable-hdpi/ic_text_holo_light.png b/res/drawable-hdpi/ic_text_holo_light.png Binary files differnew file mode 100644 index 00000000..01af1898 --- /dev/null +++ b/res/drawable-hdpi/ic_text_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/res/drawable-mdpi/ic_menu_remove_field_holo_light.png Binary files differnew file mode 100644 index 00000000..8c44e701 --- /dev/null +++ b/res/drawable-mdpi/ic_menu_remove_field_holo_light.png diff --git a/res/drawable-mdpi/ic_text_holo_light.png b/res/drawable-mdpi/ic_text_holo_light.png Binary files differnew file mode 100644 index 00000000..76dae054 --- /dev/null +++ b/res/drawable-mdpi/ic_text_holo_light.png diff --git a/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png Binary files differnew file mode 100644 index 00000000..65a6b7bb --- /dev/null +++ b/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png diff --git a/res/drawable-xhdpi/ic_text_holo_light.png b/res/drawable-xhdpi/ic_text_holo_light.png Binary files differnew file mode 100644 index 00000000..6fb8e92a --- /dev/null +++ b/res/drawable-xhdpi/ic_text_holo_light.png diff --git a/res/layout-sw580dp/text_fields_editor_view.xml b/res/layout-sw580dp/text_fields_editor_view.xml new file mode 100644 index 00000000..89970c6c --- /dev/null +++ b/res/layout-sw580dp/text_fields_editor_view.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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.contacts.editor.TextFieldsEditorView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="horizontal" + android:gravity="center_vertical" + android:focusable="true" + android:clickable="true"> + + <include + android:id="@+id/editors" + layout="@layout/edit_field_list" /> + + <include + android:id="@+id/expansion_view_container" + layout="@layout/edit_expansion_view" + android:visibility="gone" /> + + <include + android:id="@+id/spinner" + layout="@layout/edit_spinner" + android:visibility="gone" /> + + <include + android:id="@+id/delete_button_container" + layout="@layout/edit_delete_button" + android:visibility="gone" /> + + </LinearLayout> + +</com.android.contacts.editor.TextFieldsEditorView> diff --git a/res/layout/edit_date_picker.xml b/res/layout/edit_date_picker.xml new file mode 100644 index 00000000..d9516524 --- /dev/null +++ b/res/layout/edit_date_picker.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- Button to select a date in the contact editor. --> + +<Button + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/date_view" + style="?android:attr/spinnerStyle" + android:layout_width="0dip" + android:layout_height="@dimen/editor_min_line_item_height" + android:layout_weight="1" + android:gravity="center_vertical" + android:layout_marginLeft="@dimen/editor_field_left_padding" + android:layout_marginRight="@dimen/editor_field_right_padding" + android:textAppearance="?android:attr/textAppearanceMedium" + android:paddingLeft="12dip" /> diff --git a/res/layout/edit_delete_button.xml b/res/layout/edit_delete_button.xml new file mode 100644 index 00000000..ca9d8b8d --- /dev/null +++ b/res/layout/edit_delete_button.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- "Delete field" button in the contact editor. --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="@dimen/editor_min_line_item_height" + android:layout_marginRight="2dip" + android:layout_gravity="bottom"> + <ImageView + android:id="@+id/delete_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:duplicateParentState="true" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/ic_menu_remove_field_holo_light" + android:paddingLeft="@dimen/editor_round_button_padding_left" + android:paddingRight="@dimen/editor_round_button_padding_right" + android:paddingTop="@dimen/editor_round_button_padding_top" + android:paddingBottom="@dimen/editor_round_button_padding_bottom" + android:contentDescription="@string/description_minus_button" /> +</FrameLayout> diff --git a/res/layout/edit_expansion_view.xml b/res/layout/edit_expansion_view.xml new file mode 100644 index 00000000..f196a694 --- /dev/null +++ b/res/layout/edit_expansion_view.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- "More" or "less" expansion button in the contact editor. --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="@dimen/editor_min_line_item_height" + android:layout_gravity="top"> + <ImageView + android:id="@+id/expansion_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:duplicateParentState="true" + android:background="?android:attr/selectableItemBackground" + android:paddingLeft="@dimen/editor_round_button_padding_left" + android:paddingRight="@dimen/editor_round_button_padding_right" + android:paddingTop="@dimen/editor_round_button_padding_top" + android:paddingBottom="@dimen/editor_round_button_padding_bottom" /> +</FrameLayout> diff --git a/res/layout/edit_field_list.xml b/res/layout/edit_field_list.xml new file mode 100644 index 00000000..354ea65f --- /dev/null +++ b/res/layout/edit_field_list.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- Layout to contain a list of fields in the contact editor. --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/editors" + android:layout_width="0dip" + android:layout_weight="1" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/editor_field_left_padding" + android:orientation="vertical" /> diff --git a/res/layout/edit_field_list_with_anchor_view.xml b/res/layout/edit_field_list_with_anchor_view.xml new file mode 100644 index 00000000..493226e7 --- /dev/null +++ b/res/layout/edit_field_list_with_anchor_view.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- Layout that behaves similarly to edit_field_list.xml, + but also has an anchor view for ListPopupWindow --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingLeft="@dimen/editor_field_left_padding" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/editors" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + <View + android:id="@+id/anchor_view" + android:layout_width="match_parent" + android:layout_height="0px" /> +</LinearLayout> diff --git a/res/layout/edit_spinner.xml b/res/layout/edit_spinner.xml new file mode 100644 index 00000000..43ac6244 --- /dev/null +++ b/res/layout/edit_spinner.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- Spinner for a field in the contact editor. --> + +<!-- Note: explicitly override the default left and right padding on spinner --> +<Spinner + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/spinner" + android:layout_gravity="bottom" + android:layout_width="@dimen/editor_type_label_width" + android:layout_height="@dimen/editor_min_line_item_height" + android:paddingLeft="0dip" + android:paddingRight="10dip"/> diff --git a/res/layout/event_field_editor_view.xml b/res/layout/event_field_editor_view.xml new file mode 100644 index 00000000..560b9e1d --- /dev/null +++ b/res/layout/event_field_editor_view.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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. +--> + +<!-- Editor for a single event entry in the contact editor --> + +<com.android.contacts.editor.EventFieldEditorView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="@dimen/editor_min_line_item_height" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="horizontal" + android:gravity="center_vertical" + android:focusable="true" + android:clickable="true"> + + <include + android:id="@+id/date_view" + layout="@layout/edit_date_picker" /> + + <Spinner + android:id="@+id/spinner" + android:layout_width="@dimen/editor_type_label_width" + android:layout_height="match_parent" + android:layout_gravity="bottom" + android:paddingLeft="0dip" + android:paddingRight="10dip" + android:visibility="gone"/> + + <include + android:id="@+id/delete_button_container" + layout="@layout/edit_delete_button" + android:visibility="gone" /> + + </LinearLayout> + +</com.android.contacts.editor.EventFieldEditorView> diff --git a/res/layout/name_edit_expansion_view.xml b/res/layout/name_edit_expansion_view.xml new file mode 100644 index 00000000..44c13178 --- /dev/null +++ b/res/layout/name_edit_expansion_view.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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 + --> + +<!-- "More" or "less" expansion button in the contact editor. --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="@dimen/editor_min_line_item_height" + android:layout_gravity="top" + android:contentDescription="@string/expand_collapse_name_fields_description" + android:importantForAccessibility="yes"> + <ImageView + android:id="@+id/expansion_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:duplicateParentState="true" + android:background="?android:attr/selectableItemBackground" + android:paddingLeft="@dimen/editor_round_button_padding_left" + android:paddingRight="@dimen/editor_round_button_padding_right" + android:paddingTop="@dimen/editor_round_button_padding_top" + android:paddingBottom="@dimen/editor_round_button_padding_bottom" /> +</FrameLayout> diff --git a/res/layout/phonetic_name_editor_view.xml b/res/layout/phonetic_name_editor_view.xml new file mode 100644 index 00000000..c0e8827b --- /dev/null +++ b/res/layout/phonetic_name_editor_view.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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.contacts.editor.PhoneticNameEditorView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/editor_min_line_item_height" + android:orientation="vertical"> + + <include + android:id="@+id/spinner" + layout="@layout/edit_spinner" + android:visibility="gone" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical" + android:focusable="true" + android:clickable="true"> + + <include + android:id="@+id/editors" + layout="@layout/edit_field_list" /> + + <include + android:id="@+id/expansion_view_container" + layout="@layout/name_edit_expansion_view" + android:visibility="gone" /> + + <include + android:id="@+id/delete_button_container" + layout="@layout/edit_delete_button" + android:visibility="gone" /> + + </LinearLayout> + +</com.android.contacts.editor.PhoneticNameEditorView> diff --git a/res/layout/structured_name_editor_view.xml b/res/layout/structured_name_editor_view.xml new file mode 100644 index 00000000..4fa5ae17 --- /dev/null +++ b/res/layout/structured_name_editor_view.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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.contacts.editor.StructuredNameEditorView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/editor_min_line_item_height" + android:orientation="vertical"> + + <include + android:id="@+id/spinner" + layout="@layout/edit_spinner" + android:visibility="gone" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:focusable="true" + android:clickable="true"> + + <include + layout="@layout/edit_field_list_with_anchor_view" /> + + <include + android:id="@+id/expansion_view_container" + layout="@layout/name_edit_expansion_view" + android:visibility="gone" /> + + <include + android:id="@+id/delete_button_container" + layout="@layout/edit_delete_button" + android:visibility="gone" /> + + </LinearLayout> + +</com.android.contacts.editor.StructuredNameEditorView> diff --git a/res/layout/text_fields_editor_view.xml b/res/layout/text_fields_editor_view.xml new file mode 100644 index 00000000..6572e4c4 --- /dev/null +++ b/res/layout/text_fields_editor_view.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2012 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.contacts.editor.TextFieldsEditorView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="horizontal" + android:gravity="center_vertical" + android:focusable="true" + android:clickable="true"> + + <include + layout="@layout/edit_field_list_with_anchor_view" /> + + <include + android:id="@+id/expansion_view_container" + layout="@layout/edit_expansion_view" + android:visibility="gone" /> + + <include + android:id="@+id/spinner" + layout="@layout/edit_spinner" + android:visibility="gone" /> + + <include + android:id="@+id/delete_button_container" + layout="@layout/edit_delete_button" + android:visibility="gone" /> + + </LinearLayout> + +</com.android.contacts.editor.TextFieldsEditorView> diff --git a/res/mipmap-hdpi/ic_launcher_contacts.png b/res/mipmap-hdpi/ic_launcher_contacts.png Binary files differnew file mode 100644 index 00000000..e0136f66 --- /dev/null +++ b/res/mipmap-hdpi/ic_launcher_contacts.png diff --git a/res/mipmap-mdpi/ic_launcher_contacts.png b/res/mipmap-mdpi/ic_launcher_contacts.png Binary files differnew file mode 100644 index 00000000..3d490c38 --- /dev/null +++ b/res/mipmap-mdpi/ic_launcher_contacts.png diff --git a/res/mipmap-xhdpi/ic_launcher_contacts.png b/res/mipmap-xhdpi/ic_launcher_contacts.png Binary files differnew file mode 100644 index 00000000..dde3cbb8 --- /dev/null +++ b/res/mipmap-xhdpi/ic_launcher_contacts.png diff --git a/res/mipmap-xxhdpi/ic_launcher_contacts.png b/res/mipmap-xxhdpi/ic_launcher_contacts.png Binary files differnew file mode 100644 index 00000000..99b403bc --- /dev/null +++ b/res/mipmap-xxhdpi/ic_launcher_contacts.png diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml new file mode 100644 index 00000000..50cb55cb --- /dev/null +++ b/res/values-land/dimens.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2012 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> + <dimen name="editor_type_label_width">120dip</dimen> +</resources> diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml index bdbc0d4a..45618244 100644 --- a/res/values-sw580dp/dimens.xml +++ b/res/values-sw580dp/dimens.xml @@ -16,4 +16,9 @@ <resources> <dimen name="detail_item_side_margin">0dip</dimen> + + <dimen name="editor_round_button_padding_left">16dip</dimen> + <dimen name="editor_round_button_padding_right">16dip</dimen> + + <dimen name="editor_type_label_width">122dip</dimen> </resources> diff --git a/res/values-sw680dp/dimens.xml b/res/values-sw680dp/dimens.xml new file mode 100644 index 00000000..b99d0c2e --- /dev/null +++ b/res/values-sw680dp/dimens.xml @@ -0,0 +1,22 @@ +<!-- + ~ Copyright (C) 2012 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> + <dimen name="editor_round_button_padding_left">8dip</dimen> + <dimen name="editor_round_button_padding_right">8dip</dimen> + + <dimen name="editor_type_label_width">180dip</dimen> +</resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 97e6fe78..8901aea1 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -40,4 +40,22 @@ <!-- Top padding of the ListView in the contact tile list --> <dimen name="contact_tile_list_padding_top">0dip</dimen> + + <!-- Minimum height of a row in the Editor --> + <dimen name="editor_min_line_item_height">48dip</dimen> + + <!-- Padding of the rounded plus/minus/expand/collapse buttons in the editor --> + <dimen name="editor_round_button_padding_left">8dip</dimen> + <dimen name="editor_round_button_padding_right">8dip</dimen> + <dimen name="editor_round_button_padding_top">8dip</dimen> + <dimen name="editor_round_button_padding_bottom">8dip</dimen> + + <!-- Right padding of a field in the Editor --> + <dimen name="editor_field_right_padding">4dip</dimen> + + <!-- Left padding of a field in the Editor --> + <dimen name="editor_field_left_padding">4dip</dimen> + + <!-- Width of the Type-Label in the Editor --> + <dimen name="editor_type_label_width">100dip</dimen> </resources> diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml index 8603bb71..f4bf5fef 100644 --- a/res/values/donottranslate_config.xml +++ b/res/values/donottranslate_config.xml @@ -27,4 +27,7 @@ <!-- If true, the default sort order is primary (i.e. by given name) --> <bool name="config_default_display_order_primary">true</bool> + + <!-- If true, the order of name fields in the editor is primary (i.e. given name first) --> + <bool name="config_editor_field_order_primary">true</bool> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index cc855664..fc966eef 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -194,4 +194,176 @@ <!-- Contact list filter selection indicating that the list shows all contacts with phone numbers [CHAR LIMIT=64] --> <string name="list_filter_phones">All contacts with phone numbers</string> + + <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=25] --> + <string name="view_updates_from_group">View updates</string> + + + <!-- Title for data source when creating or editing a contact that doesn't + belong to a specific account. This contact will only exist on the phone + and will not be synced. --> + <string name="account_phone" product="tablet">Tablet-only, unsynced</string> + <!-- Title for data source when creating or editing a contact that doesn't + belong to a specific account. This contact will only exist on the phone + and will not be synced. --> + <string name="account_phone" product="default">Phone-only, unsynced</string> + + <!-- Header that expands to list all name types when editing a structured name of a contact + [CHAR LIMIT=20] --> + <string name="nameLabelsGroup">Name</string> + + <!-- Header that expands to list all nickname types when editing a nickname of a contact + [CHAR LIMIT=20] --> + <string name="nicknameLabelsGroup">Nickname</string> + + <!-- Field title for the full name of a contact [CHAR LIMIT=64]--> + <string name="full_name">Name</string> + <!-- Field title for the given name of a contact --> + <string name="name_given">Given name</string> + <!-- Field title for the family name of a contact --> + <string name="name_family">Family name</string> + <!-- Field title for the prefix name of a contact --> + <string name="name_prefix">Name prefix</string> + <!-- Field title for the middle name of a contact --> + <string name="name_middle">Middle name</string> + <!-- Field title for the suffix name of a contact --> + <string name="name_suffix">Name suffix</string> + + <!-- Field title for the phonetic name of a contact [CHAR LIMIT=64]--> + <string name="name_phonetic">Phonetic name</string> + + <!-- Field title for the phonetic given name of a contact --> + <string name="name_phonetic_given">Phonetic given name</string> + <!-- Field title for the phonetic middle name of a contact --> + <string name="name_phonetic_middle">Phonetic middle name</string> + <!-- Field title for the phonetic family name of a contact --> + <string name="name_phonetic_family">Phonetic family name</string> + + <!-- Header that expands to list all of the types of phone numbers when editing or creating a + phone number for a contact [CHAR LIMIT=20] --> + <string name="phoneLabelsGroup">Phone</string> + + <!-- Header that expands to list all of the types of email addresses when editing or creating + an email address for a contact [CHAR LIMIT=20] --> + <string name="emailLabelsGroup">Email</string> + + <!-- Header that expands to list all of the types of postal addresses when editing or creating + an postal address for a contact [CHAR LIMIT=20] --> + <string name="postalLabelsGroup">Address</string> + + <!-- Header that expands to list all of the types of IM account when editing or creating an IM + account for a contact [CHAR LIMIT=20] --> + <string name="imLabelsGroup">IM</string> + + <!-- Header that expands to list all organization types when editing an organization of a + contact [CHAR LIMIT=20] --> + <string name="organizationLabelsGroup">Organization</string> + + <!-- Header for the list of all relationships for a contact [CHAR LIMIT=20] --> + <string name="relationLabelsGroup">Relationship</string> + + <!-- Header that expands to list all event types when editing an event of a contact + [CHAR LIMIT=20] --> + <string name="eventLabelsGroup">Events</string> + + <!-- Generic action string for text messaging a contact. Used by AccessibilityService to + announce the purpose of the view. [CHAR LIMIT=NONE] --> + <string name="sms">Text message</string> + + <!-- Field title for the full postal address of a contact [CHAR LIMIT=64]--> + <string name="postal_address">Address</string> + + <!-- Hint text for the organization name when editing --> + <string name="ghostData_company">Company</string> + + <!-- Hint text for the organization title when editing --> + <string name="ghostData_title">Title</string> + + <!-- The label describing the Notes field of a contact. This field allows free form text entry + about a contact --> + <string name="label_notes">Notes</string> + + <!-- The label describing the SIP address field of a contact. [CHAR LIMIT=20] --> + <string name="label_sip_address">Internet call</string> + + <!-- Header that expands to list all website types when editing a website of a contact + [CHAR LIMIT=20] --> + <string name="websiteLabelsGroup">Website</string> + + <!-- Header for the list of all groups for a contact [CHAR LIMIT=20] --> + <string name="groupsLabel">Groups</string> + + <!-- Action string for sending an email to a home email address --> + <string name="email_home">Email home</string> + <!-- Action string for sending an email to a mobile email address --> + <string name="email_mobile">Email mobile</string> + <!-- Action string for sending an email to a work email address --> + <string name="email_work">Email work</string> + <!-- Action string for sending an email to an other email address --> + <string name="email_other">Email</string> + <!-- Action string for sending an email to a custom email address --> + <string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string> + + <!-- Generic action string for sending an email --> + <string name="email">Email</string> + + <!-- Field title for the street of a structured postal address of a contact --> + <string name="postal_street">Street</string> + <!-- Field title for the PO box of a structured postal address of a contact --> + <string name="postal_pobox">PO box</string> + <!-- Field title for the neighborhood of a structured postal address of a contact --> + <string name="postal_neighborhood">Neighborhood</string> + <!-- Field title for the city of a structured postal address of a contact --> + <string name="postal_city">City</string> + <!-- Field title for the region, or state, of a structured postal address of a contact --> + <string name="postal_region">State</string> + <!-- Field title for the postal code of a structured postal address of a contact --> + <string name="postal_postcode">ZIP code</string> + <!-- Field title for the country of a structured postal address of a contact --> + <string name="postal_country">Country</string> + + <!-- Action string for viewing a home postal address --> + <string name="map_home">View home address</string> + <!-- Action string for viewing a work postal address --> + <string name="map_work">View work address</string> + <!-- Action string for viewing an other postal address --> + <string name="map_other">View address</string> + <!-- Action string for viewing a custom postal address --> + <string name="map_custom">View <xliff:g id="custom">%s</xliff:g> address</string> + + <!-- Action string for starting an IM chat with the AIM protocol --> + <string name="chat_aim">Chat using AIM</string> + <!-- Action string for starting an IM chat with the MSN or Windows Live protocol --> + <string name="chat_msn">Chat using Windows Live</string> + <!-- Action string for starting an IM chat with the Yahoo protocol --> + <string name="chat_yahoo">Chat using Yahoo</string> + <!-- Action string for starting an IM chat with the Skype protocol --> + <string name="chat_skype">Chat using Skype</string> + <!-- Action string for starting an IM chat with the QQ protocol --> + <string name="chat_qq">Chat using QQ</string> + <!-- Action string for starting an IM chat with the Google Talk protocol --> + <string name="chat_gtalk">Chat using Google Talk</string> + <!-- Action string for starting an IM chat with the ICQ protocol --> + <string name="chat_icq">Chat using ICQ</string> + <!-- Action string for starting an IM chat with the Jabber protocol --> + <string name="chat_jabber">Chat using Jabber</string> + + <!-- Generic action string for starting an IM chat --> + <string name="chat">Chat</string> + + <!-- String describing the Contact Editor Minus button + + Used by AccessibilityService to announce the purpose of the button. + + [CHAR LIMIT=NONE] + --> + <string name="description_minus_button">delete</string> + + <!-- Content description for the expand or collapse name fields button. + Clicking this button causes the name editor to toggle between showing + a single field where the entire name is edited at once, or multiple + fields corresponding to each part of the name (Name Prefix, First Name, + Middle Name, Last Name, Name Suffix). + [CHAR LIMIT=NONE] --> + <string name="expand_collapse_name_fields_description">Expand or collapse name fields</string> </resources> diff --git a/src/com/android/contacts/common/model/account/AccountType.java b/src/com/android/contacts/common/model/account/AccountType.java new file mode 100644 index 00000000..cfafa79c --- /dev/null +++ b/src/com/android/contacts/common/model/account/AccountType.java @@ -0,0 +1,531 @@ +/* + * 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. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.RawContacts; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * Internal structure that represents constraints and styles for a specific data + * source, such as the various data types they support, including details on how + * those types should be rendered and edited. + * <p> + * In the future this may be inflated from XML defined by a data source. + */ +public abstract class AccountType { + private static final String TAG = "AccountType"; + + /** + * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. + */ + public String accountType = null; + + /** + * The {@link RawContacts#DATA_SET} these constraints apply to. + */ + public String dataSet = null; + + /** + * Package that resources should be loaded from. Will be null for embedded types, in which + * case resources are stored in this package itself. + * + * TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and + * {@link #getViewContactNotifyServicePackageName()}. + * + * There's the following invariants: + * - {@link #syncAdapterPackageName} is always set to the actual sync adapter package name. + * - {@link #resourcePackageName} too is set to the same value, unless {@link #isEmbedded()}, + * in which case it'll be null. + * There's an unfortunate exception of {@link FallbackAccountType}. Even though it + * {@link #isEmbedded()}, but we set non-null to {@link #resourcePackageName} for unit tests. + */ + public String resourcePackageName; + /** + * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) + * or the sync adapter (for external type, including extensions). + */ + public String syncAdapterPackageName; + + public int titleRes; + public int iconRes; + + /** + * Set of {@link DataKind} supported by this source. + */ + private ArrayList<DataKind> mKinds = Lists.newArrayList(); + + /** + * Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. + */ + private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap(); + + protected boolean mIsInitialized; + + protected static class DefinitionException extends Exception { + public DefinitionException(String message) { + super(message); + } + + public DefinitionException(String message, Exception inner) { + super(message, inner); + } + } + + /** + * Whether this account type was able to be fully initialized. This may be false if + * (for example) the package name associated with the account type could not be found. + */ + public final boolean isInitialized() { + return mIsInitialized; + } + + /** + * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType}, + * {@link GoogleAccountType} or {@link ExternalAccountType}. + * + * If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns + * {@code false}) it's considered critical, and the application will crash. On the other + * hand if it's not an embedded type, we just skip loading the type. + */ + public boolean isEmbedded() { + return true; + } + + public boolean isExtension() { + return false; + } + + /** + * @return True if contacts can be created and edited using this app. If false, + * there could still be an external editor as provided by + * {@link #getEditContactActivityClassName()} or {@link #getCreateContactActivityClassName()} + */ + public abstract boolean areContactsWritable(); + + /** + * Returns an optional custom edit activity. + * + * Only makes sense for non-embedded account types. + * The activity class should reside in the sync adapter package as determined by + * {@link #syncAdapterPackageName}. + */ + public String getEditContactActivityClassName() { + return null; + } + + /** + * Returns an optional custom new contact activity. + * + * Only makes sense for non-embedded account types. + * The activity class should reside in the sync adapter package as determined by + * {@link #syncAdapterPackageName}. + */ + public String getCreateContactActivityClassName() { + return null; + } + + /** + * Returns an optional custom invite contact activity. + * + * Only makes sense for non-embedded account types. + * The activity class should reside in the sync adapter package as determined by + * {@link #syncAdapterPackageName}. + */ + public String getInviteContactActivityClassName() { + return null; + } + + /** + * Returns an optional service that can be launched whenever a contact is being looked at. + * This allows the sync adapter to provide more up-to-date information. + * + * The service class should reside in the sync adapter package as determined by + * {@link #getViewContactNotifyServicePackageName()}. + */ + public String getViewContactNotifyServiceClassName() { + return null; + } + + /** + * TODO This is way too hacky should be removed. + * + * This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} + * is the authenticator package name but the notification service is in the sync adapter + * package. See {@link #resourcePackageName} -- we should clean up those. + */ + public String getViewContactNotifyServicePackageName() { + return syncAdapterPackageName; + } + + /** Returns an optional Activity string that can be used to view the group. */ + public String getViewGroupActivity() { + return null; + } + + /** Returns an optional Activity string that can be used to view the stream item. */ + public String getViewStreamItemActivity() { + return null; + } + + /** Returns an optional Activity string that can be used to view the stream item photo. */ + public String getViewStreamItemPhotoActivity() { + return null; + } + + public CharSequence getDisplayLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + return getResourceText(context, syncAdapterPackageName, titleRes, accountType); + } + + /** + * @return resource ID for the "invite contact" action label, or -1 if not defined. + */ + protected int getInviteContactActionResId() { + return -1; + } + + /** + * @return resource ID for the "view group" label, or -1 if not defined. + */ + protected int getViewGroupLabelResId() { + return -1; + } + + /** + * Returns {@link AccountTypeWithDataSet} for this type. + */ + public AccountTypeWithDataSet getAccountTypeAndDataSet() { + return AccountTypeWithDataSet.get(accountType, dataSet); + } + + /** + * Returns a list of additional package names that should be inspected as additional + * external account types. This allows for a primary account type to indicate other packages + * that may not be sync adapters but which still provide contact data, perhaps under a + * separate data set within the account. + */ + public List<String> getExtensionPackageNames() { + return new ArrayList<String>(); + } + + /** + * Returns an optional custom label for the "invite contact" action, which will be shown on + * the contact card. (If not defined, returns null.) + */ + public CharSequence getInviteContactActionLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), ""); + } + + /** + * Returns a label for the "view group" action. If not defined, this falls back to our + * own "View Updates" string + */ + public CharSequence getViewGroupLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + final CharSequence customTitle = + getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null); + + return customTitle == null + ? context.getText(R.string.view_updates_from_group) + : customTitle; + } + + /** + * Return a string resource loaded from the given package (or the current package + * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns + * {@code defaultValue}. + * + * (The behavior is undefined if the resource or package doesn't exist.) + */ + @VisibleForTesting + static CharSequence getResourceText(Context context, String packageName, int resId, + String defaultValue) { + if (resId != -1 && packageName != null) { + final PackageManager pm = context.getPackageManager(); + return pm.getText(packageName, resId, null); + } else if (resId != -1) { + return context.getText(resId); + } else { + return defaultValue; + } + } + + public Drawable getDisplayIcon(Context context) { + if (this.titleRes != -1 && this.syncAdapterPackageName != null) { + final PackageManager pm = context.getPackageManager(); + return pm.getDrawable(this.syncAdapterPackageName, this.iconRes, null); + } else if (this.titleRes != -1) { + return context.getResources().getDrawable(this.iconRes); + } else { + return null; + } + } + + /** + * Whether or not groups created under this account type have editable membership lists. + */ + abstract public boolean isGroupMembershipEditable(); + + /** + * {@link Comparator} to sort by {@link DataKind#weight}. + */ + private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() { + @Override + public int compare(DataKind object1, DataKind object2) { + return object1.weight - object2.weight; + } + }; + + /** + * Return list of {@link DataKind} supported, sorted by + * {@link DataKind#weight}. + */ + public ArrayList<DataKind> getSortedDataKinds() { + // TODO: optimize by marking if already sorted + Collections.sort(mKinds, sWeightComparator); + return mKinds; + } + + /** + * Find the {@link DataKind} for a specific MIME-type, if it's handled by + * this data source. + */ + public DataKind getKindForMimetype(String mimeType) { + return this.mMimeKinds.get(mimeType); + } + + /** + * Add given {@link DataKind} to list of those provided by this source. + */ + public DataKind addKind(DataKind kind) throws DefinitionException { + if (kind.mimeType == null) { + throw new DefinitionException("null is not a valid mime type"); + } + if (mMimeKinds.get(kind.mimeType) != null) { + throw new DefinitionException( + "mime type '" + kind.mimeType + "' is already registered"); + } + + kind.resourcePackageName = this.resourcePackageName; + this.mKinds.add(kind); + this.mMimeKinds.put(kind.mimeType, kind); + return kind; + } + + /** + * Description of a specific "type" or "label" of a {@link DataKind} row, + * such as {@link Phone#TYPE_WORK}. Includes constraints on total number of + * rows a {@link Contacts} may have of this type, and details on how + * user-defined labels are stored. + */ + public static class EditType { + public int rawValue; + public int labelRes; + public boolean secondary; + /** + * The number of entries allowed for the type. -1 if not specified. + * @see DataKind#typeOverallMax + */ + public int specificMax; + public String customColumn; + + public EditType(int rawValue, int labelRes) { + this.rawValue = rawValue; + this.labelRes = labelRes; + this.specificMax = -1; + } + + public EditType setSecondary(boolean secondary) { + this.secondary = secondary; + return this; + } + + public EditType setSpecificMax(int specificMax) { + this.specificMax = specificMax; + return this; + } + + public EditType setCustomColumn(String customColumn) { + this.customColumn = customColumn; + return this; + } + + @Override + public boolean equals(Object object) { + if (object instanceof EditType) { + final EditType other = (EditType)object; + return other.rawValue == rawValue; + } + return false; + } + + @Override + public int hashCode() { + return rawValue; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + " rawValue=" + rawValue + + " labelRes=" + labelRes + + " secondary=" + secondary + + " specificMax=" + specificMax + + " customColumn=" + customColumn; + } + } + + public static class EventEditType extends EditType { + private boolean mYearOptional; + + public EventEditType(int rawValue, int labelRes) { + super(rawValue, labelRes); + } + + public boolean isYearOptional() { + return mYearOptional; + } + + public EventEditType setYearOptional(boolean yearOptional) { + mYearOptional = yearOptional; + return this; + } + + @Override + public String toString() { + return super.toString() + " mYearOptional=" + mYearOptional; + } + } + + /** + * Description of a user-editable field on a {@link DataKind} row, such as + * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and + * the column where this field is stored. + */ + public static final class EditField { + public String column; + public int titleRes; + public int inputType; + public int minLines; + public boolean optional; + public boolean shortForm; + public boolean longForm; + + public EditField(String column, int titleRes) { + this.column = column; + this.titleRes = titleRes; + } + + public EditField(String column, int titleRes, int inputType) { + this(column, titleRes); + this.inputType = inputType; + } + + public EditField setOptional(boolean optional) { + this.optional = optional; + return this; + } + + public EditField setShortForm(boolean shortForm) { + this.shortForm = shortForm; + return this; + } + + public EditField setLongForm(boolean longForm) { + this.longForm = longForm; + return this; + } + + public EditField setMinLines(int minLines) { + this.minLines = minLines; + return this; + } + + public boolean isMultiLine() { + return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0; + } + + + @Override + public String toString() { + return this.getClass().getSimpleName() + ":" + + " column=" + column + + " titleRes=" + titleRes + + " inputType=" + inputType + + " minLines=" + minLines + + " optional=" + optional + + " shortForm=" + shortForm + + " longForm=" + longForm; + } + } + + /** + * Generic method of inflating a given {@link ContentValues} into a user-readable + * {@link CharSequence}. For example, an inflater could combine the multiple + * columns of {@link StructuredPostal} together using a string resource + * before presenting to the user. + */ + public interface StringInflater { + public CharSequence inflateUsing(Context context, ContentValues values); + } + + /** + * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the + * current locale. + */ + public static class DisplayLabelComparator implements Comparator<AccountType> { + private final Context mContext; + /** {@link Comparator} for the current locale. */ + private final Collator mCollator = Collator.getInstance(); + + public DisplayLabelComparator(Context context) { + mContext = context; + } + + private String getDisplayLabel(AccountType type) { + CharSequence label = type.getDisplayLabel(mContext); + return (label == null) ? "" : label.toString(); + } + + @Override + public int compare(AccountType lhs, AccountType rhs) { + return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs)); + } + } +} diff --git a/src/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/src/com/android/contacts/common/model/account/AccountTypeWithDataSet.java new file mode 100644 index 00000000..f6bcf246 --- /dev/null +++ b/src/com/android/contacts/common/model/account/AccountTypeWithDataSet.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2011 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.contacts.common.model.account; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; + +import com.google.common.base.Objects; + + +/** + * Encapsulates an "account type" string and a "data set" string. + */ +public class AccountTypeWithDataSet { + + private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID}; + private static final Uri RAW_CONTACTS_URI_LIMIT_1 = RawContacts.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1").build(); + + /** account type. Can be null for fallback type. */ + public final String accountType; + + /** dataSet may be null, but never be "". */ + public final String dataSet; + + private AccountTypeWithDataSet(String accountType, String dataSet) { + this.accountType = TextUtils.isEmpty(accountType) ? null : accountType; + this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet; + } + + public static AccountTypeWithDataSet get(String accountType, String dataSet) { + return new AccountTypeWithDataSet(accountType, dataSet); + } + + /** + * Return true if there are any contacts in the database with this account type and data set. + * Touches DB. Don't use in the UI thread. + */ + public boolean hasData(Context context) { + final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?"; + final String selection; + final String[] args; + if (TextUtils.isEmpty(dataSet)) { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL"; + args = new String[] {accountType}; + } else { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?"; + args = new String[] {accountType, dataSet}; + } + + final Cursor c = context.getContentResolver().query(RAW_CONTACTS_URI_LIMIT_1, + ID_PROJECTION, selection, args, null); + if (c == null) return false; + try { + return c.moveToFirst(); + } finally { + c.close(); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AccountTypeWithDataSet)) return false; + + AccountTypeWithDataSet other = (AccountTypeWithDataSet) o; + return Objects.equal(accountType, other.accountType) + && Objects.equal(dataSet, other.dataSet); + } + + @Override + public int hashCode() { + return (accountType == null ? 0 : accountType.hashCode()) + ^ (dataSet == null ? 0 : dataSet.hashCode()); + } + + @Override + public String toString() { + return "[" + accountType + "/" + dataSet + "]"; + } +} diff --git a/src/com/android/contacts/common/model/account/AccountWithDataSet.java b/src/com/android/contacts/common/model/account/AccountWithDataSet.java new file mode 100644 index 00000000..cdeb2811 --- /dev/null +++ b/src/com/android/contacts/common/model/account/AccountWithDataSet.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2011 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.contacts.common.model.account; + +import android.accounts.Account; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; + +import com.android.internal.util.Objects; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Wrapper for an account that includes a data set (which may be null). + */ +public class AccountWithDataSet extends Account { + private static final String STRINGIFY_SEPARATOR = "\u0001"; + private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002"; + + private static final Pattern STRINGIFY_SEPARATOR_PAT = + Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR)); + private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT = + Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR)); + + public final String dataSet; + private final AccountTypeWithDataSet mAccountTypeWithDataSet; + + private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID}; + private static final Uri RAW_CONTACTS_URI_LIMIT_1 = RawContacts.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1").build(); + + + public AccountWithDataSet(String name, String type, String dataSet) { + super(name, type); + this.dataSet = dataSet; + mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet); + } + + public AccountWithDataSet(Parcel in) { + super(in); + this.dataSet = in.readString(); + mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(dataSet); + } + + // For Parcelable + public static final Creator<AccountWithDataSet> CREATOR = new Creator<AccountWithDataSet>() { + public AccountWithDataSet createFromParcel(Parcel source) { + return new AccountWithDataSet(source); + } + + public AccountWithDataSet[] newArray(int size) { + return new AccountWithDataSet[size]; + } + }; + + public AccountTypeWithDataSet getAccountTypeWithDataSet() { + return mAccountTypeWithDataSet; + } + + /** + * Return {@code true} if this account has any contacts in the database. + * Touches DB. Don't use in the UI thread. + */ + public boolean hasData(Context context) { + final String BASE_SELECTION = + RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?"; + final String selection; + final String[] args; + if (TextUtils.isEmpty(dataSet)) { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL"; + args = new String[] {type, name}; + } else { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?"; + args = new String[] {type, name, dataSet}; + } + + final Cursor c = context.getContentResolver().query(RAW_CONTACTS_URI_LIMIT_1, + ID_PROJECTION, selection, args, null); + if (c == null) return false; + try { + return c.moveToFirst(); + } finally { + c.close(); + } + } + + @Override + public boolean equals(Object o) { + return (o instanceof AccountWithDataSet) && super.equals(o) + && Objects.equal(((AccountWithDataSet) o).dataSet, dataSet); + } + + @Override + public int hashCode() { + return 31 * super.hashCode() + + (dataSet == null ? 0 : dataSet.hashCode()); + } + + @Override + public String toString() { + return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}"; + } + + private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) { + sb.append(account.name); + sb.append(STRINGIFY_SEPARATOR); + sb.append(account.type); + sb.append(STRINGIFY_SEPARATOR); + if (!TextUtils.isEmpty(account.dataSet)) sb.append(account.dataSet); + + return sb; + } + + /** + * Pack the instance into a string. + */ + public String stringify() { + return addStringified(new StringBuilder(), this).toString(); + } + + /** + * Unpack a string created by {@link #stringify}. + * + * @throws IllegalArgumentException if it's an invalid string. + */ + public static AccountWithDataSet unstringify(String s) { + final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3); + if (array.length < 3) { + throw new IllegalArgumentException("Invalid string " + s); + } + return new AccountWithDataSet(array[0], array[1], + TextUtils.isEmpty(array[2]) ? null : array[2]); + } + + /** + * Pack a list of {@link AccountWithDataSet} into a string. + */ + public static String stringifyList(List<AccountWithDataSet> accounts) { + final StringBuilder sb = new StringBuilder(); + + for (AccountWithDataSet account : accounts) { + if (sb.length() > 0) { + sb.append(ARRAY_STRINGIFY_SEPARATOR); + } + addStringified(sb, account); + } + + return sb.toString(); + } + + /** + * Unpack a list of {@link AccountWithDataSet} into a string. + * + * @throws IllegalArgumentException if it's an invalid string. + */ + public static List<AccountWithDataSet> unstringifyList(String s) { + final ArrayList<AccountWithDataSet> ret = Lists.newArrayList(); + if (TextUtils.isEmpty(s)) { + return ret; + } + + final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s); + + for (int i = 0; i < array.length; i++) { + ret.add(unstringify(array[i])); + } + + return ret; + } +} diff --git a/src/com/android/contacts/common/model/account/BaseAccountType.java b/src/com/android/contacts/common/model/account/BaseAccountType.java new file mode 100644 index 00000000..772657a6 --- /dev/null +++ b/src/com/android/contacts/common/model/account/BaseAccountType.java @@ -0,0 +1,1482 @@ +/* + * 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. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.content.res.Resources; +import android.provider.ContactsContract.CommonDataKinds.BaseTypes; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.AttributeSet; +import android.util.Log; +import android.view.inputmethod.EditorInfo; + +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.test.NeededForTesting; +import com.android.contacts.common.util.CommonDateUtils; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public abstract class BaseAccountType extends AccountType { + private static final String TAG = "BaseAccountType"; + + protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE; + protected static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + protected static final int FLAGS_PERSON_NAME = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME; + protected static final int FLAGS_PHONETIC = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC; + protected static final int FLAGS_GENERIC_NAME = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; + protected static final int FLAGS_NOTE = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT; + protected static final int FLAGS_WEBSITE = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_URI; + protected static final int FLAGS_POSTAL = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + protected static final int FLAGS_SIP_ADDRESS = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same + // basic format as email addresses + protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME; + + // Specify the maximum number of lines that can be used to display various field types. If no + // value is specified for a particular type, we use the default value from {@link DataKind}. + protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10; + protected static final int MAX_LINES_FOR_GROUP = 10; + protected static final int MAX_LINES_FOR_NOTE = 100; + + private interface Tag { + static final String DATA_KIND = "DataKind"; + static final String TYPE = "Type"; + } + + private interface Attr { + static final String MAX_OCCURRENCE = "maxOccurs"; + static final String DATE_WITH_TIME = "dateWithTime"; + static final String YEAR_OPTIONAL = "yearOptional"; + static final String KIND = "kind"; + static final String TYPE = "type"; + } + + private interface Weight { + static final int NONE = -1; + static final int ORGANIZATION = 5; + static final int PHONE = 10; + static final int EMAIL = 15; + static final int IM = 20; + static final int STRUCTURED_POSTAL = 25; + static final int NOTE = 110; + static final int NICKNAME = 115; + static final int WEBSITE = 120; + static final int SIP_ADDRESS = 130; + static final int EVENT = 150; + static final int RELATIONSHIP = 160; + static final int GROUP_MEMBERSHIP = 999; + } + + public BaseAccountType() { + this.accountType = null; + this.dataSet = null; + this.titleRes = R.string.account_phone; + this.iconRes = R.mipmap.ic_launcher_contacts; + } + + protected static EditType buildPhoneType(int type) { + return new EditType(type, Phone.getTypeLabelResource(type)); + } + + protected static EditType buildEmailType(int type) { + return new EditType(type, Email.getTypeLabelResource(type)); + } + + protected static EditType buildPostalType(int type) { + return new EditType(type, StructuredPostal.getTypeLabelResource(type)); + } + + protected static EditType buildImType(int type) { + return new EditType(type, Im.getProtocolLabelResource(type)); + } + + protected static EditType buildEventType(int type, boolean yearOptional) { + return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional); + } + + protected static EditType buildRelationType(int type) { + return new EditType(type, Relation.getTypeLabelResource(type)); + } + + protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE, + R.string.nameLabelsGroup, -1, true, R.layout.structured_name_editor_view)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, + R.string.full_name, FLAGS_PERSON_NAME)); + kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME, + R.string.name_phonetic_middle, FLAGS_PHONETIC)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME, + R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + R.string.nameLabelsGroup, -1, true, R.layout.text_fields_editor_view)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, + R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true)); + + boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + + if (!displayOrderPrimary) { + kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix, + FLAGS_PERSON_NAME).setLongForm(true)); + } else { + kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family, + FLAGS_PERSON_NAME).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix, + FLAGS_PERSON_NAME).setLongForm(true)); + } + + return kind; + } + + protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + R.string.name_phonetic, -1, true, R.layout.phonetic_name_editor_view)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, + R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME, + R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME, + R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true)); + + return kind; + } + + protected DataKind addDataKindNickname(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Nickname.CONTENT_ITEM_TYPE, + R.string.nicknameLabelsGroup, 115, true, R.layout.text_fields_editor_view)); + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, + FLAGS_PERSON_NAME)); + + return kind; + } + + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, + 10, true, R.layout.text_fields_editor_view)); + kind.iconAltRes = R.drawable.ic_text_holo_light; + kind.iconAltDescriptionRes = R.string.sms; + kind.actionHeader = new PhoneActionInflater(); + kind.actionAltHeader = new PhoneActionAltInflater(); + kind.actionBody = new SimpleInflater(Phone.NUMBER); + kind.typeColumn = Phone.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, + 15, true, R.layout.text_fields_editor_view)); + kind.actionHeader = new EmailActionInflater(); + kind.actionBody = new SimpleInflater(Email.DATA); + kind.typeColumn = Email.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add(buildEmailType(Email.TYPE_MOBILE)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(StructuredPostal.CONTENT_ITEM_TYPE, + R.string.postalLabelsGroup, 25, true, R.layout.text_fields_editor_view)); + kind.actionHeader = new PostalActionInflater(); + kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_CUSTOM).setSecondary(true) + .setCustomColumn(StructuredPostal.LABEL)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add( + new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, + FLAGS_POSTAL)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS; + + return kind; + } + + protected DataKind addDataKindIm(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, 20, true, + R.layout.text_fields_editor_view)); + kind.actionHeader = new ImActionInflater(); + kind.actionBody = new SimpleInflater(Im.DATA); + + // NOTE: even though a traditional "type" exists, for editing + // purposes we're using the protocol to pick labels + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + kind.typeColumn = Im.PROTOCOL; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildImType(Im.PROTOCOL_AIM)); + kind.typeList.add(buildImType(Im.PROTOCOL_MSN)); + kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO)); + kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE)); + kind.typeList.add(buildImType(Im.PROTOCOL_QQ)); + kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK)); + kind.typeList.add(buildImType(Im.PROTOCOL_ICQ)); + kind.typeList.add(buildImType(Im.PROTOCOL_JABBER)); + kind.typeList.add(buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn( + Im.CUSTOM_PROTOCOL)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + protected DataKind addDataKindOrganization(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Organization.CONTENT_ITEM_TYPE, + R.string.organizationLabelsGroup, 5, true, + R.layout.text_fields_editor_view)); + kind.actionHeader = new SimpleInflater(Organization.COMPANY); + kind.actionBody = new SimpleInflater(Organization.TITLE); + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company, + FLAGS_GENERIC_NAME)); + kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title, + FLAGS_GENERIC_NAME)); + + return kind; + } + + protected DataKind addDataKindPhoto(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, true, -1)); + kind.typeOverallMax = 1; + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + return kind; + } + + protected DataKind addDataKindNote(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Note.CONTENT_ITEM_TYPE, + R.string.label_notes, 110, true, R.layout.text_fields_editor_view)); + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.label_notes); + kind.actionBody = new SimpleInflater(Note.NOTE); + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE; + + return kind; + } + + protected DataKind addDataKindWebsite(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Website.CONTENT_ITEM_TYPE, + R.string.websiteLabelsGroup, 120, true, R.layout.text_fields_editor_view)); + kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup); + kind.actionBody = new SimpleInflater(Website.URL); + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + return kind; + } + + protected DataKind addDataKindSipAddress(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(SipAddress.CONTENT_ITEM_TYPE, + R.string.label_sip_address, 130, true, R.layout.text_fields_editor_view)); + + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.label_sip_address); + kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS); + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS, + R.string.label_sip_address, FLAGS_SIP_ADDRESS)); + + return kind; + } + + protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE, + R.string.groupsLabel, 999, true, -1)); + + kind.typeOverallMax = 1; + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP; + + return kind; + } + + /** + * Simple inflater that assumes a string resource has a "%s" that will be + * filled from the given column. + */ + public static class SimpleInflater implements StringInflater { + private final int mStringRes; + private final String mColumnName; + + public SimpleInflater(int stringRes) { + this(stringRes, null); + } + + public SimpleInflater(String columnName) { + this(-1, columnName); + } + + public SimpleInflater(int stringRes, String columnName) { + mStringRes = stringRes; + mColumnName = columnName; + } + + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final boolean validColumn = values.containsKey(mColumnName); + final boolean validString = mStringRes > 0; + + final CharSequence stringValue = validString ? context.getText(mStringRes) : null; + final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null; + + if (validString && validColumn) { + return String.format(stringValue.toString(), columnValue); + } else if (validString) { + return stringValue; + } else if (validColumn) { + return columnValue; + } else { + return null; + } + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + " mStringRes=" + mStringRes + + " mColumnName" + mColumnName; + } + + @NeededForTesting + public String getColumnNameForTest() { + return mColumnName; + } + } + + public static abstract class CommonInflater implements StringInflater { + protected abstract int getTypeLabelResource(Integer type); + + protected boolean isCustom(Integer type) { + return type == BaseTypes.TYPE_CUSTOM; + } + + protected String getTypeColumn() { + return Phone.TYPE; + } + + protected String getLabelColumn() { + return Phone.LABEL; + } + + protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) { + final int labelRes = getTypeLabelResource(type); + if (type == null) { + return res.getText(labelRes); + } else if (isCustom(type)) { + return res.getString(labelRes, label == null ? "" : label); + } else { + return res.getText(labelRes); + } + } + + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final Integer type = values.getAsInteger(getTypeColumn()); + final String label = values.getAsString(getLabelColumn()); + return getTypeLabel(context.getResources(), type, label); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + } + + public static class PhoneActionInflater extends CommonInflater { + @Override + protected boolean isCustom(Integer type) { + return ContactDisplayUtils.isCustomPhoneType(type); + } + + @Override + protected int getTypeLabelResource(Integer type) { + return ContactDisplayUtils.getPhoneLabelResourceId(type); + } + } + + public static class PhoneActionAltInflater extends CommonInflater { + @Override + protected boolean isCustom(Integer type) { + return ContactDisplayUtils.isCustomPhoneType(type); + } + + @Override + protected int getTypeLabelResource(Integer type) { + return ContactDisplayUtils.getSmsLabelResourceId(type); + } + } + + public static class EmailActionInflater extends CommonInflater { + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) return R.string.email; + switch (type) { + case Email.TYPE_HOME: return R.string.email_home; + case Email.TYPE_WORK: return R.string.email_work; + case Email.TYPE_OTHER: return R.string.email_other; + case Email.TYPE_MOBILE: return R.string.email_mobile; + default: return R.string.email_custom; + } + } + } + + public static class EventActionInflater extends CommonInflater { + @Override + protected int getTypeLabelResource(Integer type) { + return Event.getTypeResource(type); + } + } + + public static class RelationActionInflater extends CommonInflater { + @Override + protected int getTypeLabelResource(Integer type) { + return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type); + } + } + + public static class PostalActionInflater extends CommonInflater { + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) return R.string.map_other; + switch (type) { + case StructuredPostal.TYPE_HOME: return R.string.map_home; + case StructuredPostal.TYPE_WORK: return R.string.map_work; + case StructuredPostal.TYPE_OTHER: return R.string.map_other; + default: return R.string.map_custom; + } + } + } + + public static class ImActionInflater extends CommonInflater { + @Override + protected String getTypeColumn() { + return Im.PROTOCOL; + } + + @Override + protected String getLabelColumn() { + return Im.CUSTOM_PROTOCOL; + } + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) return R.string.chat; + switch (type) { + case Im.PROTOCOL_AIM: return R.string.chat_aim; + case Im.PROTOCOL_MSN: return R.string.chat_msn; + case Im.PROTOCOL_YAHOO: return R.string.chat_yahoo; + case Im.PROTOCOL_SKYPE: return R.string.chat_skype; + case Im.PROTOCOL_QQ: return R.string.chat_qq; + case Im.PROTOCOL_GOOGLE_TALK: return R.string.chat_gtalk; + case Im.PROTOCOL_ICQ: return R.string.chat_icq; + case Im.PROTOCOL_JABBER: return R.string.chat_jabber; + case Im.PROTOCOL_NETMEETING: return R.string.chat; + default: return R.string.chat; + } + } + } + + @Override + public boolean isGroupMembershipEditable() { + return false; + } + + /** + * Parses the content of the EditSchema tag in contacts.xml. + */ + protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException, DefinitionException { + + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + final int depth = parser.getDepth(); + if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) { + continue; // Not direct child tag + } + + final String tag = parser.getName(); + + if (Tag.DATA_KIND.equals(tag)) { + for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) { + addKind(kind); + } + } else { + Log.w(TAG, "Skipping unknown tag " + tag); + } + } + } + + // Utility methods to keep code shorter. + private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) { + return attrs.getAttributeBooleanValue(null, attribute, defaultValue); + } + + private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) { + return attrs.getAttributeIntValue(null, attribute, defaultValue); + } + + private static String getAttr(AttributeSet attrs, String attribute) { + return attrs.getAttributeValue(null, attribute); + } + + // TODO Extract it to its own class, and move all KindBuilders to it as well. + private static class KindParser { + public static final KindParser INSTANCE = new KindParser(); + + private final Map<String, KindBuilder> mBuilders = Maps.newHashMap(); + + private KindParser() { + addBuilder(new NameKindBuilder()); + addBuilder(new NicknameKindBuilder()); + addBuilder(new PhoneKindBuilder()); + addBuilder(new EmailKindBuilder()); + addBuilder(new StructuredPostalKindBuilder()); + addBuilder(new ImKindBuilder()); + addBuilder(new OrganizationKindBuilder()); + addBuilder(new PhotoKindBuilder()); + addBuilder(new NoteKindBuilder()); + addBuilder(new WebsiteKindBuilder()); + addBuilder(new SipAddressKindBuilder()); + addBuilder(new GroupMembershipKindBuilder()); + addBuilder(new EventKindBuilder()); + addBuilder(new RelationshipKindBuilder()); + } + + private void addBuilder(KindBuilder builder) { + mBuilders.put(builder.getTagName(), builder); + } + + /** + * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns + * {@link DataKind}s. (Usually just one, but there are three for the "name" kind.) + * + * This method returns a list, because we need to add 3 kinds for the name data kind. + * (structured, display and phonetic) + */ + public List<DataKind> parseDataKindTag(Context context, XmlPullParser parser, + AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final String kind = getAttr(attrs, Attr.KIND); + final KindBuilder builder = mBuilders.get(kind); + if (builder != null) { + return builder.parseDataKind(context, parser, attrs); + } else { + throw new DefinitionException("Undefined data kind '" + kind + "'"); + } + } + } + + private static abstract class KindBuilder { + + public abstract String getTagName(); + + /** + * DataKind tag parser specific to each kind. Subclasses must implement it. + */ + public abstract List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, IOException; + + /** + * Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind + * tag. + */ + protected final DataKind newDataKind(Context context, XmlPullParser parser, + AttributeSet attrs, boolean isPseudo, String mimeType, String typeColumn, + int titleRes, int weight, int editorLayoutResourceId, + StringInflater actionHeader, StringInflater actionBody) + throws DefinitionException, XmlPullParserException, IOException { + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Adding DataKind: " + mimeType); + } + + final DataKind kind = new DataKind(mimeType, titleRes, weight, true, + editorLayoutResourceId); + kind.typeColumn = typeColumn; + kind.actionHeader = actionHeader; + kind.actionBody = actionBody; + kind.fieldList = Lists.newArrayList(); + + // Get more information from the tag... + // A pseudo data kind doesn't have corresponding tag the XML, so we skip this. + if (!isPseudo) { + kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1); + + // Process "Type" tags. + // If a kind has the type column, contacts.xml must have at least one type + // definition. Otherwise, it mustn't have a type definition. + if (kind.typeColumn != null) { + // Parse and add types. + kind.typeList = Lists.newArrayList(); + parseTypes(context, parser, attrs, kind, true); + if (kind.typeList.size() == 0) { + throw new DefinitionException( + "Kind " + kind.mimeType + " must have at least one type"); + } + } else { + // Make sure it has no types. + parseTypes(context, parser, attrs, kind, false /* can't have types */); + } + } + + return kind; + } + + /** + * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds + * them to the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, + * so throws {@link DefinitionException}. + */ + private void parseTypes(Context context, XmlPullParser parser, AttributeSet attrs, + DataKind kind, boolean canHaveTypes) + throws DefinitionException, XmlPullParserException, IOException { + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + final int depth = parser.getDepth(); + if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) { + continue; // Not direct child tag + } + + final String tag = parser.getName(); + if (Tag.TYPE.equals(tag)) { + if (canHaveTypes) { + kind.typeList.add(parseTypeTag(parser, attrs, kind)); + } else { + throw new DefinitionException( + "Kind " + kind.mimeType + " can't have types"); + } + } else { + throw new DefinitionException("Unknown tag: " + tag); + } + } + } + + /** + * Parses a single Type element and returns an {@link EditType} built from it. Uses + * {@link #buildEditTypeForTypeTag} defined in subclasses to actually build an + * {@link EditType}. + */ + private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind) + throws DefinitionException { + + final String typeName = getAttr(attrs, Attr.TYPE); + + final EditType et = buildEditTypeForTypeTag(attrs, typeName); + if (et == null) { + throw new DefinitionException( + "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'"); + } + et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1); + + return et; + } + + /** + * Returns an {@link EditType} for the given "type". Subclasses may optionally use + * the attributes in the tag to set optional values. + * (e.g. "yearOptional" for the event kind) + */ + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + return null; + } + + protected final void throwIfList(DataKind kind) throws DefinitionException { + if (kind.typeOverallMax != 1) { + throw new DefinitionException( + "Kind " + kind.mimeType + " must have 'overallMax=\"1\"'"); + } + } + } + + /** + * DataKind parser for Name. (structured, display, phonetic) + */ + private static class NameKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "name"; + } + + private static void checkAttributeTrue(boolean value, String attrName) + throws DefinitionException { + if (!value) { + throw new DefinitionException(attrName + " must be true"); + } + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + + // Build 3 data kinds: + // - StructuredName.CONTENT_ITEM_TYPE + // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME + // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME + + final boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + + final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false); + final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false); + final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false); + final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false); + final boolean supportsPhoneticFamilyName = + getAttr(attrs, "supportsPhoneticFamilyName", false); + final boolean supportsPhoneticMiddleName = + getAttr(attrs, "supportsPhoneticMiddleName", false); + final boolean supportsPhoneticGivenName = + getAttr(attrs, "supportsPhoneticGivenName", false); + + // For now, every things must be supported. + checkAttributeTrue(supportsDisplayName, "supportsDisplayName"); + checkAttributeTrue(supportsPrefix, "supportsPrefix"); + checkAttributeTrue(supportsMiddleName, "supportsMiddleName"); + checkAttributeTrue(supportsSuffix, "supportsSuffix"); + checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName"); + checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName"); + checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName"); + + final List<DataKind> kinds = Lists.newArrayList(); + + // Structured name + final DataKind ks = newDataKind(context, parser, attrs, false, + StructuredName.CONTENT_ITEM_TYPE, null, R.string.nameLabelsGroup, Weight.NONE, + R.layout.structured_name_editor_view, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + + throwIfList(ks); + kinds.add(ks); + + // Note about setLongForm/setShortForm below. + // We need to set this only when the type supports display name. (=supportsDisplayName) + // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields + // "optional". + + ks.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, + FLAGS_PERSON_NAME)); + ks.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setLongForm(true)); + ks.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family, + FLAGS_PERSON_NAME).setLongForm(true)); + ks.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, + FLAGS_PERSON_NAME).setLongForm(true)); + ks.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given, + FLAGS_PERSON_NAME).setLongForm(true)); + ks.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix, + FLAGS_PERSON_NAME).setLongForm(true)); + ks.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, FLAGS_PHONETIC)); + ks.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME, + R.string.name_phonetic_middle, FLAGS_PHONETIC)); + ks.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME, + R.string.name_phonetic_given, FLAGS_PHONETIC)); + + // Display name + final DataKind kd = newDataKind(context, parser, attrs, true, + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, null, + R.string.nameLabelsGroup, Weight.NONE, R.layout.text_fields_editor_view, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + kd.typeOverallMax = 1; + kinds.add(kd); + + kd.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, + R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true)); + + if (!displayOrderPrimary) { + kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix, + FLAGS_PERSON_NAME).setLongForm(true)); + } else { + kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family, + FLAGS_PERSON_NAME).setLongForm(true)); + kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix, + FLAGS_PERSON_NAME).setLongForm(true)); + } + + // Phonetic name + final DataKind kp = newDataKind(context, parser, attrs, true, + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, null, + R.string.name_phonetic, Weight.NONE, R.layout.phonetic_name_editor_view, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + kp.typeOverallMax = 1; + kinds.add(kp); + + // We may want to change the order depending on displayOrderPrimary too. + kp.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, + R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true)); + kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true)); + kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME, + R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true)); + kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME, + R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true)); + return kinds; + } + } + + private static class NicknameKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "nickname"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Nickname.CONTENT_ITEM_TYPE, null, R.string.nicknameLabelsGroup, Weight.NICKNAME, + R.layout.text_fields_editor_view, + new SimpleInflater(R.string.nicknameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + + kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, + FLAGS_PERSON_NAME)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT); + + throwIfList(kind); + return Lists.newArrayList(kind); + } + } + + private static class PhoneKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "phone"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Phone.CONTENT_ITEM_TYPE, Phone.TYPE, R.string.phoneLabelsGroup, Weight.PHONE, + R.layout.text_fields_editor_view, + new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER)); + + kind.iconAltRes = R.drawable.ic_text_holo_light; + kind.iconAltDescriptionRes = R.string.sms; + kind.actionAltHeader = new PhoneActionAltInflater(); + + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return Lists.newArrayList(kind); + } + + /** Just to avoid line-wrapping... */ + protected static EditType build(int type, boolean secondary) { + return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary); + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + if ("home".equals(type)) return build(Phone.TYPE_HOME, false); + if ("mobile".equals(type)) return build(Phone.TYPE_MOBILE, false); + if ("work".equals(type)) return build(Phone.TYPE_WORK, false); + if ("fax_work".equals(type)) return build(Phone.TYPE_FAX_WORK, true); + if ("fax_home".equals(type)) return build(Phone.TYPE_FAX_HOME, true); + if ("pager".equals(type)) return build(Phone.TYPE_PAGER, true); + if ("other".equals(type)) return build(Phone.TYPE_OTHER, false); + if ("callback".equals(type)) return build(Phone.TYPE_CALLBACK, true); + if ("car".equals(type)) return build(Phone.TYPE_CAR, true); + if ("company_main".equals(type)) return build(Phone.TYPE_COMPANY_MAIN, true); + if ("isdn".equals(type)) return build(Phone.TYPE_ISDN, true); + if ("main".equals(type)) return build(Phone.TYPE_MAIN, true); + if ("other_fax".equals(type)) return build(Phone.TYPE_OTHER_FAX, true); + if ("radio".equals(type)) return build(Phone.TYPE_RADIO, true); + if ("telex".equals(type)) return build(Phone.TYPE_TELEX, true); + if ("tty_tdd".equals(type)) return build(Phone.TYPE_TTY_TDD, true); + if ("work_mobile".equals(type)) return build(Phone.TYPE_WORK_MOBILE, true); + if ("work_pager".equals(type)) return build(Phone.TYPE_WORK_PAGER, true); + + // Note "assistant" used to be a custom column for the fallback type, but not anymore. + if ("assistant".equals(type)) return build(Phone.TYPE_ASSISTANT, true); + if ("mms".equals(type)) return build(Phone.TYPE_MMS, true); + if ("custom".equals(type)) { + return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL); + } + return null; + } + } + + private static class EmailKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "email"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Email.CONTENT_ITEM_TYPE, Email.TYPE, R.string.emailLabelsGroup, Weight.EMAIL, + R.layout.text_fields_editor_view, + new EmailActionInflater(), new SimpleInflater(Email.DATA)); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return Lists.newArrayList(kind); + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("home".equals(type)) return buildEmailType(Email.TYPE_HOME); + if ("work".equals(type)) return buildEmailType(Email.TYPE_WORK); + if ("other".equals(type)) return buildEmailType(Email.TYPE_OTHER); + if ("mobile".equals(type)) return buildEmailType(Email.TYPE_MOBILE); + if ("custom".equals(type)) { + return buildEmailType(Email.TYPE_CUSTOM) + .setSecondary(true).setCustomColumn(Email.LABEL); + } + return null; + } + } + + private static class StructuredPostalKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "postal"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, + R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL, + R.layout.text_fields_editor_view, new PostalActionInflater(), + new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS)); + + if (getAttr(attrs, "needsStructured", false)) { + if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) { + // Japanese order + kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, + R.string.postal_country, FLAGS_POSTAL).setOptional(true)); + kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, + R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.REGION, + R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, + R.string.postal_city,FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.STREET, + R.string.postal_street, FLAGS_POSTAL)); + } else { + // Generic order + kind.fieldList.add(new EditField(StructuredPostal.STREET, + R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, + R.string.postal_city,FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.REGION, + R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, + R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, + R.string.postal_country, FLAGS_POSTAL).setOptional(true)); + } + } else { + kind.maxLinesForDisplay= MAX_LINES_FOR_POSTAL_ADDRESS; + kind.fieldList.add( + new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, + FLAGS_POSTAL)); + } + + return Lists.newArrayList(kind); + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("home".equals(type)) return buildPostalType(StructuredPostal.TYPE_HOME); + if ("work".equals(type)) return buildPostalType(StructuredPostal.TYPE_WORK); + if ("other".equals(type)) return buildPostalType(StructuredPostal.TYPE_OTHER); + if ("custom".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_CUSTOM) + .setSecondary(true).setCustomColumn(Email.LABEL); + } + return null; + } + } + + private static class ImKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "im"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + + // IM is special: + // - It uses "protocol" as the custom label field + // - Its TYPE is fixed to TYPE_OTHER + + final DataKind kind = newDataKind(context, parser, attrs, false, + Im.CONTENT_ITEM_TYPE, Im.PROTOCOL, R.string.imLabelsGroup, Weight.IM, + R.layout.text_fields_editor_view, + new ImActionInflater(), new SimpleInflater(Im.DATA) // header / action + ); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + return Lists.newArrayList(kind); + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + if ("aim".equals(type)) return buildImType(Im.PROTOCOL_AIM); + if ("msn".equals(type)) return buildImType(Im.PROTOCOL_MSN); + if ("yahoo".equals(type)) return buildImType(Im.PROTOCOL_YAHOO); + if ("skype".equals(type)) return buildImType(Im.PROTOCOL_SKYPE); + if ("qq".equals(type)) return buildImType(Im.PROTOCOL_QQ); + if ("google_talk".equals(type)) return buildImType(Im.PROTOCOL_GOOGLE_TALK); + if ("icq".equals(type)) return buildImType(Im.PROTOCOL_ICQ); + if ("jabber".equals(type)) return buildImType(Im.PROTOCOL_JABBER); + if ("custom".equals(type)) { + return buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true) + .setCustomColumn(Im.CUSTOM_PROTOCOL); + } + return null; + } + } + + private static class OrganizationKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "organization"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Organization.CONTENT_ITEM_TYPE, null, R.string.organizationLabelsGroup, + Weight.ORGANIZATION, R.layout.text_fields_editor_view , + new SimpleInflater(Organization.COMPANY), + new SimpleInflater(Organization.TITLE)); + + kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company, + FLAGS_GENERIC_NAME)); + kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title, + FLAGS_GENERIC_NAME)); + + throwIfList(kind); + + return Lists.newArrayList(kind); + } + } + + private static class PhotoKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "photo"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Photo.CONTENT_ITEM_TYPE, null /* no type */, -1, Weight.NONE, -1, + null, null // no header, no body + ); + + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + + throwIfList(kind); + + return Lists.newArrayList(kind); + } + } + + private static class NoteKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "note"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Note.CONTENT_ITEM_TYPE, null, R.string.label_notes, Weight.NOTE, + R.layout.text_fields_editor_view, + new SimpleInflater(R.string.label_notes), new SimpleInflater(Note.NOTE)); + + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE; + + throwIfList(kind); + + return Lists.newArrayList(kind); + } + } + + private static class WebsiteKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "website"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Website.CONTENT_ITEM_TYPE, null, R.string.websiteLabelsGroup, Weight.WEBSITE, + R.layout.text_fields_editor_view, + new SimpleInflater(R.string.websiteLabelsGroup), + new SimpleInflater(Website.URL)); + + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, + FLAGS_WEBSITE)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER); + + return Lists.newArrayList(kind); + } + } + + private static class SipAddressKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "sip_address"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + SipAddress.CONTENT_ITEM_TYPE, null, R.string.label_sip_address, + Weight.SIP_ADDRESS, R.layout.text_fields_editor_view, + new SimpleInflater(R.string.label_sip_address), + new SimpleInflater(SipAddress.SIP_ADDRESS)); + + kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS, + R.string.label_sip_address, FLAGS_SIP_ADDRESS)); + + throwIfList(kind); + + return Lists.newArrayList(kind); + } + } + + private static class GroupMembershipKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "group_membership"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + GroupMembership.CONTENT_ITEM_TYPE, null, + R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, -1, null, null); + + kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1)); + kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP; + + throwIfList(kind); + + return Lists.newArrayList(kind); + } + } + + /** + * Event DataKind parser. + * + * Event DataKind is used only for Google/Exchange types, so this parser is not used for now. + */ + private static class EventKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "event"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Event.CONTENT_ITEM_TYPE, Event.TYPE, R.string.eventLabelsGroup, Weight.EVENT, + R.layout.event_field_editor_view, + new EventActionInflater(), new SimpleInflater(Event.START_DATE)); + + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) { + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT; + } else { + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + } + + return Lists.newArrayList(kind); + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false); + + if ("birthday".equals(type)) { + return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1); + } + if ("anniversary".equals(type)) return buildEventType(Event.TYPE_ANNIVERSARY, yo); + if ("other".equals(type)) return buildEventType(Event.TYPE_OTHER, yo); + if ("custom".equals(type)) { + return buildEventType(Event.TYPE_CUSTOM, yo) + .setSecondary(true).setCustomColumn(Event.LABEL); + } + return null; + } + } + + /** + * Relationship DataKind parser. + * + * Relationship DataKind is used only for Google/Exchange types, so this parser is not used for + * now. + */ + private static class RelationshipKindBuilder extends KindBuilder { + @Override + public String getTagName() { + return "relationship"; + } + + @Override + public List<DataKind> parseDataKind(Context context, XmlPullParser parser, + AttributeSet attrs) throws DefinitionException, XmlPullParserException, + IOException { + final DataKind kind = newDataKind(context, parser, attrs, false, + Relation.CONTENT_ITEM_TYPE, Relation.TYPE, + R.string.relationLabelsGroup, Weight.RELATIONSHIP, + R.layout.text_fields_editor_view, + new RelationActionInflater(), new SimpleInflater(Relation.NAME)); + + kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, + FLAGS_RELATION)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + return Lists.newArrayList(kind); + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("assistant".equals(type)) return buildRelationType(Relation.TYPE_ASSISTANT); + if ("brother".equals(type)) return buildRelationType(Relation.TYPE_BROTHER); + if ("child".equals(type)) return buildRelationType(Relation.TYPE_CHILD); + if ("domestic_partner".equals(type)) { + return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER); + } + if ("father".equals(type)) return buildRelationType(Relation.TYPE_FATHER); + if ("friend".equals(type)) return buildRelationType(Relation.TYPE_FRIEND); + if ("manager".equals(type)) return buildRelationType(Relation.TYPE_MANAGER); + if ("mother".equals(type)) return buildRelationType(Relation.TYPE_MOTHER); + if ("parent".equals(type)) return buildRelationType(Relation.TYPE_PARENT); + if ("partner".equals(type)) return buildRelationType(Relation.TYPE_PARTNER); + if ("referred_by".equals(type)) return buildRelationType(Relation.TYPE_REFERRED_BY); + if ("relative".equals(type)) return buildRelationType(Relation.TYPE_RELATIVE); + if ("sister".equals(type)) return buildRelationType(Relation.TYPE_SISTER); + if ("spouse".equals(type)) return buildRelationType(Relation.TYPE_SPOUSE); + if ("custom".equals(type)) { + return buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true) + .setCustomColumn(Relation.LABEL); + } + return null; + } + } +} diff --git a/src/com/android/contacts/common/model/account/ExchangeAccountType.java b/src/com/android/contacts/common/model/account/ExchangeAccountType.java new file mode 100644 index 00000000..300e4d41 --- /dev/null +++ b/src/com/android/contacts/common/model/account/ExchangeAccountType.java @@ -0,0 +1,348 @@ +/* + * 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. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; + +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import com.google.common.collect.Lists; + +import java.util.Locale; + +public class ExchangeAccountType extends BaseAccountType { + private static final String TAG = "ExchangeAccountType"; + + public static final String ACCOUNT_TYPE_AOSP = "com.android.exchange"; + public static final String ACCOUNT_TYPE_GOOGLE = "com.google.android.exchange"; + + public ExchangeAccountType(Context context, String authenticatorPackageName, String type) { + this.accountType = type; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindEvent(context); + addDataKindWebsite(context); + addDataKindGroupMembership(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + public static boolean isExchangeType(String type) { + return ACCOUNT_TYPE_AOSP.equals(type) || ACCOUNT_TYPE_GOOGLE.equals(type); + } + + @Override + protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE, + R.string.nameLabelsGroup, -1, true, R.layout.structured_name_editor_view)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setOptional(true)); + kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, + R.string.name_family, FLAGS_PERSON_NAME)); + kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, + R.string.name_middle, FLAGS_PERSON_NAME)); + kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, + R.string.name_given, FLAGS_PERSON_NAME)); + kind.fieldList.add(new EditField(StructuredName.SUFFIX, + R.string.name_suffix, FLAGS_PERSON_NAME)); + + kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME, + R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + @Override + protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + R.string.nameLabelsGroup, -1, true, R.layout.text_fields_editor_view)); + + boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix, + FLAGS_PERSON_NAME).setOptional(true)); + if (!displayOrderPrimary) { + kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, + R.string.name_family, FLAGS_PERSON_NAME)); + kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, + R.string.name_middle, FLAGS_PERSON_NAME).setOptional(true)); + kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, + R.string.name_given, FLAGS_PERSON_NAME)); + } else { + kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, + R.string.name_given, FLAGS_PERSON_NAME)); + kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, + R.string.name_middle, FLAGS_PERSON_NAME).setOptional(true)); + kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, + R.string.name_family, FLAGS_PERSON_NAME)); + } + kind.fieldList.add(new EditField(StructuredName.SUFFIX, + R.string.name_suffix, FLAGS_PERSON_NAME).setOptional(true)); + + return kind; + } + + @Override + protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + R.string.name_phonetic, -1, true, R.layout.phonetic_name_editor_view)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME, + R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + @Override + protected DataKind addDataKindNickname(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindNickname(context); + + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, + FLAGS_PERSON_NAME)); + + return kind; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true) + .setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true) + .setSpecificMax(1)); + kind.typeList + .add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true) + .setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1)); + kind.typeList + .add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true) + .setSpecificMax(1)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeOverallMax = 3; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + @Override + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindStructuredPostal(context); + + final boolean useJapaneseOrder = + Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage()); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1)); + + kind.fieldList = Lists.newArrayList(); + if (useJapaneseOrder) { + kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, + R.string.postal_country, FLAGS_POSTAL).setOptional(true)); + kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, + R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.REGION, + R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, + R.string.postal_city,FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.STREET, + R.string.postal_street, FLAGS_POSTAL)); + } else { + kind.fieldList.add(new EditField(StructuredPostal.STREET, + R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, + R.string.postal_city,FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.REGION, + R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, + R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, + R.string.postal_country, FLAGS_POSTAL).setOptional(true)); + } + + return kind; + } + + @Override + protected DataKind addDataKindIm(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindIm(context); + + // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first + kind.typeOverallMax = 3; + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + @Override + protected DataKind addDataKindOrganization(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindOrganization(context); + + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company, + FLAGS_GENERIC_NAME)); + kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title, + FLAGS_GENERIC_NAME)); + + return kind; + } + + @Override + protected DataKind addDataKindPhoto(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhoto(context); + + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + + return kind; + } + + @Override + protected DataKind addDataKindNote(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindNote(context); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + + return kind; + } + + protected DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = addKind( + new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true, + R.layout.event_field_editor_view)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeOverallMax = 1; + + kind.typeColumn = Event.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1)); + + kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + protected DataKind addDataKindWebsite(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindWebsite(context); + + kind.typeOverallMax = 1; + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/src/com/android/contacts/common/model/account/ExternalAccountType.java b/src/com/android/contacts/common/model/account/ExternalAccountType.java new file mode 100644 index 00000000..3bc5b3a5 --- /dev/null +++ b/src/com/android/contacts/common/model/account/ExternalAccountType.java @@ -0,0 +1,442 @@ +/* + * 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. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import com.android.contacts.common.model.dataitem.DataKind; +import com.google.common.annotations.VisibleForTesting; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A general contacts account type descriptor. + */ +public class ExternalAccountType extends BaseAccountType { + private static final String TAG = "ExternalAccountType"; + + private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE"; + + private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource"; + private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType"; + private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind"; + private static final String TAG_EDIT_SCHEMA = "EditSchema"; + + private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity"; + private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel"; + private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService"; + private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity"; + private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel"; + private static final String ATTR_VIEW_STREAM_ITEM_ACTIVITY = "viewStreamItemActivity"; + private static final String ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY = + "viewStreamItemPhotoActivity"; + private static final String ATTR_DATA_SET = "dataSet"; + private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames"; + + // The following attributes should only be set in non-sync-adapter account types. They allow + // for the account type and resource IDs to be specified without an associated authenticator. + private static final String ATTR_ACCOUNT_TYPE = "accountType"; + private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel"; + private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon"; + + private final boolean mIsExtension; + + private String mEditContactActivityClassName; + private String mCreateContactActivityClassName; + private String mInviteContactActivity; + private String mInviteActionLabelAttribute; + private int mInviteActionLabelResId; + private String mViewContactNotifyService; + private String mViewGroupActivity; + private String mViewGroupLabelAttribute; + private int mViewGroupLabelResId; + private String mViewStreamItemActivity; + private String mViewStreamItemPhotoActivity; + private List<String> mExtensionPackageNames; + private String mAccountTypeLabelAttribute; + private String mAccountTypeIconAttribute; + private boolean mHasContactsMetadata; + private boolean mHasEditSchema; + + public ExternalAccountType(Context context, String resPackageName, boolean isExtension) { + this(context, resPackageName, isExtension, null); + } + + /** + * Constructor used for testing to initialize with any arbitrary XML. + * + * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by + * tests. If null, the metadata is loaded from the specified package. + */ + ExternalAccountType(Context context, String packageName, boolean isExtension, + XmlResourceParser injectedMetadata) { + this.mIsExtension = isExtension; + this.resourcePackageName = packageName; + this.syncAdapterPackageName = packageName; + + final PackageManager pm = context.getPackageManager(); + final XmlResourceParser parser; + if (injectedMetadata == null) { + try { + parser = loadContactsXml(context, packageName); + } catch (NameNotFoundException e1) { + // If the package name is not found, we can't initialize this account type. + return; + } + } else { + parser = injectedMetadata; + } + boolean needLineNumberInErrorLog = true; + try { + if (parser != null) { + inflate(context, parser); + } + + // Done parsing; line number no longer needed in error log. + needLineNumberInErrorLog = false; + if (mHasEditSchema) { + checkKindExists(StructuredName.CONTENT_ITEM_TYPE); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME); + checkKindExists(Photo.CONTENT_ITEM_TYPE); + } else { + // Bring in name and photo from fallback source, which are non-optional + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindPhoto(context); + } + } catch (DefinitionException e) { + final StringBuilder error = new StringBuilder(); + error.append("Problem reading XML"); + if (needLineNumberInErrorLog && (parser != null)) { + error.append(" in line "); + error.append(parser.getLineNumber()); + } + error.append(" for external package "); + error.append(packageName); + + Log.e(TAG, error.toString(), e); + return; + } finally { + if (parser != null) { + parser.close(); + } + } + + mExtensionPackageNames = new ArrayList<String>(); + mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute, + syncAdapterPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL); + mViewGroupLabelResId = resolveExternalResId(context, mViewGroupLabelAttribute, + syncAdapterPackageName, ATTR_VIEW_GROUP_ACTION_LABEL); + titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute, + syncAdapterPackageName, ATTR_ACCOUNT_LABEL); + iconRes = resolveExternalResId(context, mAccountTypeIconAttribute, + syncAdapterPackageName, ATTR_ACCOUNT_ICON); + + // If we reach this point, the account type has been successfully initialized. + mIsInitialized = true; + } + + /** + * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package. + * + * Unfortunately, there's no public way to determine which service defines a sync service for + * which account type, so this method looks through all services in the package, and just + * returns the first CONTACTS_STRUCTURE metadata defined in any of them. + * + * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case + * the account type *will* be initialized with minimal configuration. + * + * On the other hand, if the package is not found, it throws a {@link NameNotFoundException}, + * in which case the account type will *not* be initialized. + */ + private XmlResourceParser loadContactsXml(Context context, String resPackageName) + throws NameNotFoundException { + final PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageInfo(resPackageName, + PackageManager.GET_SERVICES|PackageManager.GET_META_DATA); + for (ServiceInfo serviceInfo : packageInfo.services) { + final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, + METADATA_CONTACTS); + if (parser != null) { + return parser; + } + } + // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. + return null; + } + + private void checkKindExists(String mimeType) throws DefinitionException { + if (getKindForMimetype(mimeType) == null) { + throw new DefinitionException(mimeType + " must be supported"); + } + } + + @Override + public boolean isEmbedded() { + return false; + } + + @Override + public boolean isExtension() { + return mIsExtension; + } + + @Override + public boolean areContactsWritable() { + return mHasEditSchema; + } + + /** + * Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. + */ + public boolean hasContactsMetadata() { + return mHasContactsMetadata; + } + + @Override + public String getEditContactActivityClassName() { + return mEditContactActivityClassName; + } + + @Override + public String getCreateContactActivityClassName() { + return mCreateContactActivityClassName; + } + + @Override + public String getInviteContactActivityClassName() { + return mInviteContactActivity; + } + + @Override + protected int getInviteContactActionResId() { + return mInviteActionLabelResId; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return mViewContactNotifyService; + } + + @Override + public String getViewGroupActivity() { + return mViewGroupActivity; + } + + @Override + protected int getViewGroupLabelResId() { + return mViewGroupLabelResId; + } + + @Override + public String getViewStreamItemActivity() { + return mViewStreamItemActivity; + } + + @Override + public String getViewStreamItemPhotoActivity() { + return mViewStreamItemPhotoActivity; + } + + @Override + public List<String> getExtensionPackageNames() { + return mExtensionPackageNames; + } + + /** + * Inflate this {@link AccountType} from the given parser. This may only + * load details matching the publicly-defined schema. + */ + protected void inflate(Context context, XmlPullParser parser) throws DefinitionException { + final AttributeSet attrs = Xml.asAttributeSet(parser); + + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Drain comments and whitespace + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("No start tag found"); + } + + String rootTag = parser.getName(); + if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) && + !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) { + throw new IllegalStateException("Top level element must be " + + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag); + } + + mHasContactsMetadata = true; + + int attributeCount = parser.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + String attr = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, attr + "=" + value); + } + if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) { + mEditContactActivityClassName = value; + } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) { + mCreateContactActivityClassName = value; + } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) { + mInviteContactActivity = value; + } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) { + mInviteActionLabelAttribute = value; + } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) { + mViewContactNotifyService = value; + } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) { + mViewGroupActivity = value; + } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) { + mViewGroupLabelAttribute = value; + } else if (ATTR_VIEW_STREAM_ITEM_ACTIVITY.equals(attr)) { + mViewStreamItemActivity = value; + } else if (ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY.equals(attr)) { + mViewStreamItemPhotoActivity = value; + } else if (ATTR_DATA_SET.equals(attr)) { + dataSet = value; + } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) { + mExtensionPackageNames.add(value); + } else if (ATTR_ACCOUNT_TYPE.equals(attr)) { + accountType = value; + } else if (ATTR_ACCOUNT_LABEL.equals(attr)) { + mAccountTypeLabelAttribute = value; + } else if (ATTR_ACCOUNT_ICON.equals(attr)) { + mAccountTypeIconAttribute = value; + } else { + Log.e(TAG, "Unsupported attribute " + attr); + } + } + + // Parse all children kinds + final int startDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG + || parser.getDepth() > startDepth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) { + continue; // Not a direct child tag + } + + String tag = parser.getName(); + if (TAG_EDIT_SCHEMA.equals(tag)) { + mHasEditSchema = true; + parseEditSchema(context, parser, attrs); + } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) { + final TypedArray a = context.obtainStyledAttributes(attrs, + android.R.styleable.ContactsDataKind); + final DataKind kind = new DataKind(); + + kind.mimeType = a + .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType); + + final String summaryColumn = a.getString( + com.android.internal.R.styleable.ContactsDataKind_summaryColumn); + if (summaryColumn != null) { + // Inflate a specific column as summary when requested + kind.actionHeader = new SimpleInflater(summaryColumn); + } + + final String detailColumn = a.getString( + com.android.internal.R.styleable.ContactsDataKind_detailColumn); + final boolean detailSocialSummary = a.getBoolean( + com.android.internal.R.styleable.ContactsDataKind_detailSocialSummary, + false); + + if (detailSocialSummary) { + // Inflate social summary when requested + kind.actionBodySocial = true; + } + + if (detailColumn != null) { + // Inflate specific column as summary + kind.actionBody = new SimpleInflater(detailColumn); + } + + a.recycle(); + + addKind(kind); + } + } + } catch (XmlPullParserException e) { + throw new DefinitionException("Problem reading XML", e); + } catch (IOException e) { + throw new DefinitionException("Problem reading XML", e); + } + } + + /** + * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in + * the resource package. + * + * If the argument is in the invalid format or isn't a resource name, it returns -1. + * + * @param context context + * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel" + * @param packageName name of the package containing the resource. + * @param xmlAttributeName attribute name which the resource came from. Used for logging. + */ + @VisibleForTesting + static int resolveExternalResId(Context context, String resourceName, + String packageName, String xmlAttributeName) { + if (TextUtils.isEmpty(resourceName)) { + return -1; // Empty text is okay. + } + if (resourceName.charAt(0) != '@') { + Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'"); + return -1; + } + final String name = resourceName.substring(1); + final Resources res; + try { + res = context.getPackageManager().getResourcesForApplication(packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to load package " + packageName); + return -1; + } + final int resId = res.getIdentifier(name, null, packageName); + if (resId == 0) { + Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName); + return -1; + } + return resId; + } +} diff --git a/src/com/android/contacts/common/model/account/FallbackAccountType.java b/src/com/android/contacts/common/model/account/FallbackAccountType.java new file mode 100644 index 00000000..71c23c20 --- /dev/null +++ b/src/com/android/contacts/common/model/account/FallbackAccountType.java @@ -0,0 +1,78 @@ +/* + * 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. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.util.Log; + +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.test.NeededForTesting; + +public class FallbackAccountType extends BaseAccountType { + private static final String TAG = "FallbackAccountType"; + + private FallbackAccountType(Context context, String resPackageName) { + this.accountType = null; + this.dataSet = null; + this.titleRes = R.string.account_phone; + this.iconRes = R.mipmap.ic_launcher_contacts; + + // Note those are only set for unit tests. + this.resourcePackageName = resPackageName; + this.syncAdapterPackageName = resPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindSipAddress(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + public FallbackAccountType(Context context) { + this(context, null); + } + + /** + * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml. + * In order to build {@link DataKind}s with the same resource package name, + * {@code resPackageName} is injectable. + */ + @NeededForTesting + static AccountType createWithPackageNameForTest(Context context, String resPackageName) { + return new FallbackAccountType(context, resPackageName); + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/src/com/android/contacts/common/model/account/GoogleAccountType.java b/src/com/android/contacts/common/model/account/GoogleAccountType.java new file mode 100644 index 00000000..a5a2c57f --- /dev/null +++ b/src/com/android/contacts/common/model/account/GoogleAccountType.java @@ -0,0 +1,197 @@ +/* + * 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. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.util.Log; + +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import com.google.common.collect.Lists; + +import java.util.List; + +public class GoogleAccountType extends BaseAccountType { + private static final String TAG = "GoogleAccountType"; + + public static final String ACCOUNT_TYPE = "com.google"; + + private static final List<String> mExtensionPackages = + Lists.newArrayList("com.google.android.apps.plus"); + + public GoogleAccountType(Context context, String authenticatorPackageName) { + this.accountType = ACCOUNT_TYPE; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindSipAddress(context); + addDataKindGroupMembership(context); + addDataKindRelation(context); + addDataKindEvent(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + @Override + public List<String> getExtensionPackageNames() { + return mExtensionPackages; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true) + .setCustomColumn(Phone.LABEL)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeColumn = Email.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add(buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn( + Email.LABEL)); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + private DataKind addDataKindRelation(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, + R.string.relationLabelsGroup, 160, true, R.layout.text_fields_editor_view)); + kind.actionHeader = new RelationActionInflater(); + kind.actionBody = new SimpleInflater(Relation.NAME); + + kind.typeColumn = Relation.TYPE; + kind.typeList = Lists.newArrayList(); + kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT)); + kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_CHILD)); + kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FATHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND)); + kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER)); + kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARENT)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY)); + kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE)); + kind.typeList.add(buildRelationType(Relation.TYPE_SISTER)); + kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE)); + kind.typeList.add(buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true) + .setCustomColumn(Relation.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, + FLAGS_RELATION)); + + return kind; + } + + private DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Event.CONTENT_ITEM_TYPE, + R.string.eventLabelsGroup, 150, true, R.layout.event_field_editor_view)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeColumn = Event.TYPE; + kind.typeList = Lists.newArrayList(); + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1)); + kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false)); + kind.typeList.add(buildEventType(Event.TYPE_OTHER, false)); + kind.typeList.add(buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true) + .setCustomColumn(Event.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY); + + kind.fieldList = Lists.newArrayList(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return "com.google.android.syncadapters.contacts." + + "SyncHighResPhotoIntentService"; + } + + @Override + public String getViewContactNotifyServicePackageName() { + return "com.google.android.syncadapters.contacts"; + } +} diff --git a/src/com/android/contacts/common/model/dataitem/DataKind.java b/src/com/android/contacts/common/model/dataitem/DataKind.java new file mode 100644 index 00000000..58a8e7ba --- /dev/null +++ b/src/com/android/contacts/common/model/dataitem/DataKind.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2011 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.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.Data; + +import com.android.contacts.common.R; +import com.android.contacts.common.model.account.AccountType.EditField; +import com.android.contacts.common.model.account.AccountType.EditType; +import com.android.contacts.common.model.account.AccountType.StringInflater; +import com.google.common.collect.Iterators; + +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * Description of a specific data type, usually marked by a unique + * {@link Data#MIMETYPE}. Includes details about how to view and edit + * {@link Data} rows of this kind, including the possible {@link EditType} + * labels and editable {@link EditField}. + */ +public final class DataKind { + + public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName"; + public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName"; + public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName"; + + public String resourcePackageName; + public String mimeType; + public int titleRes; + public int iconAltRes; + public int iconAltDescriptionRes; + public int weight; + public boolean editable; + + public StringInflater actionHeader; + public StringInflater actionAltHeader; + public StringInflater actionBody; + + public boolean actionBodySocial = false; + + public String typeColumn; + + /** + * Maximum number of values allowed in the list. -1 represents infinity. + */ + public int typeOverallMax; + + public List<EditType> typeList; + public List<EditField> fieldList; + + public ContentValues defaultValues; + + /** Layout resource id for an editor view to edit this {@link DataKind}. */ + public final int editorLayoutResourceId; + + /** + * If this is a date field, this specifies the format of the date when saving. The + * date includes year, month and day. If this is not a date field or the date field is not + * editable, this value should be ignored. + */ + public SimpleDateFormat dateFormatWithoutYear; + + /** + * If this is a date field, this specifies the format of the date when saving. The + * date includes month and day. If this is not a date field, the field is not editable or + * dates without year are not supported, this value should be ignored. + */ + public SimpleDateFormat dateFormatWithYear; + + /** + * The number of lines available for displaying this kind of data. + * Defaults to 1. + */ + public int maxLinesForDisplay; + + public DataKind() { + editorLayoutResourceId = R.layout.text_fields_editor_view; + maxLinesForDisplay = 1; + } + + public DataKind(String mimeType, int titleRes, int weight, boolean editable, + int editorLayoutResourceId) { + this.mimeType = mimeType; + this.titleRes = titleRes; + this.weight = weight; + this.editable = editable; + this.typeOverallMax = -1; + this.editorLayoutResourceId = editorLayoutResourceId; + maxLinesForDisplay = 1; + } + + public String getKindString(Context context) { + return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("DataKind:"); + sb.append(" resPackageName=").append(resourcePackageName); + sb.append(" mimeType=").append(mimeType); + sb.append(" titleRes=").append(titleRes); + sb.append(" iconAltRes=").append(iconAltRes); + sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes); + sb.append(" weight=").append(weight); + sb.append(" editable=").append(editable); + sb.append(" actionHeader=").append(actionHeader); + sb.append(" actionAltHeader=").append(actionAltHeader); + sb.append(" actionBody=").append(actionBody); + sb.append(" actionBodySocial=").append(actionBodySocial); + sb.append(" typeColumn=").append(typeColumn); + sb.append(" typeOverallMax=").append(typeOverallMax); + sb.append(" typeList=").append(toString(typeList)); + sb.append(" fieldList=").append(toString(fieldList)); + sb.append(" defaultValues=").append(defaultValues); + sb.append(" editorLayoutResourceId=").append(editorLayoutResourceId); + sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear)); + sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear)); + + return sb.toString(); + } + + public static String toString(SimpleDateFormat format) { + return format == null ? "(null)" : format.toPattern(); + } + + public static String toString(Iterable<?> list) { + if (list == null) { + return "(null)"; + } else { + return Iterators.toString(list.iterator()); + } + } +} diff --git a/src/com/android/contacts/common/test/NeededForTesting.java b/src/com/android/contacts/common/test/NeededForTesting.java new file mode 100644 index 00000000..f82756ad --- /dev/null +++ b/src/com/android/contacts/common/test/NeededForTesting.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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.contacts.common.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Denotes that the class, constructor, method or field is used by tests and therefore cannot be + * removed by tools like ProGuard. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) +public @interface NeededForTesting {} diff --git a/src/com/android/contacts/common/util/CommonDateUtils.java b/src/com/android/contacts/common/util/CommonDateUtils.java new file mode 100644 index 00000000..5dfd149a --- /dev/null +++ b/src/com/android/contacts/common/util/CommonDateUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2012 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.contacts.common.util; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * Common date utilities. + */ +public class CommonDateUtils { + + // All the SimpleDateFormats in this class use the UTC timezone + public static final SimpleDateFormat NO_YEAR_DATE_FORMAT = + new SimpleDateFormat("--MM-dd", Locale.US); + public static final SimpleDateFormat FULL_DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static final SimpleDateFormat DATE_AND_TIME_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT = + new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); +} diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index e181b2b9..55a60594 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -20,6 +20,18 @@ <application> <uses-library android:name="android.test.runner" /> + + <service android:name="com.android.contacts.common.tests.testauth.TestSyncService$Basic" android:exported="true"> + <intent-filter> + <action android:name="android.content.SyncAdapter"/> + </intent-filter> + <meta-data + android:name="android.content.SyncAdapter" + android:resource="@xml/test_basic_syncadapter"/> + <meta-data + android:name="android.provider.CONTACTS_STRUCTURE" + android:resource="@xml/test_basic_contacts"/> + </service> </application> <instrumentation android:name="android.test.InstrumentationTestRunner" diff --git a/tests/proguard.flags b/tests/proguard.flags index 883dde6d..d9d7942b 100644 --- a/tests/proguard.flags +++ b/tests/proguard.flags @@ -1,8 +1,8 @@ # Any class or method annotated with NeededForTesting or NeededForReflection. --keep @com.android.contacts.test.NeededForTesting class * +-keep @com.android.contacts.common.test.NeededForTesting class * -keep @com.android.contacts.test.NeededForReflection class * -keepclassmembers class * { -@com.android.contacts.test.NeededForTesting *; +@com.android.contacts.common.test.NeededForTesting *; @com.android.contacts.test.NeededForReflection *; } diff --git a/tests/res/drawable/android.jpg b/tests/res/drawable/android.jpg Binary files differnew file mode 100644 index 00000000..95693b2f --- /dev/null +++ b/tests/res/drawable/android.jpg diff --git a/tests/res/drawable/default_icon.png b/tests/res/drawable/default_icon.png Binary files differnew file mode 100644 index 00000000..cea0eb3b --- /dev/null +++ b/tests/res/drawable/default_icon.png diff --git a/tests/res/drawable/ic_contact_picture.png b/tests/res/drawable/ic_contact_picture.png Binary files differnew file mode 100644 index 00000000..68767774 --- /dev/null +++ b/tests/res/drawable/ic_contact_picture.png diff --git a/tests/res/drawable/phone_icon.png b/tests/res/drawable/phone_icon.png Binary files differnew file mode 100644 index 00000000..4e613ecc --- /dev/null +++ b/tests/res/drawable/phone_icon.png diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml new file mode 100644 index 00000000..6c8527f9 --- /dev/null +++ b/tests/res/values/donottranslate_strings.xml @@ -0,0 +1,21 @@ +<!-- + ~ Copyright (C) 2012 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> + <string name="test_string">TEST STRING</string> + + <string name="authenticator_basic_label">Test adapter</string> +</resources> diff --git a/tests/res/xml/contacts_fallback.xml b/tests/res/xml/contacts_fallback.xml new file mode 100644 index 00000000..ae262eba --- /dev/null +++ b/tests/res/xml/contacts_fallback.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- + contacts.xml to build "fallback account type" equivalent. + This is directly used in ExternalAccountTypeTest to test the parser. There's no sync adapter + that actually defined with this definition. +--> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema + > + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + > + </DataKind> + <DataKind kind="photo" maxOccurs="1" /> + <DataKind kind="phone" > + <Type type="mobile" /> + <Type type="home" /> + <Type type="work" /> + <Type type="fax_work" /> + <Type type="fax_home" /> + <Type type="pager" /> + <Type type="other" /> + <Type type="custom"/> + <Type type="callback" /> + <Type type="car" /> + <Type type="company_main" /> + <Type type="isdn" /> + <Type type="main" /> + <Type type="other_fax" /> + <Type type="radio" /> + <Type type="telex" /> + <Type type="tty_tdd" /> + <Type type="work_mobile"/> + <Type type="work_pager" /> + <Type type="assistant" /> + <Type type="mms" /> + </DataKind> + <DataKind kind="email" > + <Type type="home" /> + <Type type="work" /> + <Type type="other" /> + <Type type="mobile" /> + <Type type="custom" /> + </DataKind> + <DataKind kind="nickname" maxOccurs="1" /> + <DataKind kind="im" > + <Type type="aim" /> + <Type type="msn" /> + <Type type="yahoo" /> + <Type type="skype" /> + <Type type="qq" /> + <Type type="google_talk" /> + <Type type="icq" /> + <Type type="jabber" /> + <Type type="custom" /> + </DataKind> + <DataKind kind="postal" needsStructured="false" > + <Type type="home" /> + <Type type="work" /> + <Type type="other" /> + <Type type="custom" /> + </DataKind> + <DataKind kind="organization" maxOccurs="1" /> + <DataKind kind="website" /> + <DataKind kind="sip_address" maxOccurs="1" /> + <DataKind kind="note" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/contacts_readonly.xml b/tests/res/xml/contacts_readonly.xml new file mode 100644 index 00000000..df8d9c06 --- /dev/null +++ b/tests/res/xml/contacts_readonly.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- + Contacts.xml without EditSchema. +--> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <ContactsDataKind + android:icon="@drawable/android" + android:mimeType="vnd.android.cursor.item/a.b.c" + android:summaryColumn="data1" + android:detailColumn="data2" + android:detailSocialSummary="true" + > + </ContactsDataKind> + <ContactsDataKind + android:icon="@drawable/default_icon" + android:mimeType="vnd.android.cursor.item/d.e.f" + android:summaryColumn="data3" + android:detailColumn="data4" + android:detailSocialSummary="false" + > + </ContactsDataKind> + <ContactsDataKind + android:icon="@drawable/android" + android:mimeType="vnd.android.cursor.item/xyz" + android:summaryColumn="data5" + android:detailColumn="data6" + android:detailSocialSummary="true" + > + </ContactsDataKind> +</ContactsAccountType> diff --git a/tests/res/xml/iconset.xml b/tests/res/xml/iconset.xml new file mode 100644 index 00000000..d1207e79 --- /dev/null +++ b/tests/res/xml/iconset.xml @@ -0,0 +1,24 @@ +<?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. +--> + +<icon-set + xmlns:android="http://schemas.android.com/apk/res/android"> + + <icon-default android:icon="@drawable/default_icon" /> + <icon android:mimeType="vnd.android.cursor.item/phone" + android:icon="@drawable/phone_icon" /> + +</icon-set>
\ No newline at end of file diff --git a/tests/res/xml/missing_contacts_base.xml b/tests/res/xml/missing_contacts_base.xml new file mode 100644 index 00000000..2c9aa6db --- /dev/null +++ b/tests/res/xml/missing_contacts_base.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Base definition, which is valid. --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + > + </DataKind> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name.xml b/tests/res/xml/missing_contacts_name.xml new file mode 100644 index 00000000..1ac26be3 --- /dev/null +++ b/tests/res/xml/missing_contacts_name.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing "name" kind. --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr1.xml b/tests/res/xml/missing_contacts_name_attr1.xml new file mode 100644 index 00000000..b7b0f191 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr1.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr2.xml b/tests/res/xml/missing_contacts_name_attr2.xml new file mode 100644 index 00000000..41be9e87 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr2.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr3.xml b/tests/res/xml/missing_contacts_name_attr3.xml new file mode 100644 index 00000000..e639a767 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr3.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr4.xml b/tests/res/xml/missing_contacts_name_attr4.xml new file mode 100644 index 00000000..b42cdcd9 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr4.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr5.xml b/tests/res/xml/missing_contacts_name_attr5.xml new file mode 100644 index 00000000..3778d2f6 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr5.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr6.xml b/tests/res/xml/missing_contacts_name_attr6.xml new file mode 100644 index 00000000..b3a34114 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr6.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticGivenName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_name_attr7.xml b/tests/res/xml/missing_contacts_name_attr7.xml new file mode 100644 index 00000000..c87e4f17 --- /dev/null +++ b/tests/res/xml/missing_contacts_name_attr7.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing one of the "support*" attributes". --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + /> + <DataKind kind="photo" maxOccurs="1" /> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/missing_contacts_photo.xml b/tests/res/xml/missing_contacts_photo.xml new file mode 100644 index 00000000..87f4fc69 --- /dev/null +++ b/tests/res/xml/missing_contacts_photo.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<!-- XML for must-have checks. Missing "photo" kind. --> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + > + </DataKind> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/test_basic_contacts.xml b/tests/res/xml/test_basic_contacts.xml new file mode 100644 index 00000000..0047204f --- /dev/null +++ b/tests/res/xml/test_basic_contacts.xml @@ -0,0 +1,283 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<ContactsAccountType + xmlns:android="http://schemas.android.com/apk/res/android" + > + <EditSchema + > + <!-- + Name: + - maxOccurs must be 1 + - No types. + + - Currently all the supportsXxx attributes must be true, but here's the plan for the + future: + (There's some hardcoded assumptions in the contact editor, which is one reason + for the above restriction) + + - "Family name" and "Given name" must be supported. + - All sync adapters must support structured name. "display name only" is not + supported. + -> Supporting this would require relatively large changes to + the contact editor. + + - Fields are decided from the attributes: + StructuredName.DISPLAY_NAME if supportsDisplayName == true + StructuredName.PREFIX if supportsPrefix == true + StructuredName.FAMILY_NAME (always) + StructuredName.MIDDLE_NAME if supportsPrefix == true + StructuredName.GIVEN_NAME (always) + StructuredName.SUFFIX if supportsSuffix == true + StructuredName.PHONETIC_FAMILY_NAME if supportsPhoneticFamilyName == true + StructuredName.PHONETIC_MIDDLE_NAME if supportsPhoneticMiddleName == true + StructuredName.PHONETIC_GIVEN_NAME if supportsPhoneticGivenName == true + + - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME is always added. + - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME is added + if any of supportsPhoneticXxx == true + --> + <!-- Fallback/Google definition. Supports all. --> + <DataKind kind="name" + maxOccurs="1" + supportsDisplayName="true" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="true" + supportsPhoneticGivenName="true" + > + </DataKind> + + <!-- Exchange definition. No display-name, no phonetic-middle. + <DataKind kind="name" + supportsDisplayName="false" + supportsPrefix="true" + supportsMiddleName="true" + supportsSuffix="true" + supportsPhoneticFamilyName="true" + supportsPhoneticMiddleName="false" + supportsPhoneticGivenName ="true" + > + </DataKind> + --> + + <!-- + Photo: + - maxOccurs must be 1 + - No types. + --> + <DataKind kind="photo" maxOccurs="1" /> + + <!-- + Phone definition. + - "is secondary?" is inferred from type. + --> + <!-- Fallback, Google definition. --> + <DataKind kind="phone" > + <!-- Note: Google type doesn't have obsolete ones --> + <Type type="mobile" /> + <Type type="home" /> + <Type type="work" /> + <Type type="fax_work" /> + <Type type="fax_home" /> + <Type type="pager" /> + <Type type="other" /> + <Type type="custom"/> + <Type type="callback" /> + <Type type="car" /> + <Type type="company_main" /> + <Type type="isdn" /> + <Type type="main" /> + <Type type="other_fax" /> + <Type type="radio" /> + <Type type="telex" /> + <Type type="tty_tdd" /> + <Type type="work_mobile"/> + <Type type="work_pager" /> + <Type type="assistant" /> + <Type type="mms" /> + </DataKind> + + <!-- Exchange definition. + <DataKind kind="phone" > + <Type type="home" maxOccurs="2" /> + <Type type="mobile" maxOccurs="1" /> + <Type type="work" maxOccurs="2" /> + <Type type="fax_work" maxOccurs="1" /> + <Type type="fax_home" maxOccurs="1" /> + <Type type="pager" maxOccurs="1" /> + <Type type="car" maxOccurs="1" /> + <Type type="company_main" maxOccurs="1" /> + <Type type="mms" maxOccurs="1" /> + <Type type="radio" maxOccurs="1" /> + <Type type="assistant" maxOccurs="1" /> + </DataKind> + --> + + <!-- + Email + --> + <!-- Fallback/Google definition. --> + <DataKind kind="email" > + <!-- Note: Google type doesn't have obsolete ones --> + <Type type="home" /> + <Type type="work" /> + <Type type="other" /> + <Type type="mobile" /> + <Type type="custom" /> + </DataKind> + + <!-- + Exchange definition. + - Same definition as "fallback" except for maxOccurs=3 + <DataKind kind="email" maxOccurs="3" > + <Type type="home" /> + <Type type="work" /> + <Type type="other" /> + <Type type="mobile" /> + <Type type="custom" /> + </DataKind> + --> + + <!-- + Nickname + - maxOccurs must be 1 + - No types. + --> + <DataKind kind="nickname" maxOccurs="1" /> + + <!-- + Im: + - The TYPE column always stores Im.TYPE_OTHER (defaultValues is always set) + - The user-selected type is stored in Im.PROTOCOL + --> + <!-- Fallback, Google definition. --> + <DataKind kind="im" > + <Type type="aim" /> + <Type type="msn" /> + <Type type="yahoo" /> + <Type type="skype" /> + <Type type="qq" /> + <Type type="google_talk" /> + <Type type="icq" /> + <Type type="jabber" /> + <Type type="custom" /> + </DataKind> + + <!-- Exchange definition. + <DataKind kind="im" maxOccurs="3" > + <Type type="aim" /> + <Type type="msn" /> + <Type type="yahoo" /> + <Type type="skype" /> + <Type type="qq" /> + <Type type="google_talk" /> + <Type type="icq" /> + <Type type="jabber" /> + <Type type="custom" /> + </DataKind> + --> + + <!-- + Postal address. + --> + <!-- Fallback/Google definition. Not structured. --> + <DataKind kind="postal" needsStructured="false" > + <Type type="home" /> + <Type type="work" /> + <Type type="other" /> + <Type type="custom" /> + </DataKind> + + <!-- Exchange definition. Structured. + <DataKind kind="postal" needsStructured="true" > + <Type type="work" /> + <Type type="home" /> + <Type type="other" /> + </DataKind> + --> + + <!-- + Organization: + - Fields are fixed: COMPANY, TITLE + - maxOccurs must be 1 + - No types. + --> + <DataKind kind="organization" maxOccurs="1" /> + + <!-- + Website: + - No types. + --> + <DataKind kind="website" /> + + <!-- + Below kinds have nothing configurable. + - No types are supported. + - maxOccurs must be 1 + --> + <DataKind kind="sip_address" maxOccurs="1" /> + <DataKind kind="note" maxOccurs="1" /> + + <!-- + Google/Exchange supports it, but fallback doesn't. + <DataKind kind="group_membership" maxOccurs="1" /> + --> + + <!-- + Event + --> + <DataKind kind="event" dateWithTime="false"> + <Type type="birthday" maxOccurs="1" yearOptional="true" /> + <Type type="anniversary" /> + <Type type="other" /> + <Type type="custom" /> + </DataKind> + + <!-- + Exchange definition. dateWithTime is needed only for Exchange. + <DataKind kind="event" dateWithTime="true"> + <Type type="birthday" maxOccurs="1" /> + </DataKind> + --> + + <!-- + Relationship + --> + <DataKind kind="relationship" > + <Type type="assistant" /> + <Type type="brother" /> + <Type type="child" /> + <Type type="domestic_partner" /> + <Type type="father" /> + <Type type="friend" /> + <Type type="manager" /> + <Type type="mother" /> + <Type type="parent" /> + <Type type="partner" /> + <Type type="referred_by" /> + <Type type="relative" /> + <Type type="sister" /> + <Type type="spouse" /> + <Type type="custom" /> + </DataKind> + </EditSchema> +</ContactsAccountType> diff --git a/tests/res/xml/test_basic_syncadapter.xml b/tests/res/xml/test_basic_syncadapter.xml new file mode 100644 index 00000000..fecc0eb1 --- /dev/null +++ b/tests/res/xml/test_basic_syncadapter.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2011, 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. + */ +--> + +<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" + android:contentAuthority="com.android.contacts" + android:accountType="com.android.contacts.tests.authtest.basic" + android:supportsUploading="true" + android:userVisible="true" +/> diff --git a/tests/src/com/android/contacts/common/model/AccountWithDataSetTest.java b/tests/src/com/android/contacts/common/model/AccountWithDataSetTest.java new file mode 100644 index 00000000..e28f09e8 --- /dev/null +++ b/tests/src/com/android/contacts/common/model/AccountWithDataSetTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2011 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.contacts.common.model; + +import android.os.Bundle; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Test case for {@link AccountWithDataSet}. + * + * adb shell am instrument -w -e class com.android.contacts.model.AccountWithDataSetTest \ + com.android.contacts.tests/android.test.InstrumentationTestRunner + */ +@SmallTest +public class AccountWithDataSetTest extends AndroidTestCase { + public void testStringifyAndUnstringify() { + AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null); + AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null); + AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset"); + + // stringify() & unstringify + AccountWithDataSet a1r = AccountWithDataSet.unstringify(a1.stringify()); + AccountWithDataSet a2r = AccountWithDataSet.unstringify(a2.stringify()); + AccountWithDataSet a3r = AccountWithDataSet.unstringify(a3.stringify()); + + assertEquals(a1, a1r); + assertEquals(a2, a2r); + assertEquals(a3, a3r); + + MoreAsserts.assertNotEqual(a1, a2r); + MoreAsserts.assertNotEqual(a1, a3r); + + MoreAsserts.assertNotEqual(a2, a1r); + MoreAsserts.assertNotEqual(a2, a3r); + + MoreAsserts.assertNotEqual(a3, a1r); + MoreAsserts.assertNotEqual(a3, a2r); + } + + public void testStringifyListAndUnstringify() { + AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null); + AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null); + AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset"); + + // Empty list + assertEquals(0, stringifyListAndUnstringify().size()); + + // 1 element + final List<AccountWithDataSet> listA = stringifyListAndUnstringify(a1); + assertEquals(1, listA.size()); + assertEquals(a1, listA.get(0)); + + // 2 elements + final List<AccountWithDataSet> listB = stringifyListAndUnstringify(a2, a1); + assertEquals(2, listB.size()); + assertEquals(a2, listB.get(0)); + assertEquals(a1, listB.get(1)); + + // 3 elements + final List<AccountWithDataSet> listC = stringifyListAndUnstringify(a3, a2, a1); + assertEquals(3, listC.size()); + assertEquals(a3, listC.get(0)); + assertEquals(a2, listC.get(1)); + assertEquals(a1, listC.get(2)); + } + + private static List<AccountWithDataSet> stringifyListAndUnstringify( + AccountWithDataSet... accounts) { + + List<AccountWithDataSet> list = Lists.newArrayList(accounts); + return AccountWithDataSet.unstringifyList(AccountWithDataSet.stringifyList(list)); + } + + public void testParcelable() { + AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null); + AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null); + AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset"); + + // Parcel them & unpercel. + final Bundle b = new Bundle(); + b.putParcelable("a1", a1); + b.putParcelable("a2", a2); + b.putParcelable("a3", a3); + + AccountWithDataSet a1r = b.getParcelable("a1"); + AccountWithDataSet a2r = b.getParcelable("a2"); + AccountWithDataSet a3r = b.getParcelable("a3"); + + assertEquals(a1, a1r); + assertEquals(a2, a2r); + assertEquals(a3, a3r); + + MoreAsserts.assertNotEqual(a1, a2r); + MoreAsserts.assertNotEqual(a1, a3r); + + MoreAsserts.assertNotEqual(a2, a1r); + MoreAsserts.assertNotEqual(a2, a3r); + + MoreAsserts.assertNotEqual(a3, a1r); + MoreAsserts.assertNotEqual(a3, a2r); + } +} diff --git a/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java b/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java new file mode 100644 index 00000000..4374ad36 --- /dev/null +++ b/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 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.contacts.common.model.account; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.contacts.common.unittest.R; + +/** + * Test case for {@link AccountType}. + * + * adb shell am instrument -w -e class com.android.contacts.model.AccountTypeTest \ + com.android.contacts.tests/android.test.InstrumentationTestRunner + */ +@SmallTest +public class AccountTypeTest extends AndroidTestCase { + public void testGetResourceText() { + // In this test we use the test package itself as an external package. + final String packageName = getTestContext().getPackageName(); + + final Context c = getContext(); + final String DEFAULT = "ABC"; + + // Package name null, resId -1, use the default + assertEquals(DEFAULT, AccountType.getResourceText(c, null, -1, DEFAULT)); + + // Resource ID -1, use the default + assertEquals(DEFAULT, AccountType.getResourceText(c, packageName, -1, DEFAULT)); + + // Load from an external package. (here, we use this test package itself) + final int externalResID = R.string.test_string; + assertEquals(getTestContext().getString(externalResID), + AccountType.getResourceText(c, packageName, externalResID, DEFAULT)); + + // Load from the contacts package itself. + final int internalResId = com.android.contacts.common.R.string.contactsList; + assertEquals(c.getString(internalResId), + AccountType.getResourceText(c, null, internalResId, DEFAULT)); + } + + /** + * Verify if {@link AccountType#getInviteContactActionLabel} correctly gets the resource ID + * from {@link AccountType#getInviteContactActionResId} + */ + public void testGetInviteContactActionLabel() { + final String packageName = getTestContext().getPackageName(); + final Context c = getContext(); + + final int externalResID = R.string.test_string; + + AccountType accountType = new AccountType() { + { + resourcePackageName = packageName; + syncAdapterPackageName = packageName; + } + @Override protected int getInviteContactActionResId() { + return externalResID; + } + + @Override public boolean isGroupMembershipEditable() { + return false; + } + + @Override public boolean areContactsWritable() { + return false; + } + }; + + assertEquals(getTestContext().getString(externalResID), + accountType.getInviteContactActionLabel(c)); + } + + public void testDisplayLabelComparator() { + final AccountTypeForDisplayLabelTest EMPTY = new AccountTypeForDisplayLabelTest(""); + final AccountTypeForDisplayLabelTest NULL = new AccountTypeForDisplayLabelTest(null); + final AccountTypeForDisplayLabelTest AA = new AccountTypeForDisplayLabelTest("aa"); + final AccountTypeForDisplayLabelTest BBB = new AccountTypeForDisplayLabelTest("bbb"); + final AccountTypeForDisplayLabelTest C = new AccountTypeForDisplayLabelTest("c"); + + assertTrue(compareDisplayLabel(AA, BBB) < 0); + assertTrue(compareDisplayLabel(BBB, C) < 0); + assertTrue(compareDisplayLabel(AA, C) < 0); + assertTrue(compareDisplayLabel(AA, AA) == 0); + assertTrue(compareDisplayLabel(BBB, AA) > 0); + + assertTrue(compareDisplayLabel(EMPTY, AA) < 0); + assertTrue(compareDisplayLabel(EMPTY, NULL) == 0); + } + + private int compareDisplayLabel(AccountType lhs, AccountType rhs) { + return new AccountType.DisplayLabelComparator(getContext()).compare(lhs, rhs); + } + + private class AccountTypeForDisplayLabelTest extends AccountType { + private final String mDisplayLabel; + + public AccountTypeForDisplayLabelTest(String displayLabel) { + mDisplayLabel = displayLabel; + } + + @Override + public CharSequence getDisplayLabel(Context context) { + return mDisplayLabel; + } + + @Override + public boolean isGroupMembershipEditable() { + return false; + } + + @Override + public boolean areContactsWritable() { + return false; + } + } +} diff --git a/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java new file mode 100644 index 00000000..56ee8321 --- /dev/null +++ b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2011 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.contacts.common.model.account; + +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.unittest.R; +import com.google.common.base.Objects; + +import java.util.List; + +/** + * Test case for {@link com.android.contacts.common.model.account.ExternalAccountType}. + * + * adb shell am instrument -w -e class com.android.contacts.model.ExternalAccountTypeTest \ + com.android.contacts.tests/android.test.InstrumentationTestRunner + */ +@SmallTest +public class ExternalAccountTypeTest extends AndroidTestCase { + public void testResolveExternalResId() { + final Context c = getContext(); + // In this test we use the test package itself as an external package. + final String packageName = getTestContext().getPackageName(); + + // Resource name empty. + assertEquals(-1, ExternalAccountType.resolveExternalResId(c, null, packageName, "")); + assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "", packageName, "")); + + // Name doesn't begin with '@' + assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "x", packageName, "")); + + // Invalid resource name + assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "@", packageName, "")); + assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "@a", packageName, "")); + assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "@a/b", packageName, "")); + + // Valid resource name + assertEquals(R.string.test_string, ExternalAccountType.resolveExternalResId(c, + "@string/test_string", packageName, "")); + } + + /** + * Initialize with an invalid package name and see if type type will *not* be initialized. + */ + public void testNoPackage() { + final ExternalAccountType type = new ExternalAccountType(getContext(), + "!!!no such package name!!!", false); + assertFalse(type.isInitialized()); + } + + /** + * Initialize with the name of an existing package, which has no contacts.xml metadata. + */ + /* + public void testNoMetadata() { + // Use the main application package, which does exist, but has no contacts.xml in it. + String packageName = getContext().getPackageName(); + Log.e("TEST", packageName); + final ExternalAccountType type = new ExternalAccountType(getContext(), + packageName, false); + assertTrue(type.isInitialized()); + } + */ + + /** + * Initialize with the test package itself and see if EditSchema is correctly parsed. + */ + public void testEditSchema() { + final ExternalAccountType type = new ExternalAccountType(getContext(), + getTestContext().getPackageName(), false); + + assertTrue(type.isInitialized()); + + // Let's just check if the DataKinds are registered. + assertNotNull(type.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME)); + assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME)); + assertNotNull(type.getKindForMimetype(Email.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Im.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Organization.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Note.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Website.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(SipAddress.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Event.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(Relation.CONTENT_ITEM_TYPE)); + } + + /** + * Initialize with "contacts_fallback.xml" and compare the DataKinds to those of + * {@link com.android.contacts.common.model.account.FallbackAccountType}. + */ + public void testEditSchema_fallback() { + final ExternalAccountType type = new ExternalAccountType(getContext(), + getTestContext().getPackageName(), false, + getTestContext().getResources().getXml(R.xml.contacts_fallback) + ); + + assertTrue(type.isInitialized()); + + // Create a fallback type with the same resource package name, and compare all the data + // kinds to its. + final AccountType reference = FallbackAccountType.createWithPackageNameForTest( + getContext(), type.resourcePackageName); + + assertsDataKindEquals(reference.getSortedDataKinds(), type.getSortedDataKinds()); + } + + public void testEditSchema_mustHaveChecks() { + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_base, true); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_photo, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr1, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr2, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr3, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr4, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr5, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr6, false); + checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr7, false); + } + + private void checkEditSchema_mustHaveChecks(int xmlResId, boolean expectInitialized) { + final ExternalAccountType type = new ExternalAccountType(getContext(), + getTestContext().getPackageName(), false, + getTestContext().getResources().getXml(xmlResId) + ); + + assertEquals(expectInitialized, type.isInitialized()); + } + + /** + * Initialize with "contacts_readonly.xml" and see if all data kinds are correctly registered. + */ + public void testReadOnlyDefinition() { + final ExternalAccountType type = new ExternalAccountType(getContext(), + getTestContext().getPackageName(), false, + getTestContext().getResources().getXml(R.xml.contacts_readonly) + ); + assertTrue(type.isInitialized()); + + // Shouldn't have a "null" mimetype. + assertTrue(type.getKindForMimetype(null) == null); + + // 3 kinds are defined in XML and 4 are added by default. + assertEquals(4 + 3, type.getSortedDataKinds().size()); + + // Check for the default kinds. + assertNotNull(type.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE)); + assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME)); + assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME)); + assertNotNull(type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE)); + + // Check for type specific kinds. + DataKind kind = type.getKindForMimetype("vnd.android.cursor.item/a.b.c"); + assertNotNull(kind); + // No check for icon -- we actually just ignore it. + assertEquals("data1", ((BaseAccountType.SimpleInflater) kind.actionHeader) + .getColumnNameForTest()); + assertEquals("data2", ((BaseAccountType.SimpleInflater) kind.actionBody) + .getColumnNameForTest()); + assertEquals(true, kind.actionBodySocial); + + kind = type.getKindForMimetype("vnd.android.cursor.item/d.e.f"); + assertNotNull(kind); + assertEquals("data3", ((BaseAccountType.SimpleInflater) kind.actionHeader) + .getColumnNameForTest()); + assertEquals("data4", ((BaseAccountType.SimpleInflater) kind.actionBody) + .getColumnNameForTest()); + assertEquals(false, kind.actionBodySocial); + + kind = type.getKindForMimetype("vnd.android.cursor.item/xyz"); + assertNotNull(kind); + assertEquals("data5", ((BaseAccountType.SimpleInflater) kind.actionHeader) + .getColumnNameForTest()); + assertEquals("data6", ((BaseAccountType.SimpleInflater) kind.actionBody) + .getColumnNameForTest()); + assertEquals(true, kind.actionBodySocial); + } + + private static void assertsDataKindEquals(List<DataKind> expectedKinds, + List<DataKind> actualKinds) { + final int count = Math.max(actualKinds.size(), expectedKinds.size()); + for (int i = 0; i < count; i++) { + String actual = actualKinds.size() > i ? actualKinds.get(i).toString() : "(n/a)"; + String expected = expectedKinds.size() > i ? expectedKinds.get(i).toString() : "(n/a)"; + + // Because assertEquals()'s output is not very friendly when comparing two similar + // strings, we manually do the check. + if (!Objects.equal(actual, expected)) { + final int commonPrefixEnd = findCommonPrefixEnd(actual, expected); + fail("Kind #" + i + + "\n[Actual]\n" + insertMarkerAt(actual, commonPrefixEnd) + + "\n[Expected]\n" + insertMarkerAt(expected, commonPrefixEnd)); + } + } + } + + private static int findCommonPrefixEnd(String s1, String s2) { + int i = 0; + for (;;) { + final boolean s1End = (s1.length() <= i); + final boolean s2End = (s2.length() <= i); + if (s1End || s2End) { + return i; + } + if (s1.charAt(i) != s2.charAt(i)) { + return i; + } + i++; + } + } + + private static String insertMarkerAt(String s, int position) { + final String MARKER = "***"; + if (position > s.length()) { + return s + MARKER; + } else { + return new StringBuilder(s).insert(position, MARKER).toString(); + } + } +} diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticationService.java b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticationService.java new file mode 100644 index 00000000..93d1f4a9 --- /dev/null +++ b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticationService.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 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.contacts.common.tests.testauth; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +public abstract class TestAuthenticationService extends Service { + + private TestAuthenticator mAuthenticator; + + @Override + public void onCreate() { + Log.v(TestauthConstants.LOG_TAG, this + " Service started."); + mAuthenticator = new TestAuthenticator(this); + } + + @Override + public void onDestroy() { + Log.v(TestauthConstants.LOG_TAG, this + " Service stopped."); + } + + @Override + public IBinder onBind(Intent intent) { + Log.v(TestauthConstants.LOG_TAG, this + " getBinder() intent=" + intent); + return mAuthenticator.getIBinder(); + } + + public static class Basic extends TestAuthenticationService { + } +} diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticator.java b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticator.java new file mode 100644 index 00000000..2f676c70 --- /dev/null +++ b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticator.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2011 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.contacts.common.tests.testauth; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; + +/** + * Simple authenticator. It has no "login" dialogs/activities. When you add a new account, it'll + * just create a new account with a unique name. + */ +class TestAuthenticator extends AbstractAccountAuthenticator { + private static final String PASSWORD = "xxx"; // any string will do. + + // To remember the last user-ID. + private static final String PREF_KEY_LAST_USER_ID = "TestAuthenticator.PREF_KEY_LAST_USER_ID"; + + private final Context mContext; + + public TestAuthenticator(Context context) { + super(context); + mContext = context.getApplicationContext(); + } + + /** + * @return a new, unique username. + */ + private String newUniqueUserName() { + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(mContext); + final int nextId = prefs.getInt(PREF_KEY_LAST_USER_ID, 0) + 1; + prefs.edit().putInt(PREF_KEY_LAST_USER_ID, nextId).apply(); + + return "User-" + nextId; + } + + /** + * Create a new account with the name generated by {@link #newUniqueUserName()}. + */ + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) { + Log.v(TestauthConstants.LOG_TAG, "addAccount() type=" + accountType); + final Bundle bundle = new Bundle(); + + final Account account = new Account(newUniqueUserName(), accountType); + + // Create an account. + AccountManager.get(mContext).addAccountExplicitly(account, PASSWORD, null); + + // And return it. + bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + return bundle; + } + + /** + * Just return the user name as the authtoken. + */ + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle loginOptions) { + Log.v(TestauthConstants.LOG_TAG, "getAuthToken() account=" + account); + final Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + bundle.putString(AccountManager.KEY_AUTHTOKEN, account.name); + + return bundle; + } + + @Override + public Bundle confirmCredentials( + AccountAuthenticatorResponse response, Account account, Bundle options) { + Log.v(TestauthConstants.LOG_TAG, "confirmCredentials()"); + return null; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + Log.v(TestauthConstants.LOG_TAG, "editProperties()"); + throw new UnsupportedOperationException(); + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + // null means we don't support multiple authToken types + Log.v(TestauthConstants.LOG_TAG, "getAuthTokenLabel()"); + return null; + } + + @Override + public Bundle hasFeatures( + AccountAuthenticatorResponse response, Account account, String[] features) { + // This call is used to query whether the Authenticator supports + // specific features. We don't expect to get called, so we always + // return false (no) for any queries. + Log.v(TestauthConstants.LOG_TAG, "hasFeatures()"); + final Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + return result; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle loginOptions) { + Log.v(TestauthConstants.LOG_TAG, "updateCredentials()"); + return null; + } +} diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestSyncAdapter.java b/tests/src/com/android/contacts/common/tests/testauth/TestSyncAdapter.java new file mode 100644 index 00000000..a7c0f83c --- /dev/null +++ b/tests/src/com/android/contacts/common/tests/testauth/TestSyncAdapter.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2011 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.contacts.common.tests.testauth; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SyncResult; +import android.os.Bundle; +import android.provider.ContactsContract.RawContacts; +import android.util.Log; + +/** + * Simple (minimal) sync adapter. + * + */ +public class TestSyncAdapter extends AbstractThreadedSyncAdapter { + private final AccountManager mAccountManager; + + private final Context mContext; + + public TestSyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + mContext = context.getApplicationContext(); + mAccountManager = AccountManager.get(mContext); + } + + /** + * Doesn't actually sync, but sweep up all existing local-only contacts. + */ + @Override + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + Log.v(TestauthConstants.LOG_TAG, "TestSyncAdapter.onPerformSync() account=" + account); + + // First, claim all local-only contacts, if any. + ContentResolver cr = mContext.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(RawContacts.ACCOUNT_NAME, account.name); + values.put(RawContacts.ACCOUNT_TYPE, account.type); + final int count = cr.update(RawContacts.CONTENT_URI, values, + RawContacts.ACCOUNT_NAME + " IS NULL AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", + null); + if (count > 0) { + Log.v(TestauthConstants.LOG_TAG, "Claimed " + count + " local raw contacts"); + } + + // TODO: Clear isDirty flag + // TODO: Remove isDeleted raw contacts + } +} diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestSyncService.java b/tests/src/com/android/contacts/common/tests/testauth/TestSyncService.java new file mode 100644 index 00000000..3354cb4e --- /dev/null +++ b/tests/src/com/android/contacts/common/tests/testauth/TestSyncService.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 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.contacts.common.tests.testauth; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public abstract class TestSyncService extends Service { + + private static TestSyncAdapter sSyncAdapter; + + @Override + public void onCreate() { + if (sSyncAdapter == null) { + sSyncAdapter = new TestSyncAdapter(getApplicationContext(), true); + } + } + + @Override + public IBinder onBind(Intent intent) { + return sSyncAdapter.getSyncAdapterBinder(); + } + + public static class Basic extends TestSyncService { + } +} diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestauthConstants.java b/tests/src/com/android/contacts/common/tests/testauth/TestauthConstants.java new file mode 100644 index 00000000..3ce7f5ab --- /dev/null +++ b/tests/src/com/android/contacts/common/tests/testauth/TestauthConstants.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2011 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.contacts.common.tests.testauth; + +class TestauthConstants { + public static final String LOG_TAG = "Testauth"; +} |