diff options
author | Chiao Cheng <chiaocheng@google.com> | 2012-11-07 18:20:17 -0800 |
---|---|---|
committer | Chiao Cheng <chiaocheng@google.com> | 2012-11-09 13:16:16 -0800 |
commit | 63ac534dcf60e9a6c651ef2434557bec922b9a7d (patch) | |
tree | 6dc88518e13f50fd7d30ba38ef813568dabce82b | |
parent | a23825490fdd593bceca56fd474a2f9d823c6137 (diff) | |
download | android_packages_apps_ContactsCommon-63ac534dcf60e9a6c651ef2434557bec922b9a7d.tar.gz android_packages_apps_ContactsCommon-63ac534dcf60e9a6c651ef2434557bec922b9a7d.tar.bz2 android_packages_apps_ContactsCommon-63ac534dcf60e9a6c651ef2434557bec922b9a7d.zip |
Further clean-up of PhoneFavoriteFragment.
Moving common dependencies from Contacts to ContactsCommon.
Bug: 6993891
Change-Id: I7530d13771b65f17dafa3f4f8283965622b1c71e
22 files changed, 1773 insertions, 0 deletions
diff --git a/res/color/primary_text_color.xml b/res/color/primary_text_color.xml new file mode 100644 index 00000000..acc2fb7a --- /dev/null +++ b/res/color/primary_text_color.xml @@ -0,0 +1,22 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_activated="true" android:color="#FFFFFF" /> + <item android:color="#333333" /> <!-- not selected --> + +</selector> diff --git a/res/drawable-hdpi/ic_contacts_holo_dark.png b/res/drawable-hdpi/ic_contacts_holo_dark.png Binary files differnew file mode 100644 index 00000000..e5deb011 --- /dev/null +++ b/res/drawable-hdpi/ic_contacts_holo_dark.png diff --git a/res/drawable-hdpi/ic_divider_dashed_holo_dark.png b/res/drawable-hdpi/ic_divider_dashed_holo_dark.png Binary files differnew file mode 100644 index 00000000..663a2f88 --- /dev/null +++ b/res/drawable-hdpi/ic_divider_dashed_holo_dark.png diff --git a/res/drawable-mdpi/ic_contacts_holo_dark.png b/res/drawable-mdpi/ic_contacts_holo_dark.png Binary files differnew file mode 100644 index 00000000..d08b94a4 --- /dev/null +++ b/res/drawable-mdpi/ic_contacts_holo_dark.png diff --git a/res/drawable-mdpi/ic_divider_dashed_holo_dark.png b/res/drawable-mdpi/ic_divider_dashed_holo_dark.png Binary files differnew file mode 100644 index 00000000..6652541c --- /dev/null +++ b/res/drawable-mdpi/ic_divider_dashed_holo_dark.png diff --git a/res/drawable-xhdpi/ic_contacts_holo_dark.png b/res/drawable-xhdpi/ic_contacts_holo_dark.png Binary files differnew file mode 100644 index 00000000..dc4c390f --- /dev/null +++ b/res/drawable-xhdpi/ic_contacts_holo_dark.png diff --git a/res/drawable-xhdpi/ic_divider_dashed_holo_dark.png b/res/drawable-xhdpi/ic_divider_dashed_holo_dark.png Binary files differnew file mode 100644 index 00000000..34e8fd25 --- /dev/null +++ b/res/drawable-xhdpi/ic_divider_dashed_holo_dark.png diff --git a/res/layout/contact_tile_frequent.xml b/res/layout/contact_tile_frequent.xml new file mode 100644 index 00000000..9219f56a --- /dev/null +++ b/res/layout/contact_tile_frequent.xml @@ -0,0 +1,78 @@ +<?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. +--> +<view + xmlns:android="http://schemas.android.com/apk/res/android" + class="com.android.contacts.list.ContactTileFrequentView" + android:focusable="true" + android:background="?android:attr/selectableItemBackground" + android:nextFocusRight="@+id/contact_tile_quick"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <com.android.contacts.widget.LayoutSuppressingQuickContactBadge + android:id="@+id/contact_tile_quick" + android:layout_width="64dip" + android:layout_height="64dip" + android:layout_alignParentRight="true" + android:scaleType="centerCrop" + android:focusable="true" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="64dip" + android:orientation="vertical" + android:layout_alignParentBottom="true" + android:gravity="center_vertical" + android:paddingRight="80dip" + android:paddingLeft="8dip"> + + <TextView + android:id="@+id/contact_tile_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primary_text_color" + android:textSize="18sp" + android:singleLine="true" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:ellipsize="marquee" /> + + <TextView + android:id="@+id/contact_tile_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:singleLine="true" + android:drawablePadding="4dip" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:ellipsize="marquee" /> + + </LinearLayout> + + <View + android:id="@+id/contact_tile_horizontal_divider" + android:layout_width="match_parent" + android:layout_height="1px" + android:background="?android:attr/listDivider" + android:layout_below="@id/contact_tile_quick" /> + + </RelativeLayout> + +</view> diff --git a/res/layout/contact_tile_frequent_phone.xml b/res/layout/contact_tile_frequent_phone.xml new file mode 100644 index 00000000..cae5ec28 --- /dev/null +++ b/res/layout/contact_tile_frequent_phone.xml @@ -0,0 +1,97 @@ +<?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. +--> + +<!-- Layout parameters are set programmatically. --> +<view + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/contact_tile_frequent_phone" + class="com.android.contacts.list.ContactTilePhoneFrequentView" + android:focusable="true" + android:background="?android:attr/selectableItemBackground" + android:nextFocusLeft="@+id/contact_tile_quick"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <com.android.contacts.widget.LayoutSuppressingQuickContactBadge + android:id="@id/contact_tile_quick" + android:layout_width="64dip" + android:layout_height="64dip" + android:layout_alignParentLeft="true" + android:nextFocusRight="@id/contact_tile_frequent_phone" + android:scaleType="centerCrop" + android:focusable="true" /> + + <TextView + android:id="@+id/contact_tile_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="8dip" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_marginTop="8dip" + android:layout_toRightOf="@id/contact_tile_quick" + android:singleLine="true" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:ellipsize="marquee" /> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/contact_tile_name" + android:layout_toRightOf="@id/contact_tile_quick" + android:gravity="center_vertical"> + + <TextView + android:id="@+id/contact_tile_phone_number" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="?attr/list_item_data_width_weight" + android:textSize="14sp" + android:ellipsize="marquee" + android:textColor="@color/dialtacts_secondary_text_color" + android:layout_marginLeft="8dip" + android:singleLine="true" + android:layout_gravity="bottom" /> + + <TextView + android:id="@+id/contact_tile_phone_type" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="?attr/list_item_label_width_weight" + android:textSize="12sp" + android:ellipsize="marquee" + android:singleLine="true" + android:textAllCaps="true" + android:textColor="@color/dialtacts_secondary_text_color" + android:layout_marginLeft="8dip" + android:gravity="right" + android:layout_gravity="bottom" /> + + </LinearLayout> + + <View + android:id="@+id/contact_tile_horizontal_divider" + android:layout_width="match_parent" + android:layout_height="1px" + android:background="?android:attr/listDivider" + android:layout_below="@id/contact_tile_quick" /> + + </RelativeLayout> + +</view> diff --git a/res/layout/contact_tile_phone_starred.xml b/res/layout/contact_tile_phone_starred.xml new file mode 100644 index 00000000..053ffa6b --- /dev/null +++ b/res/layout/contact_tile_phone_starred.xml @@ -0,0 +1,72 @@ +<?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. +--> +<view + xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@null" + android:paddingBottom="1dip" + android:paddingRight="1dip" + class="com.android.contacts.list.ContactTilePhoneStarredView" > + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <com.android.contacts.widget.LayoutSuppressingImageView + android:id="@+id/contact_tile_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" /> + + <TextView + android:id="@+id/contact_tile_name" + android:layout_width="match_parent" + android:layout_height="@dimen/contact_tile_shadowbox_height" + android:background="@color/contact_tile_shadow_box_color" + android:gravity="center_vertical" + android:textColor="@android:color/white" + android:singleLine="true" + android:textSize="16sp" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:ellipsize="marquee" + android:layout_alignParentBottom="true" + android:paddingLeft="8dip" + android:paddingRight="47dip" + android:drawableRight="@drawable/ic_divider_dashed_holo_dark" /> + + <View + android:id="@+id/contact_tile_push_state" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusable="true" + android:nextFocusRight="@+id/contact_tile_secondary_button" + android:background="?android:attr/selectableItemBackground" /> + + <ImageButton + android:id="@id/contact_tile_secondary_button" + android:src="@drawable/ic_contacts_holo_dark" + android:background="?android:attr/selectableItemBackground" + android:layout_height="@dimen/contact_tile_shadowbox_height" + android:layout_width="48dip" + android:paddingRight="8dip" + android:paddingLeft="8dip" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:contentDescription="@string/description_view_contact_detail" /> + + </RelativeLayout> + +</view> diff --git a/res/layout/contact_tile_starred.xml b/res/layout/contact_tile_starred.xml new file mode 100644 index 00000000..cfc74d80 --- /dev/null +++ b/res/layout/contact_tile_starred.xml @@ -0,0 +1,80 @@ +<?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. +--> +<view + xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@null" + android:paddingBottom="1dip" + android:paddingRight="1dip" + class="com.android.contacts.list.ContactTileStarredView" > + + <RelativeLayout + android:id="@+id/contact_tile_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <com.android.contacts.widget.LayoutSuppressingImageView + android:id="@+id/contact_tile_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="@dimen/contact_tile_shadowbox_height" + android:orientation="vertical" + android:background="@color/contact_tile_shadow_box_color" + android:layout_alignParentBottom="true" + android:gravity="center_vertical" + android:paddingRight="8dip" + android:paddingLeft="8dip"> + + <TextView + android:id="@+id/contact_tile_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@android:color/white" + android:textSize="16sp" + android:singleLine="true" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:ellipsize="marquee" /> + + <TextView + android:id="@+id/contact_tile_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@color/people_contact_tile_status_color" + android:singleLine="true" + android:drawablePadding="4dip" + android:paddingBottom="4dip" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:layout_marginTop="-3dip" + android:ellipsize="marquee" /> + + </LinearLayout> + + <View + android:id="@+id/contact_tile_push_state" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusable="true" + android:background="?android:attr/selectableItemBackground" /> + + </RelativeLayout> + +</view> diff --git a/res/layout/contact_tile_starred_quick_contact.xml b/res/layout/contact_tile_starred_quick_contact.xml new file mode 100644 index 00000000..a396c41b --- /dev/null +++ b/res/layout/contact_tile_starred_quick_contact.xml @@ -0,0 +1,78 @@ +<?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. +--> +<view + xmlns:android="http://schemas.android.com/apk/res/android" + android:paddingBottom="1dip" + android:paddingRight="1dip" + android:background="@null" + class="com.android.contacts.list.ContactTileStarredView" > + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <com.android.contacts.widget.LayoutSuppressingImageView + android:id="@+id/contact_tile_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="@dimen/contact_tile_shadowbox_height" + android:orientation="vertical" + android:background="@color/contact_tile_shadow_box_color" + android:layout_alignParentBottom="true" + android:gravity="center_vertical" + android:paddingRight="8dip" + android:paddingLeft="8dip"> + + <TextView + android:id="@+id/contact_tile_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@android:color/white" + android:textSize="16sp" + android:singleLine="true" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:ellipsize="marquee" /> + + <TextView + android:id="@+id/contact_tile_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@color/people_contact_tile_status_color" + android:singleLine="true" + android:drawablePadding="4dip" + android:fadingEdge="horizontal" + android:fadingEdgeLength="3dip" + android:layout_marginTop="-3dip" + android:ellipsize="marquee" /> + + </LinearLayout> + + <QuickContactBadge + android:id="@+id/contact_tile_quick" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusable="true" + android:background="@null" /> + + </RelativeLayout> + +</view> diff --git a/res/layout/list_separator.xml b/res/layout/list_separator.xml new file mode 100644 index 00000000..d94e18c4 --- /dev/null +++ b/res/layout/list_separator.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2008 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="16dip" + android:paddingRight="16dip" + android:focusable="false"> + <TextView + android:id="@+id/title" + style="@style/ContactListSeparatorTextViewStyle" + android:paddingLeft="8dip" + android:paddingRight="8dip" /> +</FrameLayout> diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml new file mode 100644 index 00000000..bdbc0d4a --- /dev/null +++ b/res/values-sw580dp/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="detail_item_side_margin">0dip</dimen> +</resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 13eadd35..48ef4ece 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -25,4 +25,12 @@ <!-- Color of image view placeholder. --> <color name="image_placeholder">#DDDDDD</color> + <!-- Secondary text color in the Phone app --> + <color name="dialtacts_secondary_text_color">#888888</color> + + <!-- Color of the semi-transparent shadow box on contact tiles --> + <color name="contact_tile_shadow_box_color">#7F000000</color> + + <!-- Color of the status message for starred contacts in the People app --> + <color name="people_contact_tile_status_color">#CCCCCC</color> </resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c17234c5..97e6fe78 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -24,4 +24,20 @@ Right now the drawable has implicit 32dip minimal height, which is confusing. This value is for making the hidden configuration explicit in xml. --> <dimen name="list_section_divider_min_height">32dip</dimen> + + <!-- Vertical and horizontal padding in between contact tiles --> + <dimen name="contact_tile_divider_padding">1dip</dimen> + + <!-- Left and right padding for a contact detail item --> + <dimen name="detail_item_side_margin">16dip</dimen> + + <!-- ContactTile Layouts --> + <!-- + Use sp instead of dip so that the shadowbox heights can all scale uniformly + when the font size is scaled for accessibility purposes + --> + <dimen name="contact_tile_shadowbox_height">48sp</dimen> + + <!-- Top padding of the ListView in the contact tile list --> + <dimen name="contact_tile_list_padding_top">0dip</dimen> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 87ea719c..cc855664 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -177,4 +177,21 @@ <!-- Shown as the display name for a person when the name is missing or unknown. [CHAR LIMIT=18]--> <string name="missing_name">(No name)</string> + + <!-- The text displayed on the divider for the Favorites tab in Phone app indicating that items below it are frequently called as opposed to starred contacts [CHAR LIMIT = 39] --> + <string name="favoritesFrequentCalled">Frequently called</string> + + <!-- The text displayed on the divider for the Favorites tab in People app indicating that items below it are frequently contacted [CHAR LIMIT = 39] --> + <string name="favoritesFrequentContacted">Frequently contacted</string> + + <!-- String describing a contact picture that introduces users to the contact detail screen. + + Used by AccessibilityService to announce the purpose of the button. + + [CHAR LIMIT=NONE] + --> + <string name="description_view_contact_detail" msgid="2795575601596468581">View contact</string> + + <!-- 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> </resources> diff --git a/res/values/styles.xml b/res/values/styles.xml index 45137e73..7522f99a 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -19,4 +19,25 @@ <style name="DirectoryHeader"> <item name="android:background">@android:color/transparent</item> </style> + + <!-- TextView style with blue underline. It is most suitable for headers. + +This is similar to ?android:attr/listSeparatorTextView but uses different +background and text color. See also android:style/Widget.Holo.TextView.ListSeparator +(which is private, so we cannot specify it as a parent style). --> + <style name="ContactListSeparatorTextViewStyle"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <!-- See comments for @dimen/list_section_divider_min_height --> + <item name="android:minHeight">@dimen/list_section_divider_min_height</item> + <item name="android:background">@drawable/list_section_divider_holo_custom</item> + <item name="android:textAppearance">?android:attr/textAppearanceSmall</item> + <item name="android:textStyle">bold</item> + <item name="android:textColor">@color/people_app_theme_color</item> + <item name="android:gravity">center_vertical</item> + <item name="android:paddingLeft">8dip</item> + <item name="android:ellipsize">end</item> + <item name="android:singleLine">true</item> + <item name="android:textAllCaps">true</item> + </style> </resources> diff --git a/src/com/android/contacts/common/MoreContactUtils.java b/src/com/android/contacts/common/MoreContactUtils.java index cceea6a0..c37e29c6 100644 --- a/src/com/android/contacts/common/MoreContactUtils.java +++ b/src/com/android/contacts/common/MoreContactUtils.java @@ -16,9 +16,13 @@ package com.android.contacts.common; +import android.content.Context; +import android.graphics.Rect; import android.provider.ContactsContract; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; /** * Shared static contact utility methods. @@ -85,4 +89,34 @@ public class MoreContactUtils { index2++; } } + + /** + * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates + * that are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to + * how the target {@link android.graphics.Rect} is calculated in + * {@link android.provider.ContactsContract.QuickContact#showQuickContact}. + */ + public static Rect getTargetRectFromView(Context context, View view) { + final float appScale = context.getResources().getCompatibilityInfo().applicationScale; + final int[] pos = new int[2]; + view.getLocationOnScreen(pos); + + final Rect rect = new Rect(); + rect.left = (int) (pos[0] * appScale + 0.5f); + rect.top = (int) (pos[1] * appScale + 0.5f); + rect.right = (int) ((pos[0] + view.getWidth()) * appScale + 0.5f); + rect.bottom = (int) ((pos[1] + view.getHeight()) * appScale + 0.5f); + return rect; + } + + /** + * Returns a header view based on the R.layout.list_separator, where the + * containing {@link android.widget.TextView} is set using the given textResourceId. + */ + public static View createHeaderView(Context context, int textResourceId) { + View view = View.inflate(context, R.layout.list_separator, null); + TextView textView = (TextView) view.findViewById(R.id.title); + textView.setText(context.getString(textResourceId)); + return view; + } } diff --git a/src/com/android/contacts/common/list/ContactTileAdapter.java b/src/com/android/contacts/common/list/ContactTileAdapter.java new file mode 100644 index 00000000..50b5ee39 --- /dev/null +++ b/src/com/android/contacts/common/list/ContactTileAdapter.java @@ -0,0 +1,680 @@ +/* + * 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.list; + +import android.content.ContentUris; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; + +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPresenceIconUtil; +import com.android.contacts.common.ContactStatusUtil; +import com.android.contacts.common.ContactTileLoaderFactory; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.R; + +import java.util.ArrayList; + +/** + * Arranges contacts favorites according to provided {@link DisplayType}. + * Also allows for a configurable number of columns and {@link DisplayType} + */ +public class ContactTileAdapter extends BaseAdapter { + private static final String TAG = ContactTileAdapter.class.getSimpleName(); + + private DisplayType mDisplayType; + private ContactTileView.Listener mListener; + private Context mContext; + private Resources mResources; + protected Cursor mContactCursor = null; + private ContactPhotoManager mPhotoManager; + protected int mNumFrequents; + + /** + * Index of the first NON starred contact in the {@link Cursor} + * Only valid when {@link DisplayType#STREQUENT} is true + */ + private int mDividerPosition; + protected int mColumnCount; + private int mStarredIndex; + + protected int mIdIndex; + protected int mLookupIndex; + protected int mPhotoUriIndex; + protected int mNameIndex; + protected int mPresenceIndex; + protected int mStatusIndex; + + /** + * Only valid when {@link DisplayType#STREQUENT_PHONE_ONLY} is true + */ + private int mPhoneNumberIndex; + private int mPhoneNumberTypeIndex; + private int mPhoneNumberLabelIndex; + + private boolean mIsQuickContactEnabled = false; + private final int mPaddingInPixels; + + /** + * Configures the adapter to filter and display contacts using different view types. + * TODO: Create Uris to support getting Starred_only and Frequent_only cursors. + */ + public enum DisplayType { + /** + * Displays a mixed view type of starred and frequent contacts + */ + STREQUENT, + + /** + * Displays a mixed view type of starred and frequent contacts based on phone data. + * Also includes secondary touch target. + */ + STREQUENT_PHONE_ONLY, + + /** + * Display only starred contacts + */ + STARRED_ONLY, + + /** + * Display only most frequently contacted + */ + FREQUENT_ONLY, + + /** + * Display all contacts from a group in the cursor + * Use {@link com.android.contacts.GroupMemberLoader} + * when passing {@link Cursor} into loadFromCusor method. + * + * Group member logic has been moved into GroupMemberTileAdapter. This constant is still + * needed by calling classes. + */ + GROUP_MEMBERS + } + + public ContactTileAdapter(Context context, ContactTileView.Listener listener, int numCols, + DisplayType displayType) { + mListener = listener; + mContext = context; + mResources = context.getResources(); + mColumnCount = (displayType == DisplayType.FREQUENT_ONLY ? 1 : numCols); + mDisplayType = displayType; + mNumFrequents = 0; + + // Converting padding in dips to padding in pixels + mPaddingInPixels = mContext.getResources() + .getDimensionPixelSize(R.dimen.contact_tile_divider_padding); + + bindColumnIndices(); + } + + public void setPhotoLoader(ContactPhotoManager photoLoader) { + mPhotoManager = photoLoader; + } + + public void setColumnCount(int columnCount) { + mColumnCount = columnCount; + } + + public void setDisplayType(DisplayType displayType) { + mDisplayType = displayType; + } + + public void enableQuickContact(boolean enableQuickContact) { + mIsQuickContactEnabled = enableQuickContact; + } + + /** + * Sets the column indices for expected {@link Cursor} + * based on {@link DisplayType}. + */ + protected void bindColumnIndices() { + mIdIndex = ContactTileLoaderFactory.CONTACT_ID; + mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY; + mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI; + mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME; + mStarredIndex = ContactTileLoaderFactory.STARRED; + mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE; + mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS; + + mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER; + mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE; + mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL; + } + + /** + * Gets the number of frequents from the passed in cursor. + * + * This methods is needed so the GroupMemberTileAdapter can override this. + * + * @param cursor The cursor to get number of frequents from. + */ + protected void saveNumFrequentsFromCursor(Cursor cursor) { + + // count the number of frequents + switch (mDisplayType) { + case STARRED_ONLY: + mNumFrequents = 0; + break; + case STREQUENT: + case STREQUENT_PHONE_ONLY: + mNumFrequents = cursor.getCount() - mDividerPosition; + break; + case FREQUENT_ONLY: + mNumFrequents = cursor.getCount(); + break; + default: + throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); + } + } + + /** + * Creates {@link ContactTileView}s for each item in {@link Cursor}. + * + * Else use {@link ContactTileLoaderFactory} + */ + public void setContactCursor(Cursor cursor) { + mContactCursor = cursor; + mDividerPosition = getDividerPosition(cursor); + + saveNumFrequentsFromCursor(cursor); + + // cause a refresh of any views that rely on this data + notifyDataSetChanged(); + } + + /** + * Iterates over the {@link Cursor} + * Returns position of the first NON Starred Contact + * Returns -1 if {@link DisplayType#STARRED_ONLY} + * Returns 0 if {@link DisplayType#FREQUENT_ONLY} + */ + protected int getDividerPosition(Cursor cursor) { + if (cursor == null || cursor.isClosed()) { + throw new IllegalStateException("Unable to access cursor"); + } + + switch (mDisplayType) { + case STREQUENT: + case STREQUENT_PHONE_ONLY: + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + if (cursor.getInt(mStarredIndex) == 0) { + return cursor.getPosition(); + } + } + break; + case STARRED_ONLY: + // There is no divider + return -1; + case FREQUENT_ONLY: + // Divider is first + return 0; + default: + throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); + } + + // There are not NON Starred contacts in cursor + // Set divider positon to end + return cursor.getCount(); + } + + protected ContactEntry createContactEntryFromCursor(Cursor cursor, int position) { + // If the loader was canceled we will be given a null cursor. + // In that case, show an empty list of contacts. + if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null; + + cursor.moveToPosition(position); + long id = cursor.getLong(mIdIndex); + String photoUri = cursor.getString(mPhotoUriIndex); + String lookupKey = cursor.getString(mLookupIndex); + + ContactEntry contact = new ContactEntry(); + String name = cursor.getString(mNameIndex); + contact.name = (name != null) ? name : mResources.getString(R.string.missing_name); + contact.status = cursor.getString(mStatusIndex); + contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); + contact.lookupKey = ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); + + // Set phone number and label + if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) { + int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); + String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); + contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, + phoneNumberCustomLabel); + contact.phoneNumber = cursor.getString(mPhoneNumberIndex); + } else { + // Set presence icon and status message + Drawable icon = null; + int presence = 0; + if (!cursor.isNull(mPresenceIndex)) { + presence = cursor.getInt(mPresenceIndex); + icon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence); + } + contact.presenceIcon = icon; + + String statusMessage = null; + if (mStatusIndex != 0 && !cursor.isNull(mStatusIndex)) { + statusMessage = cursor.getString(mStatusIndex); + } + // If there is no status message from the contact, but there was a presence value, + // then use the default status message string + if (statusMessage == null && presence != 0) { + statusMessage = ContactStatusUtil.getStatusString(mContext, presence); + } + contact.status = statusMessage; + } + + return contact; + } + + /** + * Returns the number of frequents that will be displayed in the list. + */ + public int getNumFrequents() { + return mNumFrequents; + } + + @Override + public int getCount() { + if (mContactCursor == null || mContactCursor.isClosed()) { + return 0; + } + + switch (mDisplayType) { + case STARRED_ONLY: + return getRowCount(mContactCursor.getCount()); + case STREQUENT: + case STREQUENT_PHONE_ONLY: + // Takes numbers of rows the Starred Contacts Occupy + int starredRowCount = getRowCount(mDividerPosition); + + // Compute the frequent row count which is 1 plus the number of frequents + // (to account for the divider) or 0 if there are no frequents. + int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents + 1; + + // Return the number of starred plus frequent rows + return starredRowCount + frequentRowCount; + case FREQUENT_ONLY: + // Number of frequent contacts + return mContactCursor.getCount(); + default: + throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); + } + } + + /** + * Returns the number of rows required to show the provided number of entries + * with the current number of columns. + */ + protected int getRowCount(int entryCount) { + return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1; + } + + public int getColumnCount() { + return mColumnCount; + } + + /** + * Returns an ArrayList of the {@link ContactEntry}s that are to appear + * on the row for the given position. + */ + @Override + public ArrayList<ContactEntry> getItem(int position) { + ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); + int contactIndex = position * mColumnCount; + + switch (mDisplayType) { + case FREQUENT_ONLY: + resultList.add(createContactEntryFromCursor(mContactCursor, position)); + break; + case STARRED_ONLY: + for (int columnCounter = 0; columnCounter < mColumnCount; columnCounter++) { + resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + contactIndex++; + } + break; + case STREQUENT: + case STREQUENT_PHONE_ONLY: + if (position < getRowCount(mDividerPosition)) { + for (int columnCounter = 0; columnCounter < mColumnCount && + contactIndex != mDividerPosition; columnCounter++) { + resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + contactIndex++; + } + } else { + /* + * Current position minus how many rows are before the divider and + * Minus 1 for the divider itself provides the relative index of the frequent + * contact being displayed. Then add the dividerPostion to give the offset + * into the contacts cursor to get the absoulte index. + */ + contactIndex = position - getRowCount(mDividerPosition) - 1 + mDividerPosition; + resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + } + break; + default: + throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); + } + return resultList; + } + + @Override + public long getItemId(int position) { + // As we show several selectable items for each ListView row, + // we can not determine a stable id. But as we don't rely on ListView's selection, + // this should not be a problem. + return position; + } + + @Override + public boolean areAllItemsEnabled() { + return (mDisplayType != DisplayType.STREQUENT && + mDisplayType != DisplayType.STREQUENT_PHONE_ONLY); + } + + @Override + public boolean isEnabled(int position) { + return position != getRowCount(mDividerPosition); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + int itemViewType = getItemViewType(position); + + if (itemViewType == ViewTypes.DIVIDER) { + // Checking For Divider First so not to cast convertView + return convertView == null ? getDivider() : convertView; + } + + ContactTileRow contactTileRowView = (ContactTileRow) convertView; + ArrayList<ContactEntry> contactList = getItem(position); + + if (contactTileRowView == null) { + // Creating new row if needed + contactTileRowView = new ContactTileRow(mContext, itemViewType); + } + + contactTileRowView.configureRow(contactList, position == getCount() - 1); + return contactTileRowView; + } + + /** + * Divider uses a list_seperator.xml along with text to denote + * the most frequently contacted contacts. + */ + public View getDivider() { + return MoreContactUtils.createHeaderView(mContext, + mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? + R.string.favoritesFrequentCalled : R.string.favoritesFrequentContacted); + } + + private int getLayoutResourceId(int viewType) { + switch (viewType) { + case ViewTypes.STARRED: + return mIsQuickContactEnabled ? + R.layout.contact_tile_starred_quick_contact : R.layout.contact_tile_starred; + case ViewTypes.FREQUENT: + return mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? + R.layout.contact_tile_frequent_phone : R.layout.contact_tile_frequent; + case ViewTypes.STARRED_PHONE: + return R.layout.contact_tile_phone_starred; + default: + throw new IllegalArgumentException("Unrecognized viewType " + viewType); + } + } + @Override + public int getViewTypeCount() { + return ViewTypes.COUNT; + } + + @Override + public int getItemViewType(int position) { + /* + * Returns view type based on {@link DisplayType}. + * {@link DisplayType#STARRED_ONLY} and {@link DisplayType#GROUP_MEMBERS} + * are {@link ViewTypes#STARRED}. + * {@link DisplayType#FREQUENT_ONLY} is {@link ViewTypes#FREQUENT}. + * {@link DisplayType#STREQUENT} mixes both {@link ViewTypes} + * and also adds in {@link ViewTypes#DIVIDER}. + */ + switch (mDisplayType) { + case STREQUENT: + if (position < getRowCount(mDividerPosition)) { + return ViewTypes.STARRED; + } else if (position == getRowCount(mDividerPosition)) { + return ViewTypes.DIVIDER; + } else { + return ViewTypes.FREQUENT; + } + case STREQUENT_PHONE_ONLY: + if (position < getRowCount(mDividerPosition)) { + return ViewTypes.STARRED_PHONE; + } else if (position == getRowCount(mDividerPosition)) { + return ViewTypes.DIVIDER; + } else { + return ViewTypes.FREQUENT; + } + case STARRED_ONLY: + return ViewTypes.STARRED; + case FREQUENT_ONLY: + return ViewTypes.FREQUENT; + default: + throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); + } + } + + /** + * Returns the "frequent header" position. Only available when STREQUENT or + * STREQUENT_PHONE_ONLY is used for its display type. + */ + public int getFrequentHeaderPosition() { + return getRowCount(mDividerPosition); + } + + /** + * Acts as a row item composed of {@link ContactTileView} + * + * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return + */ + private class ContactTileRow extends FrameLayout { + private int mItemViewType; + private int mLayoutResId; + + public ContactTileRow(Context context, int itemViewType) { + super(context); + mItemViewType = itemViewType; + mLayoutResId = getLayoutResourceId(mItemViewType); + } + + /** + * Configures the row to add {@link ContactEntry}s information to the views + */ + public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) { + int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; + + // Adding tiles to row and filling in contact information + for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { + ContactEntry entry = + columnCounter < list.size() ? list.get(columnCounter) : null; + addTileFromEntry(entry, columnCounter, isLastRow); + } + } + + private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { + final ContactTileView contactTile; + + if (getChildCount() <= childIndex) { + contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null); + // Note: the layoutparam set here is only actually used for FREQUENT. + // We override onMeasure() for STARRED and we don't care the layout param there. + Resources resources = mContext.getResources(); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins( + resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), + 0, + resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), + 0); + contactTile.setLayoutParams(params); + contactTile.setPhotoManager(mPhotoManager); + contactTile.setListener(mListener); + addView(contactTile); + } else { + contactTile = (ContactTileView) getChildAt(childIndex); + } + contactTile.loadFromContact(entry); + + switch (mItemViewType) { + case ViewTypes.STARRED_PHONE: + case ViewTypes.STARRED: + // Setting divider visibilities + contactTile.setPadding(0, 0, + childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, + isLastRow ? 0 : mPaddingInPixels); + break; + case ViewTypes.FREQUENT: + contactTile.setHorizontalDividerVisibility( + isLastRow ? View.GONE : View.VISIBLE); + break; + default: + break; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + switch (mItemViewType) { + case ViewTypes.STARRED_PHONE: + case ViewTypes.STARRED: + onLayoutForTiles(); + return; + default: + super.onLayout(changed, left, top, right, bottom); + return; + } + } + + private void onLayoutForTiles() { + final int count = getChildCount(); + + // Just line up children horizontally. + int childLeft = 0; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + + // Note MeasuredWidth includes the padding. + final int childWidth = child.getMeasuredWidth(); + child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); + childLeft += childWidth; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + switch (mItemViewType) { + case ViewTypes.STARRED_PHONE: + case ViewTypes.STARRED: + onMeasureForTiles(widthMeasureSpec); + return; + default: + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + } + + private void onMeasureForTiles(int widthMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + + final int childCount = getChildCount(); + if (childCount == 0) { + // Just in case... + setMeasuredDimension(width, 0); + return; + } + + // 1. Calculate image size. + // = ([total width] - [total padding]) / [child count] + // + // 2. Set it to width/height of each children. + // If we have a remainder, some tiles will have 1 pixel larger width than its height. + // + // 3. Set the dimensions of itself. + // Let width = given width. + // Let height = image size + bottom paddding. + + final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels; + + // Preferred width / height for images (excluding the padding). + // The actual width may be 1 pixel larger than this if we have a remainder. + final int imageSize = (width - totalPaddingsInPixels) / mColumnCount; + final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels; + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int childWidth = imageSize + child.getPaddingRight() + // Compensate for the remainder + + (i < remainder ? 1 : 0); + final int childHeight = imageSize + child.getPaddingBottom(); + child.measure( + MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY) + ); + } + setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom()); + } + + @Override + public void sendAccessibilityEvent(int eventType) { + // This method is called when the child tile is INVISIBLE (meaning "empty"), and the + // Accessibility Manager needs to find alternative content description to speak. + // Here, we ignore the default behavior, since we don't want to let the manager speak + // a contact name for the tile next to the INVISIBLE tile. + } + } + + /** + * Class to hold contact information + */ + public static class ContactEntry { + public String name; + public String status; + public String phoneLabel; + public String phoneNumber; + public Uri photoUri; + public Uri lookupKey; + public Drawable presenceIcon; + } + + protected static class ViewTypes { + public static final int COUNT = 4; + public static final int STARRED = 0; + public static final int DIVIDER = 1; + public static final int FREQUENT = 2; + public static final int STARRED_PHONE = 3; + } +} diff --git a/src/com/android/contacts/common/list/ContactTileView.java b/src/com/android/contacts/common/list/ContactTileView.java new file mode 100644 index 00000000..e30ed49a --- /dev/null +++ b/src/com/android/contacts/common/list/ContactTileView.java @@ -0,0 +1,191 @@ +/* + * 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.list; + +import android.content.Context; +import android.graphics.Rect; +import android.net.Uri; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.QuickContactBadge; +import android.widget.TextView; + +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.R; + +/** + * A ContactTile displays a contact's picture and name + */ +public abstract class ContactTileView extends FrameLayout { + private final static String TAG = ContactTileView.class.getSimpleName(); + + private Uri mLookupUri; + private ImageView mPhoto; + private QuickContactBadge mQuickContact; + private TextView mName; + private TextView mStatus; + private TextView mPhoneLabel; + private TextView mPhoneNumber; + private ContactPhotoManager mPhotoManager = null; + private View mPushState; + private View mHorizontalDivider; + protected Listener mListener; + + public ContactTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mName = (TextView) findViewById(R.id.contact_tile_name); + + mQuickContact = (QuickContactBadge) findViewById(R.id.contact_tile_quick); + mPhoto = (ImageView) findViewById(R.id.contact_tile_image); + mStatus = (TextView) findViewById(R.id.contact_tile_status); + mPhoneLabel = (TextView) findViewById(R.id.contact_tile_phone_type); + mPhoneNumber = (TextView) findViewById(R.id.contact_tile_phone_number); + mPushState = findViewById(R.id.contact_tile_push_state); + mHorizontalDivider = findViewById(R.id.contact_tile_horizontal_divider); + + OnClickListener listener = createClickListener(); + + if(mPushState != null) { + mPushState.setOnClickListener(listener); + } else { + setOnClickListener(listener); + } + } + + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) return; + mListener.onContactSelected( + getLookupUri(), + MoreContactUtils.getTargetRectFromView(mContext, ContactTileView.this)); + } + }; + } + + public void setPhotoManager(ContactPhotoManager photoManager) { + mPhotoManager = photoManager; + } + + /** + * Populates the data members to be displayed from the + * fields in {@link com.android.contacts.common.list.ContactTileAdapter.ContactEntry} + */ + public void loadFromContact(ContactTileAdapter.ContactEntry entry) { + + if (entry != null) { + mName.setText(entry.name); + mLookupUri = entry.lookupKey; + + if (mStatus != null) { + if (entry.status == null) { + mStatus.setVisibility(View.GONE); + } else { + mStatus.setText(entry.status); + mStatus.setCompoundDrawablesWithIntrinsicBounds(entry.presenceIcon, + null, null, null); + mStatus.setVisibility(View.VISIBLE); + } + } + + if (mPhoneLabel != null) { + mPhoneLabel.setText(entry.phoneLabel); + } + + if (mPhoneNumber != null) { + // TODO: Format number correctly + mPhoneNumber.setText(entry.phoneNumber); + } + + setVisibility(View.VISIBLE); + + if (mPhotoManager != null) { + if (mPhoto != null) { + mPhotoManager.loadPhoto(mPhoto, entry.photoUri, getApproximateImageSize(), + isDarkTheme()); + + if (mQuickContact != null) { + mQuickContact.assignContactUri(mLookupUri); + } + } else if (mQuickContact != null) { + mQuickContact.assignContactUri(mLookupUri); + mPhotoManager.loadPhoto(mQuickContact, entry.photoUri, + getApproximateImageSize(), isDarkTheme()); + } + } else { + Log.w(TAG, "contactPhotoManager not set"); + } + + if (mPushState != null) { + mPushState.setContentDescription(entry.name); + } else if (mQuickContact != null) { + mQuickContact.setContentDescription(entry.name); + } + } else { + setVisibility(View.INVISIBLE); + } + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setHorizontalDividerVisibility(int visibility) { + if (mHorizontalDivider != null) mHorizontalDivider.setVisibility(visibility); + } + + public Uri getLookupUri() { + return mLookupUri; + } + + protected QuickContactBadge getQuickContact() { + return mQuickContact; + } + + /** + * Implemented by subclasses to estimate the size of the picture. This can return -1 if only + * a thumbnail is shown anyway + */ + protected abstract int getApproximateImageSize(); + + protected abstract boolean isDarkTheme(); + + public interface Listener { + /** + * Notification that the contact was selected; no specific action is dictated. + */ + void onContactSelected(Uri contactLookupUri, Rect viewRect); + /** + * Notification that the specified number is to be called. + */ + void onCallNumberDirectly(String phoneNumber); + /** + * @return The width of each tile. This doesn't have to be a precise number (e.g. paddings + * can be ignored), but is used to load the correct picture size from the database + */ + int getApproximateTileWidth(); + } +} diff --git a/src/com/android/contacts/common/list/PhoneNumberListAdapter.java b/src/com/android/contacts/common/list/PhoneNumberListAdapter.java new file mode 100644 index 00000000..c62b6b1f --- /dev/null +++ b/src/com/android/contacts/common/list/PhoneNumberListAdapter.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.ContentUris; +import android.content.Context; +import android.content.CursorLoader; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Callable; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.ContactCounts; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import com.android.contacts.common.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and + * {@link SipAddress#CONTENT_ITEM_TYPE}. + * + * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is + * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable} + * API instead of {@link Phone}. + */ +public class PhoneNumberListAdapter extends ContactEntryListAdapter { + private static final String TAG = PhoneNumberListAdapter.class.getSimpleName(); + + protected static class PhoneQuery { + private static final String[] PROJECTION_PRIMARY = new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_PRIMARY, // 7 + }; + + private static final String[] PROJECTION_ALTERNATIVE = new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_ALTERNATIVE, // 7 + }; + + public static final int PHONE_ID = 0; + public static final int PHONE_TYPE = 1; + public static final int PHONE_LABEL = 2; + public static final int PHONE_NUMBER = 3; + public static final int PHONE_CONTACT_ID = 4; + public static final int PHONE_LOOKUP_KEY = 5; + public static final int PHONE_PHOTO_ID = 6; + public static final int PHONE_DISPLAY_NAME = 7; + } + + private final CharSequence mUnknownNameText; + + private ContactListItemView.PhotoPosition mPhotoPosition; + + private boolean mUseCallableUri; + + public PhoneNumberListAdapter(Context context) { + super(context); + setDefaultFilterHeaderText(R.string.list_filter_phones); + mUnknownNameText = context.getText(android.R.string.unknownName); + } + + protected CharSequence getUnknownNameText() { + return mUnknownNameText; + } + + @Override + public void configureLoader(CursorLoader loader, long directoryId) { + if (directoryId != Directory.DEFAULT) { + Log.w(TAG, "PhoneNumberListAdapter is not ready for non-default directory ID (" + + "directoryId: " + directoryId + ")"); + } + + final Builder builder; + if (isSearchMode()) { + final Uri baseUri = + mUseCallableUri ? Callable.CONTENT_FILTER_URI : Phone.CONTENT_FILTER_URI; + builder = baseUri.buildUpon(); + final String query = getQueryString(); + if (TextUtils.isEmpty(query)) { + builder.appendPath(""); + } else { + builder.appendPath(query); // Builder will encode the query + } + builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, + String.valueOf(directoryId)); + } else { + final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI; + builder = baseUri.buildUpon().appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); + if (isSectionHeaderDisplayEnabled()) { + builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true"); + } + applyFilter(loader, builder, directoryId, getFilter()); + } + + // Remove duplicates when it is possible. + builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true"); + loader.setUri(builder.build()); + + // TODO a projection that includes the search snippet + if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { + loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); + } else { + loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE); + } + + if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) { + loader.setSortOrder(Phone.SORT_KEY_PRIMARY); + } else { + loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE); + } + } + + /** + * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code + * filter}. + */ + private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId, + ContactListFilter filter) { + if (filter == null || directoryId != Directory.DEFAULT) { + return; + } + + final StringBuilder selection = new StringBuilder(); + final List<String> selectionArgs = new ArrayList<String>(); + + switch (filter.filterType) { + case ContactListFilter.FILTER_TYPE_CUSTOM: { + selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); + selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); + break; + } + case ContactListFilter.FILTER_TYPE_ACCOUNT: { + filter.addAccountQueryParameterToUrl(uriBuilder); + break; + } + case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: + case ContactListFilter.FILTER_TYPE_DEFAULT: + break; // No selection needed. + case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + break; // This adapter is always "phone only", so no selection needed either. + default: + Log.w(TAG, "Unsupported filter type came " + + "(type: " + filter.filterType + ", toString: " + filter + ")" + + " showing all contacts."); + // No selection. + break; + } + loader.setSelection(selection.toString()); + loader.setSelectionArgs(selectionArgs.toArray(new String[0])); + } + + @Override + public String getContactDisplayName(int position) { + return ((Cursor) getItem(position)).getString(PhoneQuery.PHONE_DISPLAY_NAME); + } + + /** + * Builds a {@link Data#CONTENT_URI} for the given cursor position. + * + * @return Uri for the data. may be null if the cursor is not ready. + */ + public Uri getDataUri(int position) { + Cursor cursor = ((Cursor)getItem(position)); + if (cursor != null) { + long id = cursor.getLong(PhoneQuery.PHONE_ID); + return ContentUris.withAppendedId(Data.CONTENT_URI, id); + } else { + Log.w(TAG, "Cursor was null in getDataUri() call. Returning null instead."); + return null; + } + } + + @Override + protected View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent) { + final ContactListItemView view = new ContactListItemView(context, null); + view.setUnknownNameText(mUnknownNameText); + view.setQuickContactEnabled(isQuickContactEnabled()); + view.setPhotoPosition(mPhotoPosition); + return view; + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + ContactListItemView view = (ContactListItemView)itemView; + + view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); + + // Look at elements before and after this position, checking if contact IDs are same. + // If they have one same contact ID, it means they can be grouped. + // + // In one group, only the first entry will show its photo and its name, and the other + // entries in the group show just their data (e.g. phone number, email address). + cursor.moveToPosition(position); + boolean isFirstEntry = true; + boolean showBottomDivider = true; + final long currentContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID); + if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) { + final long previousContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID); + if (currentContactId == previousContactId) { + isFirstEntry = false; + } + } + cursor.moveToPosition(position); + if (cursor.moveToNext() && !cursor.isAfterLast()) { + final long nextContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID); + if (currentContactId == nextContactId) { + // The following entry should be in the same group, which means we don't want a + // divider between them. + // TODO: we want a different divider than the divider between groups. Just hiding + // this divider won't be enough. + showBottomDivider = false; + } + } + cursor.moveToPosition(position); + + bindSectionHeaderAndDivider(view, position); + if (isFirstEntry) { + bindName(view, cursor); + if (isQuickContactEnabled()) { + // No need for photo uri here, because we can not have directory results. If we + // ever do, we need to add photo uri to the query + bindQuickContact(view, partition, cursor, PhoneQuery.PHONE_PHOTO_ID, -1, + PhoneQuery.PHONE_CONTACT_ID, PhoneQuery.PHONE_LOOKUP_KEY); + } else { + bindPhoto(view, cursor); + } + } else { + unbindName(view); + + view.removePhotoView(true, false); + } + bindPhoneNumber(view, cursor); + view.setDividerVisible(showBottomDivider); + } + + protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) { + CharSequence label = null; + if (!cursor.isNull(PhoneQuery.PHONE_TYPE)) { + final int type = cursor.getInt(PhoneQuery.PHONE_TYPE); + final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL); + + // TODO cache + label = Phone.getTypeLabel(getContext().getResources(), type, customLabel); + } + view.setLabel(label); + view.showData(cursor, PhoneQuery.PHONE_NUMBER); + } + + protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) { + if (isSectionHeaderDisplayEnabled()) { + Placement placement = getItemPlacementInSection(position); + view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null); + view.setDividerVisible(!placement.lastInSection); + } else { + view.setSectionHeader(null); + view.setDividerVisible(true); + } + } + + protected void bindName(final ContactListItemView view, Cursor cursor) { + view.showDisplayName(cursor, PhoneQuery.PHONE_DISPLAY_NAME, getContactNameDisplayOrder()); + // Note: we don't show phonetic names any more (see issue 5265330) + } + + protected void unbindName(final ContactListItemView view) { + view.hideDisplayName(); + } + + protected void bindPhoto(final ContactListItemView view, Cursor cursor) { + long photoId = 0; + if (!cursor.isNull(PhoneQuery.PHONE_PHOTO_ID)) { + photoId = cursor.getLong(PhoneQuery.PHONE_PHOTO_ID); + } + + getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false); + } + + public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + } + + public ContactListItemView.PhotoPosition getPhotoPosition() { + return mPhotoPosition; + } + + public void setUseCallableUri(boolean useCallableUri) { + mUseCallableUri = useCallableUri; + } + + public boolean usesCallableUri() { + return mUseCallableUri; + } +} |