Merge "revive contacts launch performance tests & also add the test for dialer UI" into froyo
diff --git a/res/layout-finger/contacts_list_item.xml b/res/layout-finger/contacts_list_item.xml
deleted file mode 100644
index 67de04d..0000000
--- a/res/layout-finger/contacts_list_item.xml
+++ /dev/null
@@ -1,91 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
->
- <include
- android:id="@+id/header"
- layout="@layout/list_section"
- />
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
- android:paddingLeft="14dip"
- >
-
- <include
- android:id="@+id/right_side"
- layout="@layout/contacts_list_item_presence_and_action"
- />
-
- <TextView android:id="@+id/label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentBottom="true"
- android:layout_marginBottom="8dip"
- android:layout_marginTop="-8dip"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textStyle="bold"
- />
-
- <TextView android:id="@+id/data"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dip"
- android:layout_toRightOf="@id/label"
- android:layout_toLeftOf="@id/right_side"
- android:layout_alignBaseline="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- />
-
- <TextView android:id="@+id/name"
- android:layout_width="0dip"
- android:layout_height="0dip"
- android:layout_alignParentLeft="true"
- android:layout_marginBottom="1dip"
- android:layout_toLeftOf="@id/right_side"
- android:layout_alignParentTop="true"
- android:layout_above="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:gravity="center_vertical|left"
- android:textAppearance="?android:attr/textAppearanceLarge"
- />
- </RelativeLayout>
-
- <View android:id="@+id/list_divider"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@*android:drawable/divider_horizontal_dark_opaque"
- />
-</LinearLayout>
diff --git a/res/layout-finger/contacts_list_item_photo.xml b/res/layout-finger/contacts_list_item_photo.xml
deleted file mode 100644
index cad1bb9..0000000
--- a/res/layout-finger/contacts_list_item_photo.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
->
- <include
- android:id="@+id/header"
- layout="@layout/list_section"
- />
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
- android:paddingLeft="4dip"
- >
-
- <include
- android:id="@+id/right_side"
- layout="@layout/contacts_list_item_presence_and_action"
- />
-
- <android.widget.QuickContactBadge android:id="@+id/photo"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:layout_marginRight="8dip"
- style="?android:attr/quickContactBadgeStyleWindowMedium"
- />
-
- <ImageView android:id="@+id/noQuickContactPhoto"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:layout_marginRight="8dip"
- android:background="@null"
- style="?android:attr/quickContactBadgeStyleWindowMedium"
- />
-
- <TextView android:id="@+id/label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_toRightOf="@id/photo"
- android:layout_alignParentBottom="true"
- android:layout_marginBottom="8dip"
- android:layout_marginTop="-10dip"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textStyle="bold"
- />
-
- <TextView android:id="@+id/data"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dip"
- android:layout_toRightOf="@id/label"
- android:layout_toLeftOf="@id/right_side"
- android:layout_alignBaseline="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- />
-
- <TextView android:id="@+id/name"
- android:layout_width="0dip"
- android:layout_height="0dip"
- android:layout_toRightOf="@id/photo"
- android:layout_toLeftOf="@id/right_side"
- android:layout_alignParentTop="true"
- android:layout_above="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:gravity="center_vertical|left"
- android:textAppearance="?android:attr/textAppearanceLarge"
- />
- </RelativeLayout>
-
- <View android:id="@+id/list_divider"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@*android:drawable/divider_horizontal_dark_opaque"
- />
-</LinearLayout>
diff --git a/res/layout-finger/contacts_list_item_photo_and_snippet.xml b/res/layout-finger/contacts_list_item_photo_and_snippet.xml
deleted file mode 100644
index 74e8f7e..0000000
--- a/res/layout-finger/contacts_list_item_photo_and_snippet.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
->
- <include
- android:id="@+id/header"
- layout="@layout/list_section"
- />
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
- android:paddingLeft="4dip"
- >
-
- <include
- android:id="@+id/right_side"
- layout="@layout/contacts_list_item_presence_and_action"
- />
-
- <android.widget.QuickContactBadge android:id="@+id/photo"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:layout_marginRight="8dip"
- style="?android:attr/quickContactBadgeStyleWindowMedium"
- />
-
- <ImageView android:id="@+id/noQuickContactPhoto"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:layout_marginRight="8dip"
- android:background="@null"
- style="?android:attr/quickContactBadgeStyleWindowMedium"
- />
-
- <TextView android:id="@+id/label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_toRightOf="@id/photo"
- android:layout_alignParentBottom="true"
- android:layout_marginBottom="3dip"
- android:layout_marginTop="-7dip"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textStyle="bold"
- />
-
- <TextView android:id="@+id/data"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dip"
- android:layout_toRightOf="@id/label"
- android:layout_toLeftOf="@id/right_side"
- android:layout_alignBaseline="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- />
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_toRightOf="@id/photo"
- android:layout_toLeftOf="@id/right_side"
- android:layout_above="@id/label"
- android:layout_alignParentTop="true"
- android:layout_alignWithParentIfMissing="true"
- android:gravity="center_vertical|left"
- >
-
- <TextView android:id="@+id/name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceLarge"
- />
-
- <TextView android:id="@+id/snippet"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/name"
- android:layout_alignWithParentIfMissing="true"
- android:layout_marginTop="-5dip"
- android:layout_marginBottom="3dip"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textStyle="bold"
-
- />
- </RelativeLayout>
- </RelativeLayout>
-
- <View android:id="@+id/list_divider"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@*android:drawable/divider_horizontal_dark_opaque"
- />
-</LinearLayout>
diff --git a/res/layout-finger/contacts_list_item_presence_and_action.xml b/res/layout-finger/contacts_list_item_presence_and_action.xml
deleted file mode 100644
index 80b275f..0000000
--- a/res/layout-finger/contacts_list_item_presence_and_action.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:orientation="horizontal"
- android:layout_marginLeft="11dip"
- android:layout_alignParentRight="true">
-
- <ImageView android:id="@+id/presence"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dip"
- android:layout_marginRight="5dip"
- android:padding="7dip"
- android:layout_gravity="center_vertical"
- android:scaleType="centerInside"
- />
-
- <LinearLayout android:id="@+id/call_view"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:orientation="horizontal">
-
- <View android:id="@+id/divider"
- android:layout_width="1px"
- android:layout_height="match_parent"
- android:layout_marginTop="5dip"
- android:layout_marginBottom="5dip"
- android:background="@drawable/divider_vertical_dark"
- />
-
- <view
- class="com.android.contacts.ui.widget.DontPressWithParentImageView"
- android:id="@+id/call_button"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:paddingLeft="14dip"
- android:paddingRight="14dip"
- android:layout_centerVertical="true"
- android:gravity="center"
- android:src="@android:drawable/sym_action_call"
- android:background="@drawable/call_background"
- />
-
- </LinearLayout>
-</LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 25189db..4a7a743 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -27,4 +27,16 @@
<dimen name="contact_shortcut_frame_width">50dip</dimen>
<dimen name="contact_shortcut_frame_height">56dip</dimen>
+
+ <!-- Dimensions for a list item -->
+ <dimen name="list_item_padding_top">4dip</dimen>
+ <dimen name="list_item_padding_right">11dip</dimen>
+ <dimen name="list_item_padding_bottom">4dip</dimen>
+ <dimen name="list_item_padding_left">4dip</dimen>
+ <dimen name="list_item_gap_between_image_and_text">8dip</dimen>
+ <dimen name="list_item_gap_between_label_and_data">5dip</dimen>
+ <dimen name="list_item_call_button_padding">14dip</dimen>
+ <dimen name="list_item_vertical_divider_margin">5dip</dimen>
+ <dimen name="list_item_presence_icon_margin">5dip</dimen>
+ <dimen name="list_item_header_text_width">56dip</dimen>
</resources>
diff --git a/src/com/android/contacts/ContactListItemView.java b/src/com/android/contacts/ContactListItemView.java
new file mode 100644
index 0000000..5c6c149
--- /dev/null
+++ b/src/com/android/contacts/ContactListItemView.java
@@ -0,0 +1,587 @@
+/*
+ * 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;
+
+import com.android.contacts.ui.widget.DontPressWithParentImageView;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import android.widget.ImageView.ScaleType;
+
+/**
+ * A custom view for an item in the contact list.
+ */
+public class ContactListItemView extends ViewGroup {
+
+ private static final int QUICK_CONTACT_BADGE_STYLE =
+ com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
+
+ private final Context mContext;
+
+ private final int mPreferredHeight;
+ private final int mVerticalDividerMargin;
+ private final int mPaddingTop;
+ private final int mPaddingRight;
+ private final int mPaddingBottom;
+ private final int mPaddingLeft;
+ private final int mGapBetweenImageAndText;
+ private final int mGapBetweenLabelAndData;
+ private final int mCallButtonPadding;
+ private final int mPresenceIconMargin;
+ private final int mHeaderTextWidth;
+
+ private boolean mHorizontalDividerVisible;
+ private Drawable mHorizontalDividerDrawable;
+ private int mHorizontalDividerHeight;
+
+ private boolean mVerticalDividerVisible;
+ private Drawable mVerticalDividerDrawable;
+ private int mVerticalDividerWidth;
+
+ private boolean mHeaderVisible;
+ private Drawable mHeaderBackgroundDrawable;
+ private int mHeaderBackgroundHeight;
+ private TextView mHeaderTextView;
+
+ private QuickContactBadge mQuickContact;
+ private ImageView mPhotoView;
+ private TextView mNameTextView;
+ private DontPressWithParentImageView mCallButton;
+ private TextView mLabelView;
+ private TextView mDataView;
+ private TextView mSnippetView;
+ private ImageView mPresenceIcon;
+
+ private int mPhotoViewWidth;
+ private int mPhotoViewHeight;
+ private int mLine1Height;
+ private int mLine2Height;
+ private int mLine3Height;
+
+ private OnClickListener mCallButtonClickListener;
+
+ public ContactListItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+
+ // Obtain preferred item height from the current theme
+ TypedArray a = context.obtainStyledAttributes(null, com.android.internal.R.styleable.Theme);
+ mPreferredHeight =
+ a.getDimensionPixelSize(android.R.styleable.Theme_listPreferredItemHeight, 0);
+ a.recycle();
+
+ Resources resources = context.getResources();
+ mVerticalDividerMargin =
+ resources.getDimensionPixelOffset(R.dimen.list_item_vertical_divider_margin);
+ mPaddingTop =
+ resources.getDimensionPixelOffset(R.dimen.list_item_padding_top);
+ mPaddingBottom =
+ resources.getDimensionPixelOffset(R.dimen.list_item_padding_bottom);
+ mPaddingLeft =
+ resources.getDimensionPixelOffset(R.dimen.list_item_padding_left);
+ mPaddingRight =
+ resources.getDimensionPixelOffset(R.dimen.list_item_padding_right);
+ mGapBetweenImageAndText =
+ resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_image_and_text);
+ mGapBetweenLabelAndData =
+ resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_label_and_data);
+ mCallButtonPadding =
+ resources.getDimensionPixelOffset(R.dimen.list_item_call_button_padding);
+ mPresenceIconMargin =
+ resources.getDimensionPixelOffset(R.dimen.list_item_presence_icon_margin);
+ mHeaderTextWidth =
+ resources.getDimensionPixelOffset(R.dimen.list_item_header_text_width);
+ }
+
+ /**
+ * Installs a call button listener.
+ */
+ public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) {
+ mCallButtonClickListener = callButtonClickListener;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // We will match parent's width and wrap content vertically, but make sure
+ // height is no less than listPreferredItemHeight.
+ int width = resolveSize(0, widthMeasureSpec);
+ int height = 0;
+
+ mLine1Height = 0;
+ mLine2Height = 0;
+ mLine3Height = 0;
+
+ // Obtain the natural dimensions of the name text (we only care about height)
+ mNameTextView.measure(0, 0);
+
+ mLine1Height = mNameTextView.getMeasuredHeight();
+
+ if (isVisible(mLabelView)) {
+ mLabelView.measure(0, 0);
+ mLine2Height = mLabelView.getMeasuredHeight();
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.measure(0, 0);
+ mLine2Height = Math.max(mLine2Height, mDataView.getMeasuredHeight());
+ }
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.measure(0, 0);
+ mLine3Height = mSnippetView.getMeasuredHeight();
+ }
+
+ height += mLine1Height + mLine2Height + mLine3Height;
+
+ if (isVisible(mCallButton)) {
+ mCallButton.measure(0, 0);
+ }
+ if (isVisible(mPresenceIcon)) {
+ mPresenceIcon.measure(0, 0);
+ }
+
+ ensurePhotoViewSize();
+
+ height = Math.max(height, mPhotoViewHeight);
+ height = Math.max(height, mPreferredHeight);
+
+ if (mHeaderVisible) {
+ ensureHeaderBackground();
+ mHeaderTextView.measure(
+ MeasureSpec.makeMeasureSpec(mHeaderTextWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+ height += mHeaderBackgroundDrawable.getIntrinsicHeight();
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int height = bottom - top;
+ int width = right - left;
+
+ // Determine the vertical bounds by laying out the header first.
+ int topBound = 0;
+
+ if (mHeaderVisible) {
+ mHeaderBackgroundDrawable.setBounds(
+ 0,
+ 0,
+ width,
+ mHeaderBackgroundHeight);
+ mHeaderTextView.layout(0, 0, width, mHeaderBackgroundHeight);
+ topBound += mHeaderBackgroundHeight;
+ }
+
+ // Positions of views on the left are fixed and so are those on the right side.
+ // The stretchable part of the layout is in the middle. So, we will start off
+ // by laying out the left and right sides. Then we will allocate the remainder
+ // to the text fields in the middle.
+
+ // Left side
+ int leftBound = mPaddingLeft;
+ View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+ if (photoView != null) {
+ // Center the photo vertically
+ int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ leftBound,
+ photoTop,
+ leftBound + mPhotoViewWidth,
+ photoTop + mPhotoViewHeight);
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ }
+
+ // Right side
+ int rightBound = right;
+ if (isVisible(mCallButton)) {
+ int buttonWidth = mCallButton.getMeasuredWidth();
+ rightBound -= buttonWidth;
+ mCallButton.layout(
+ rightBound,
+ topBound,
+ rightBound + buttonWidth,
+ height);
+ mVerticalDividerVisible = true;
+ ensureVerticalDivider();
+ rightBound -= mVerticalDividerWidth;
+ mVerticalDividerDrawable.setBounds(
+ rightBound,
+ topBound + mVerticalDividerMargin,
+ rightBound + mVerticalDividerWidth,
+ height - mVerticalDividerMargin);
+ } else {
+ mVerticalDividerVisible = false;
+ }
+
+ if (isVisible(mPresenceIcon)) {
+ int iconWidth = mPresenceIcon.getMeasuredWidth();
+ rightBound -= mPresenceIconMargin + iconWidth;
+ mPresenceIcon.layout(
+ rightBound,
+ topBound,
+ rightBound + iconWidth,
+ height);
+ }
+
+ if (mHorizontalDividerVisible) {
+ ensureHorizontalDivider();
+ mHorizontalDividerDrawable.setBounds(
+ 0,
+ height - mHorizontalDividerHeight,
+ width,
+ height);
+ }
+
+ topBound += mPaddingTop;
+ int bottomBound = height - mPaddingBottom;
+
+ // Text lines, centered vertically
+ rightBound -= mPaddingRight;
+
+ // Center text vertically
+ int totalTextHeight = mLine1Height + mLine2Height + mLine3Height;
+ int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
+
+ mNameTextView.layout(leftBound,
+ textTopBound,
+ rightBound,
+ textTopBound + mLine1Height);
+
+ int dataLeftBound = leftBound;
+ if (isVisible(mLabelView)) {
+ dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+ mLabelView.layout(leftBound,
+ textTopBound + mLine1Height,
+ dataLeftBound,
+ textTopBound + mLine1Height + mLine2Height);
+ dataLeftBound += mGapBetweenLabelAndData;
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.layout(dataLeftBound,
+ textTopBound + mLine1Height,
+ rightBound,
+ textTopBound + mLine1Height + mLine2Height);
+ }
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.layout(leftBound,
+ textTopBound + mLine1Height + mLine2Height,
+ rightBound,
+ textTopBound + mLine1Height + mLine2Height + mLine3Height);
+ }
+ }
+
+ private boolean isVisible(View view) {
+ return view != null && view.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * Loads the drawable for the vertical divider if it has not yet been loaded.
+ */
+ private void ensureVerticalDivider() {
+ if (mVerticalDividerDrawable == null) {
+ mVerticalDividerDrawable = mContext.getResources().getDrawable(
+ R.drawable.divider_vertical_dark);
+ mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth();
+ }
+ }
+
+ /**
+ * Loads the drawable for the horizontal divider if it has not yet been loaded.
+ */
+ private void ensureHorizontalDivider() {
+ if (mHorizontalDividerDrawable == null) {
+ mHorizontalDividerDrawable = mContext.getResources().getDrawable(
+ com.android.internal.R.drawable.divider_horizontal_dark_opaque);
+ mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
+ }
+ }
+
+ /**
+ * Loads the drawable for the header background if it has not yet been loaded.
+ */
+ private void ensureHeaderBackground() {
+ if (mHeaderBackgroundDrawable == null) {
+ mHeaderBackgroundDrawable = mContext.getResources().getDrawable(
+ android.R.drawable.dark_header);
+ mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight();
+ }
+ }
+
+ /**
+ * Extracts width and height from the style
+ */
+ private void ensurePhotoViewSize() {
+ if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) {
+ TypedArray a = mContext.obtainStyledAttributes(null,
+ com.android.internal.R.styleable.ViewGroup_Layout,
+ QUICK_CONTACT_BADGE_STYLE, 0);
+ mPhotoViewWidth = a.getLayoutDimension(
+ android.R.styleable.ViewGroup_Layout_layout_width,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPhotoViewHeight = a.getLayoutDimension(
+ android.R.styleable.ViewGroup_Layout_layout_height,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ a.recycle();
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ if (mHeaderVisible) {
+ mHeaderBackgroundDrawable.draw(canvas);
+ }
+ if (mHorizontalDividerVisible) {
+ mHorizontalDividerDrawable.draw(canvas);
+ }
+ if (mVerticalDividerVisible) {
+ mVerticalDividerDrawable.draw(canvas);
+ }
+ super.dispatchDraw(canvas);
+ }
+
+ /**
+ * Sets the flag that determines whether a divider should drawn at the bottom
+ * of the view.
+ */
+ public void setDividerVisible(boolean visible) {
+ mHorizontalDividerVisible = visible;
+ }
+
+ /**
+ * Sets section header or makes it invisible if the title is null.
+ */
+ public void setSectionHeader(String title) {
+ if (!TextUtils.isEmpty(title)) {
+ if (mHeaderTextView == null) {
+ mHeaderTextView = new TextView(mContext);
+ mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
+ mHeaderTextView.setTextColor(mContext.getResources()
+ .getColor(com.android.internal.R.color.dim_foreground_dark));
+ mHeaderTextView.setTextSize(14);
+ mHeaderTextView.setGravity(Gravity.CENTER);
+ addView(mHeaderTextView);
+ }
+ mHeaderTextView.setText(title);
+ mHeaderTextView.setVisibility(View.VISIBLE);
+ mHeaderVisible = true;
+ } else {
+ if (mHeaderTextView != null) {
+ mHeaderTextView.setVisibility(View.GONE);
+ }
+ mHeaderVisible = false;
+ }
+ }
+
+ /**
+ * Returns the quick contact badge, creating it if necessary.
+ */
+ public QuickContactBadge getQuickContact() {
+ if (mQuickContact == null) {
+ mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
+ mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
+ addView(mQuickContact);
+ }
+ return mQuickContact;
+ }
+
+ /**
+ * Returns the photo view, creating it if necessary.
+ */
+ public ImageView getPhotoView() {
+ if (mPhotoView == null) {
+ mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE);
+ // Quick contact style used above will set a background - remove it
+ mPhotoView.setBackgroundDrawable(null);
+ addView(mPhotoView);
+ }
+ return mPhotoView;
+ }
+
+ /**
+ * Returns the text view for the contact name, creating it if necessary.
+ */
+ public TextView getNameTextView() {
+ if (mNameTextView == null) {
+ mNameTextView = new TextView(mContext);
+ mNameTextView.setSingleLine(true);
+ mNameTextView.setEllipsize(TruncateAt.MARQUEE);
+ mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Large);
+ mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+ addView(mNameTextView);
+ }
+ return mNameTextView;
+ }
+
+ /**
+ * Adds a call button using the supplied arguments as an id and tag.
+ */
+ public void showCallButton(int id, int tag) {
+ if (mCallButton == null) {
+ mCallButton = new DontPressWithParentImageView(mContext, null);
+ mCallButton.setId(id);
+ mCallButton.setOnClickListener(mCallButtonClickListener);
+ mCallButton.setBackgroundResource(R.drawable.call_background);
+ mCallButton.setImageResource(android.R.drawable.sym_action_call);
+ mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0);
+ mCallButton.setScaleType(ScaleType.CENTER);
+ addView(mCallButton);
+ }
+
+ mCallButton.setTag(tag);
+ mCallButton.setVisibility(View.VISIBLE);
+ }
+
+ public void hideCallButton() {
+ if (mCallButton != null) {
+ mCallButton.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Adds or updates a text view for the data label.
+ */
+ public void setLabel(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mLabelView != null) {
+ mLabelView.setVisibility(View.GONE);
+ }
+ } else {
+ getLabelView().setText(text);
+ }
+ }
+
+ /**
+ * Adds or updates a text view for the data label.
+ */
+ public void setLabel(char[] text, int size) {
+ if (text == null || size == 0) {
+ if (mLabelView != null) {
+ mLabelView.setVisibility(View.GONE);
+ }
+ } else {
+ getLabelView().setText(text, 0, size);
+ }
+ }
+
+ /**
+ * Returns the text view for the data label, creating it if necessary.
+ */
+ public TextView getLabelView() {
+ if (mLabelView == null) {
+ mLabelView = new TextView(mContext);
+ mLabelView.setSingleLine(true);
+ mLabelView.setEllipsize(TruncateAt.MARQUEE);
+ mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+ addView(mLabelView);
+ }
+ return mLabelView;
+ }
+
+ /**
+ * Adds or updates a text view for the data element.
+ */
+ public void setData(char[] text, int size) {
+ if (text == null || size == 0) {
+ if (mDataView != null) {
+ mDataView.setVisibility(View.GONE);
+ }
+ return;
+ } else {
+ getDataView().setText(text, 0, size);
+ }
+ }
+
+ /**
+ * Returns the text view for the data text, creating it if necessary.
+ */
+ public TextView getDataView() {
+ if (mDataView == null) {
+ mDataView = new TextView(mContext);
+ mDataView.setSingleLine(true);
+ mDataView.setEllipsize(TruncateAt.MARQUEE);
+ mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ addView(mDataView);
+ }
+ return mDataView;
+ }
+
+ /**
+ * Adds or updates a text view for the search snippet.
+ */
+ public void setSnippet(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mSnippetView != null) {
+ mSnippetView.setVisibility(View.GONE);
+ }
+ } else {
+ getSnippetView().setText(text);
+ }
+ }
+
+ /**
+ * Returns the text view for the search snippet, creating it if necessary.
+ */
+ public TextView getSnippetView() {
+ if (mSnippetView == null) {
+ mSnippetView = new TextView(mContext);
+ mSnippetView.setSingleLine(true);
+ mSnippetView.setEllipsize(TruncateAt.MARQUEE);
+ mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
+ addView(mSnippetView);
+ }
+ return mSnippetView;
+ }
+
+ /**
+ * Adds or updates the presence icon view.
+ */
+ public void setPresence(Drawable icon) {
+ if (icon != null) {
+ if (mPresenceIcon == null) {
+ mPresenceIcon = new ImageView(mContext);
+ addView(mPresenceIcon);
+ }
+ mPresenceIcon.setImageDrawable(icon);
+ mPresenceIcon.setScaleType(ScaleType.CENTER);
+ mPresenceIcon.setVisibility(View.VISIBLE);
+ } else {
+ if (mPresenceIcon != null) {
+ mPresenceIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index da96e2b..6fd03f7 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -112,11 +112,11 @@
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
+import android.widget.CursorAdapter;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.QuickContactBadge;
-import android.widget.ResourceCursorAdapter;
import android.widget.SectionIndexer;
import android.widget.TextView;
import android.widget.Toast;
@@ -511,10 +511,10 @@
protected void invalidate() {
int childCount = mListView.getChildCount();
for (int i = 0; i < childCount; i++) {
- View listItem = mListView.getChildAt(i);
- Object tag = listItem.getTag();
- if (tag instanceof ContactListItemCache) {
- ((ContactListItemCache)tag).nameView.invalidate();
+ View itemView = mListView.getChildAt(i);
+ if (itemView instanceof ContactListItemView) {
+ final ContactListItemView view = (ContactListItemView)itemView;
+ view.getNameTextView().invalidate();
}
}
}
@@ -929,7 +929,8 @@
public void onClick(View v) {
int id = v.getId();
switch (id) {
- case R.id.call_button: {
+ // TODO a better way of identifying the button
+ case android.R.id.button1: {
final int position = (Integer)v.getTag();
Cursor c = mAdapter.getCursor();
if (c != null) {
@@ -2748,20 +2749,8 @@
}
final static class ContactListItemCache {
- public View header;
- public TextView headerText;
- public View divider;
- public TextView nameView;
- public View callView;
- public ImageView callButton;
public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
- public TextView labelView;
- public TextView dataView;
- public TextView snippetView;
public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
- public ImageView presenceView;
- public QuickContactBadge photoView;
- public ImageView nonQuickContactPhotoView;
public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
public TextWithHighlighting textWithHighlighting;
public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128);
@@ -2773,7 +2762,7 @@
public Drawable background;
}
- private final class ContactItemListAdapter extends ResourceCursorAdapter
+ private final class ContactItemListAdapter extends CursorAdapter
implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
private SectionIndexer mIndexer;
private boolean mLoading = true;
@@ -2787,7 +2776,7 @@
private int mSuggestionsCursorCount;
public ContactItemListAdapter(Context context) {
- super(context, R.layout.contacts_list_item, null, false);
+ super(context, null, false);
mUnknownNameText = context.getText(android.R.string.unknownName);
switch (mMode) {
@@ -2823,11 +2812,6 @@
if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
mDisplayPhotos = true;
- if (mShowSearchSnippets) {
- setViewResource(R.layout.contacts_list_item_photo_and_snippet);
- } else {
- setViewResource(R.layout.contacts_list_item_photo);
- }
}
}
@@ -2960,10 +2944,13 @@
throw new IllegalStateException("couldn't move cursor to position " + position);
}
+ boolean newView;
View v;
if (convertView == null || convertView.getTag() == null) {
+ newView = true;
v = newView(mContext, cursor, parent);
} else {
+ newView = false;
v = convertView;
}
bindView(v, mContext, cursor);
@@ -2971,7 +2958,6 @@
return v;
}
-
private View getTotalContactCountView(ViewGroup parent) {
final LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(R.layout.total_contacts, parent, false);
@@ -3023,39 +3009,17 @@
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- final View view = super.newView(context, cursor, parent);
-
- final ContactListItemCache cache = new ContactListItemCache();
- cache.header = view.findViewById(R.id.header);
- cache.headerText = (TextView)view.findViewById(R.id.header_text);
- cache.divider = view.findViewById(R.id.list_divider);
- cache.nameView = (TextView) view.findViewById(R.id.name);
- cache.callView = view.findViewById(R.id.call_view);
- cache.callButton = (ImageView) view.findViewById(R.id.call_button);
- if (cache.callButton != null) {
- cache.callButton.setOnClickListener(ContactsListActivity.this);
- }
- cache.labelView = (TextView) view.findViewById(R.id.label);
- cache.dataView = (TextView) view.findViewById(R.id.data);
- cache.presenceView = (ImageView) view.findViewById(R.id.presence);
- cache.photoView = (QuickContactBadge) view.findViewById(R.id.photo);
- if (cache.photoView != null) {
- cache.photoView.setExcludeMimes(new String[] {Contacts.CONTENT_ITEM_TYPE});
- }
- cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto);
- cache.textWithHighlighting = mHighlightingAnimation.createTextWithHighlighting();
- cache.snippetView = (TextView)view.findViewById(R.id.snippet);
-
- view.setTag(cache);
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setOnCallButtonClickListener(ContactsListActivity.this);
+ view.setTag(new ContactListItemCache());
return view;
}
@Override
- public void bindView(View view, Context context, Cursor cursor) {
+ public void bindView(View itemView, Context context, Cursor cursor) {
+ final ContactListItemView view = (ContactListItemView)itemView;
final ContactListItemCache cache = (ContactListItemCache) view.getTag();
- TextView dataView = cache.dataView;
- TextView labelView = cache.labelView;
int typeColumnIndex;
int dataColumnIndex;
int labelColumnIndex;
@@ -3100,16 +3064,21 @@
// Set the name
cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
+ TextView nameView = view.getNameTextView();
int size = cache.nameBuffer.sizeCopied;
if (size != 0) {
if (highlightingEnabled) {
- buildDisplayNameWithHighlighting(cache.nameView, cursor, cache.nameBuffer,
+ if (cache.textWithHighlighting == null) {
+ cache.textWithHighlighting =
+ mHighlightingAnimation.createTextWithHighlighting();
+ }
+ buildDisplayNameWithHighlighting(nameView, cursor, cache.nameBuffer,
cache.highlightedTextBuffer, cache.textWithHighlighting);
} else {
- cache.nameView.setText(cache.nameBuffer.data, 0, size);
+ nameView.setText(cache.nameBuffer.data, 0, size);
}
} else {
- cache.nameView.setText(mUnknownNameText);
+ nameView.setText(mUnknownNameText);
}
boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX
@@ -3118,10 +3087,9 @@
// Make the call button visible if requested.
if (mDisplayCallButton && hasPhone) {
int pos = cursor.getPosition();
- cache.callView.setVisibility(View.VISIBLE);
- cache.callButton.setTag(pos);
+ view.showCallButton(android.R.id.button1, pos);
} else {
- cache.callView.setVisibility(View.GONE);
+ view.hideCallButton();
}
// Set the photo, if requested
@@ -3135,25 +3103,20 @@
ImageView viewToUse;
if (useQuickContact) {
- viewToUse = cache.photoView;
// Build soft lookup reference
final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
- cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
- cache.photoView.setVisibility(View.VISIBLE);
- cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE);
+ QuickContactBadge quickContact = view.getQuickContact();
+ quickContact.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
+ viewToUse = quickContact;
} else {
- viewToUse = cache.nonQuickContactPhotoView;
- cache.photoView.setVisibility(View.INVISIBLE);
- cache.nonQuickContactPhotoView.setVisibility(View.VISIBLE);
+ viewToUse = view.getPhotoView();
}
-
final int position = cursor.getPosition();
mPhotoLoader.loadPhoto(viewToUse, photoId);
}
- ImageView presenceView = cache.presenceView;
if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
// Set the proper icon (star or presence or nothing)
int serverStatus;
@@ -3161,27 +3124,24 @@
serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
Drawable icon = ContactPresenceIconUtil.getPresenceIcon(mContext, serverStatus);
if (icon != null) {
- presenceView.setImageDrawable(icon);
- presenceView.setVisibility(View.VISIBLE);
+ view.setPresence(icon);
} else {
- presenceView.setVisibility(View.GONE);
+ view.setPresence(null);
}
} else {
- presenceView.setVisibility(View.GONE);
+ view.setPresence(null);
}
} else {
- presenceView.setVisibility(View.GONE);
+ view.setPresence(null);
}
- // TODO: make sure that when mShowSearchSnippets is true, the
- // snippet views are available
- if (mShowSearchSnippets && cache.snippetView != null) {
+ if (mShowSearchSnippets) {
boolean showSnippet = false;
String snippetMimeType = cursor.getString(SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX);
if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
String email = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
if (!TextUtils.isEmpty(email)) {
- cache.snippetView.setText(email);
+ view.setSnippet(email);
showSnippet = true;
}
} else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
@@ -3189,42 +3149,41 @@
String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
if (!TextUtils.isEmpty(company)) {
if (!TextUtils.isEmpty(title)) {
- cache.snippetView.setText(company + " / " + title);
+ view.setSnippet(company + " / " + title);
} else {
- cache.snippetView.setText(company);
+ view.setSnippet(company);
}
showSnippet = true;
} else if (!TextUtils.isEmpty(title)) {
- cache.snippetView.setText(title);
+ view.setSnippet(title);
showSnippet = true;
}
} else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
String nickname = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
if (!TextUtils.isEmpty(nickname)) {
- cache.snippetView.setText(nickname);
+ view.setSnippet(nickname);
showSnippet = true;
}
}
- cache.snippetView.setVisibility(showSnippet ? View.VISIBLE : View.GONE);
+ if (!showSnippet) {
+ view.setSnippet(null);
+ }
}
if (!displayAdditionalData) {
- cache.dataView.setVisibility(View.GONE);
-
if (phoneticNameColumnIndex != -1) {
// Set the name
cursor.copyStringToBuffer(phoneticNameColumnIndex, cache.phoneticNameBuffer);
int phoneticNameSize = cache.phoneticNameBuffer.sizeCopied;
if (phoneticNameSize != 0) {
- cache.labelView.setText(cache.phoneticNameBuffer.data, 0, phoneticNameSize);
- cache.labelView.setVisibility(View.VISIBLE);
+ view.setLabel(cache.phoneticNameBuffer.data, phoneticNameSize);
} else {
- cache.labelView.setVisibility(View.GONE);
+ view.setLabel(null);
}
} else {
- cache.labelView.setVisibility(View.GONE);
+ view.setLabel(null);
}
return;
}
@@ -3233,29 +3192,23 @@
cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
size = cache.dataBuffer.sizeCopied;
- if (size != 0) {
- dataView.setText(cache.dataBuffer.data, 0, size);
- dataView.setVisibility(View.VISIBLE);
- } else {
- dataView.setVisibility(View.GONE);
- }
+ view.setData(cache.dataBuffer.data, size);
// Set the label.
if (!cursor.isNull(typeColumnIndex)) {
- labelView.setVisibility(View.VISIBLE);
-
final int type = cursor.getInt(typeColumnIndex);
final String label = cursor.getString(labelColumnIndex);
if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
- labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type,
+ // TODO cache
+ view.setLabel(StructuredPostal.getTypeLabel(context.getResources(), type,
label));
} else {
- labelView.setText(Phone.getTypeLabel(context.getResources(), type, label));
+ // TODO cache
+ view.setLabel(Phone.getTypeLabel(context.getResources(), type, label));
}
} else {
- // There is no label, hide the the view
- labelView.setVisibility(View.GONE);
+ view.setLabel(null);
}
}
@@ -3278,30 +3231,27 @@
textView.setText(textWithHighlighting);
}
- private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
+ private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
+ final ContactListItemView view = (ContactListItemView)itemView;
final ContactListItemCache cache = (ContactListItemCache) view.getTag();
if (!displaySectionHeaders) {
- cache.header.setVisibility(View.GONE);
- cache.divider.setVisibility(View.VISIBLE);
+ view.setSectionHeader(null);
+ view.setDividerVisible(true);
} else {
final int section = getSectionForPosition(position);
if (getPositionForSection(section) == position) {
String title = (String)mIndexer.getSections()[section];
- if (!TextUtils.isEmpty(title)) {
- cache.headerText.setText(title);
- cache.header.setVisibility(View.VISIBLE);
- } else {
- cache.header.setVisibility(View.GONE);
- }
+ view.setSectionHeader(title);
} else {
- cache.header.setVisibility(View.GONE);
+ view.setDividerVisible(false);
+ view.setSectionHeader(null);
}
// move the divider for the last item in a section
if (getPositionForSection(section + 1) - 1 == position) {
- cache.divider.setVisibility(View.GONE);
+ view.setDividerVisible(false);
} else {
- cache.divider.setVisibility(View.VISIBLE);
+ view.setDividerVisible(true);
}
}
}
@@ -3337,8 +3287,6 @@
foundContactsText.setText(text);
}
- mPhotoLoader.clear();
-
super.changeCursor(cursor);
// Update the indexer for the fast scroll widget
updateIndexer(cursor);
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index a8b3f1e..4dcf5d5 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -31,6 +31,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
+import android.os.FileUtils;
import android.os.Handler;
import android.os.PowerManager;
import android.pim.vcard.VCardConfig;
@@ -53,9 +54,12 @@
import android.text.style.RelativeSizeSpan;
import android.util.Log;
+import java.io.BufferedOutputStream;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -153,6 +157,7 @@
private boolean mCanceled;
private PowerManager.WakeLock mWakeLock;
private Uri mUri;
+ private File mTempFile;
private List<VCardFile> mSelectedVCardFileList;
private List<String> mErrorFileNameList;
@@ -190,11 +195,18 @@
boolean shouldCallFinish = true;
mWakeLock.acquire();
Uri createdUri = null;
+ mTempFile = null;
// Some malicious vCard data may make this thread broken
// (e.g. OutOfMemoryError).
// Even in such cases, some should be done.
try {
if (mUri != null) { // Read one vCard expressed by mUri
+ final Uri uri = getReopenableUri(mUri);
+ if (uri == null) {
+ shouldCallFinish = false;
+ return;
+ }
+
mProgressDialogForReadVCard.setProgressNumberFormat("");
mProgressDialogForReadVCard.setProgress(0);
@@ -208,16 +220,15 @@
VCardSourceDetector detector = new VCardSourceDetector();
VCardInterpreterCollection builderCollection = new VCardInterpreterCollection(
Arrays.asList(counter, detector));
-
boolean result;
try {
- result = readOneVCardFile(mUri,
+ result = readOneVCardFile(uri,
VCardConfig.DEFAULT_CHARSET, builderCollection, null, true, null);
} catch (VCardNestedException e) {
try {
// Assume that VCardSourceDetector was able to detect the source.
// Try again with the detector.
- result = readOneVCardFile(mUri,
+ result = readOneVCardFile(uri,
VCardConfig.DEFAULT_CHARSET, counter, detector, false, null);
} catch (VCardNestedException e2) {
result = false;
@@ -239,7 +250,7 @@
mProgressDialogForReadVCard.setIndeterminate(false);
mProgressDialogForReadVCard.setMax(counter.getCount());
String charset = detector.getEstimatedCharset();
- createdUri = doActuallyReadOneVCard(mUri, null, charset, true, detector,
+ createdUri = doActuallyReadOneVCard(uri, null, charset, true, detector,
mErrorFileNameList);
} else { // Read multiple files.
mProgressDialogForReadVCard.setProgressNumberFormat(
@@ -251,7 +262,13 @@
if (mCanceled) {
return;
}
- final Uri uri = Uri.parse("file://" + vcardFile.getCanonicalPath());
+ // TODO: detect scheme!
+ final Uri uri = getReopenableUri(
+ Uri.parse("file://" + vcardFile.getCanonicalPath()));
+ if (uri == null) {
+ shouldCallFinish = false;
+ return;
+ }
VCardSourceDetector detector = new VCardSourceDetector();
try {
@@ -271,6 +288,12 @@
} finally {
mWakeLock.release();
mProgressDialogForReadVCard.dismiss();
+ if (mTempFile != null) {
+ if (!mTempFile.delete()) {
+ Log.w(LOG_TAG, "Failed to delete a cache file.");
+ }
+ mTempFile = null;
+ }
// finish() is called via mCancelListener, which is used in DialogDisplayer.
if (shouldCallFinish && !isFinishing()) {
if (mErrorFileNameList == null || mErrorFileNameList.isEmpty()) {
@@ -310,6 +333,49 @@
}
}
+ private Uri getReopenableUri(final Uri uri) {
+ if ("file".equals(uri.getScheme())) {
+ return uri;
+ } else {
+ // We may not be able to scan a given uri more than once when it does not
+ // point to a local file, while it is necessary to scan it more than once
+ // because of vCard limitation. We rely on a local temporary file instead.
+ //
+ // e.g. Email app's AttachmentProvider is able to give us a content Uri
+ // with an attachment file, but we cannot "sometimes" (not always) scan
+ // the Uri more than once because of permission revocation.
+ InputStream is = null;
+ OutputStream os = null;
+ Uri reopenableUri = null;
+ try {
+ is = mResolver.openInputStream(uri);
+ File dir = getDir("tmp", MODE_PRIVATE);
+ mTempFile = dir.createTempFile("vcf", null, dir);
+ FileUtils.copyToFile(is, mTempFile);
+ reopenableUri = Uri.parse("file://" + mTempFile.getCanonicalPath());
+ } catch (IOException e) {
+ mHandler.post(new DialogDisplayer(
+ getString(R.string.fail_reason_io_error) +
+ ": " + e.getLocalizedMessage()));
+ return null;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ return reopenableUri;
+ }
+ }
+
private Uri doActuallyReadOneVCard(Uri uri, Account account,
String charset, boolean showEntryParseProgress,
VCardSourceDetector detector, List<String> errorFileNameList) {
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ec365dc..ead6a4a 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -15,7 +15,6 @@
*/
package com.android.contacts;
-
import com.android.contacts.Collapser.Collapsible;
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.Sources;
@@ -92,6 +91,7 @@
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
/**
* Displays the details of a specific contact.
@@ -333,16 +333,13 @@
(new AsyncTask<Void, Void, ArrayList<Entity>>() {
@Override
protected ArrayList<Entity> doInBackground(Void... params) {
- ArrayList<Entity> newEntities = Lists.newArrayList();
+ ArrayList<Entity> newEntities = new ArrayList<Entity>(cursor.getCount());
EntityIterator iterator = RawContacts.newEntityIterator(cursor);
try {
while (iterator.hasNext()) {
Entity entity = iterator.next();
newEntities.add(entity);
}
- } catch (RemoteException e) {
- Log.w(TAG, "Problem reading contact data: " + e.toString());
- return null;
} finally {
iterator.close();
}
@@ -394,19 +391,65 @@
mHasStatuses = true;
}
+ private static Cursor setupContactCursor(ContentResolver resolver, Uri lookupUri) {
+ if (lookupUri == null) {
+ return null;
+ }
+ final List<String> segments = lookupUri.getPathSegments();
+ if (segments.size() != 4) {
+ return null;
+ }
+
+ // Contains an Id.
+ final long uriContactId = Long.parseLong(segments.get(3));
+ final String uriLookupKey = Uri.encode(segments.get(2));
+ final Uri dataUri = Uri.withAppendedPath(
+ ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId),
+ Contacts.Data.CONTENT_DIRECTORY);
+
+ // This cursor has several purposes:
+ // - Fetch NAME_RAW_CONTACT_ID and DISPLAY_NAME_SOURCE
+ // - Fetch the lookup-key to ensure we are looking at the right record
+ // - Watcher for change events
+ Cursor cursor = resolver.query(dataUri,
+ new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY
+ }, null, null, null);
+
+ if (cursor.moveToFirst()) {
+ String lookupKey =
+ cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+ if (!lookupKey.equals(uriLookupKey)) {
+ // ID and lookup key do not match
+ cursor.close();
+ return null;
+ }
+ return cursor;
+ } else {
+ cursor.close();
+ return null;
+ }
+ }
+
private synchronized void startEntityQuery() {
closeCursor();
- Uri uri = null;
- if (mLookupUri != null) {
+ // Interprete mLookupUri
+ mCursor = setupContactCursor(mResolver, mLookupUri);
+
+ // If mCursor is null now we did not succeed in using the Uri's Id (or it didn't contain
+ // a Uri). Instead we now have to use the lookup key to find the record
+ if (mCursor == null) {
mLookupUri = Contacts.getLookupUri(getContentResolver(), mLookupUri);
- if (mLookupUri != null) {
- uri = Contacts.lookupContact(getContentResolver(), mLookupUri);
- }
+ mCursor = setupContactCursor(mResolver, mLookupUri);
}
- if (uri == null) {
-
+ // If mCursor is still null, we were unsuccessful in finding the record
+ if (mCursor == null) {
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
// TODO either figure out a way to prevent a flash of black background or
// use some other UI than a toast
Toast.makeText(this, R.string.invalidContactMessage, Toast.LENGTH_SHORT).show();
@@ -415,34 +458,26 @@
return;
}
- final Uri dataUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+ final long contactId = ContentUris.parseId(mLookupUri);
- // This cursor has two purposes:
- // - Fetch NAME_RAW_CONTACT_ID and DISPLAY_NAME_SOURCE
- // - Watcher for change events
- mCursor = mResolver.query(dataUri,
- new String[] { Contacts.NAME_RAW_CONTACT_ID, Contacts.DISPLAY_NAME_SOURCE },
- null, null, null);
-
- if (mCursor.moveToFirst()) {
- mNameRawContactId =
+ mNameRawContactId =
mCursor.getLong(mCursor.getColumnIndex(Contacts.NAME_RAW_CONTACT_ID));
- mDisplayNameSource =
+ mDisplayNameSource =
mCursor.getInt(mCursor.getColumnIndex(Contacts.DISPLAY_NAME_SOURCE));
- } else {
- mNameRawContactId = -1;
- mDisplayNameSource = DisplayNameSources.UNDEFINED;
- }
mCursor.registerContentObserver(mObserver);
- final long contactId = ContentUris.parseId(uri);
// Clear flags and start queries to data and status
mHasEntities = false;
mHasStatuses = false;
mHandler.startQuery(TOKEN_ENTITIES, null, RawContactsEntity.CONTENT_URI, null,
- RawContacts.CONTACT_ID + "=" + contactId, null, null);
+ RawContacts.CONTACT_ID + "=?", new String[] {
+ String.valueOf(contactId)
+ }, null);
+ final Uri dataUri = Uri.withAppendedPath(
+ ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+ Contacts.Data.CONTENT_DIRECTORY);
mHandler.startQuery(TOKEN_STATUSES, null, dataUri, StatusQuery.PROJECTION,
StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS
+ " IS NOT NULL", null, null);
diff --git a/src/com/android/contacts/model/EntitySet.java b/src/com/android/contacts/model/EntitySet.java
index 7502d0f..83fe338 100644
--- a/src/com/android/contacts/model/EntitySet.java
+++ b/src/com/android/contacts/model/EntitySet.java
@@ -23,7 +23,6 @@
import android.content.ContentProviderOperation.Builder;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.RemoteException;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
@@ -76,8 +75,6 @@
state.add(entity);
}
return state;
- } catch (RemoteException e) {
- throw new IllegalStateException("Problem querying contact details", e);
} finally {
iterator.close();
}
diff --git a/src/com/android/contacts/ui/ContactsPreferencesActivity.java b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
index b6a20c7..0432aaf 100644
--- a/src/com/android/contacts/ui/ContactsPreferencesActivity.java
+++ b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
@@ -574,8 +574,6 @@
// Create single entry handling ungrouped status
mUngrouped = GroupDelta.fromSettings(resolver, accountName, accountType, hasGroups);
addGroup(mUngrouped);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem reading groups: " + e.toString());
} finally {
iterator.close();
}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 25132fb..c70cff6 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -67,7 +67,6 @@
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Contacts.Data;
import android.util.Log;
import android.view.ContextThemeWrapper;
@@ -111,6 +110,9 @@
private static final String KEY_EDIT_STATE = "state";
private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
+ private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+ private static final String KEY_QUERY_SELECTION = "queryselection";
+ private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
/** The result code when view activity should close after edit returns */
public static final int RESULT_CLOSE_VIEW_ACTIVITY = 777;
@@ -265,6 +267,11 @@
outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
+ if (mCurrentPhotoFile != null) {
+ outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString());
+ }
+ outState.putString(KEY_QUERY_SELECTION, mQuerySelection);
+ outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
super.onSaveInstanceState(outState);
}
@@ -275,6 +282,13 @@
mRawContactIdRequestingPhoto = savedInstanceState.getLong(
KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
mViewIdGenerator = savedInstanceState.getParcelable(KEY_VIEW_ID_GENERATOR);
+ String fileName = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+ if (fileName != null) {
+ mCurrentPhotoFile = new File(fileName);
+ }
+ mQuerySelection = savedInstanceState.getString(KEY_QUERY_SELECTION);
+ mContactIdForJoin = savedInstanceState.getLong(KEY_CONTACT_ID_FOR_JOIN);
+
bindEditors();
super.onRestoreInstanceState(savedInstanceState);
diff --git a/tests/src/com/android/contacts/EntityDeltaTests.java b/tests/src/com/android/contacts/EntityDeltaTests.java
index 70a506b..fa716c7 100644
--- a/tests/src/com/android/contacts/EntityDeltaTests.java
+++ b/tests/src/com/android/contacts/EntityDeltaTests.java
@@ -367,7 +367,7 @@
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
source.buildAssert(diff);
source.buildDiff(diff);
- assertEquals("Unexpected operations", 1, diff.size());
+ assertEquals("Unexpected operations", 2, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
@@ -395,7 +395,7 @@
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
source.buildAssert(diff);
source.buildDiff(diff);
- assertEquals("Unexpected operations", 2, diff.size());
+ assertEquals("Unexpected operations", 3, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index 4bc4622..18877a3 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -512,7 +512,7 @@
// Build diff, expecting single insert
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
state.buildDiff(diff);
- assertEquals("Unexpected operations", 1, diff.size());
+ assertEquals("Unexpected operations", 2, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
@@ -540,7 +540,7 @@
// Build diff, expecting two insert operations
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
state.buildDiff(diff);
- assertEquals("Unexpected operations", 2, diff.size());
+ assertEquals("Unexpected operations", 3, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
diff --git a/tests/src/com/android/contacts/EntitySetTests.java b/tests/src/com/android/contacts/EntitySetTests.java
index c9fc3fa..edfca6d 100644
--- a/tests/src/com/android/contacts/EntitySetTests.java
+++ b/tests/src/com/android/contacts/EntitySetTests.java
@@ -486,6 +486,7 @@
buildAssertVersion(VER_FIRST),
buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
buildOper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+ buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
buildUpdateAggregationKeepTogether(CONTACT_BOB));
// Merge in the second version, verify that our insert remains
@@ -495,6 +496,7 @@
buildAssertVersion(VER_SECOND),
buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
buildOper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+ buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
buildUpdateAggregationKeepTogether(CONTACT_BOB));
}
@@ -545,6 +547,7 @@
buildAssertVersion(VER_SECOND),
buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, contactInsert),
buildOper(Data.CONTENT_URI, TYPE_INSERT, phoneInsert),
+ buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
buildUpdateAggregationKeepTogether(CONTACT_BOB));
}