diff options
18 files changed, 1090 insertions, 32 deletions
diff --git a/Android.mk b/Android.mk index 1c9dcc475..c6bea9d49 100644 --- a/Android.mk +++ b/Android.mk @@ -9,7 +9,9 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ com.android.phone.common \ com.android.vcard \ android-common \ - guava + guava \ + android-support-v13 \ + android-support-v4 LOCAL_PACKAGE_NAME := Contacts LOCAL_CERTIFICATE := shared diff --git a/res/layout/carousel_about_tab.xml b/res/layout/carousel_about_tab.xml new file mode 100644 index 000000000..f1ed4f1b3 --- /dev/null +++ b/res/layout/carousel_about_tab.xml @@ -0,0 +1,66 @@ +<?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. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/detail_tab_carousel_tab_width" + android:layout_height="@dimen/detail_tab_carousel_height" + android:background="@color/detail_tab_background"> + + <ImageView android:id="@+id/photo" + android:scaleType="centerCrop" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true"/> + + <!-- Transparent view to overlay on the contact's photo + (to allow white text to appear over a white photo). --> + <View + android:layout_width="match_parent" + android:layout_height="@dimen/detail_tab_carousel_tab_label_height" + android:layout_alignParentLeft="true" + android:layout_alignParentBottom="true" + android:background="@android:color/black" + android:alpha=".25"/> + + <TextView + android:id="@+id/label" + android:layout_width="match_parent" + android:layout_height="@dimen/detail_tab_carousel_tab_label_height" + android:layout_alignParentLeft="true" + android:layout_alignParentBottom="true" + android:paddingLeft="@dimen/detail_item_side_margin" + android:singleLine="true" + android:gravity="left|center_vertical" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="@color/detail_header_view_text_color" + style="@android:style/Widget.Holo.ActionBar.TabView" /> + + <CheckBox + android:id="@+id/star" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dip" + android:layout_marginRight="10dip" + android:layout_alignParentTop="true" + android:layout_alignParentRight="true" + android:layout_gravity="center_vertical" + android:contentDescription="@string/description_star" + android:visibility="invisible" + style="?android:attr/starStyle"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/carousel_updates_tab.xml b/res/layout/carousel_updates_tab.xml new file mode 100644 index 000000000..9deb2f7ce --- /dev/null +++ b/res/layout/carousel_updates_tab.xml @@ -0,0 +1,66 @@ +<?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. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/detail_tab_carousel_tab_width" + android:layout_height="@dimen/detail_tab_carousel_height" + android:background="@color/detail_tab_background"> + + <!-- Transparent view to overlay on the contact's photo + (to allow white text to appear over a white photo). --> + <View + android:layout_width="match_parent" + android:layout_height="@dimen/detail_tab_carousel_tab_label_height" + android:layout_alignParentLeft="true" + android:layout_alignParentBottom="true" + android:background="@android:color/black" + android:alpha=".25"/> + + <TextView + android:id="@+id/label" + android:layout_width="match_parent" + android:layout_height="@dimen/detail_tab_carousel_tab_label_height" + android:layout_alignParentLeft="true" + android:layout_alignParentBottom="true" + android:paddingLeft="@dimen/detail_item_side_margin" + android:singleLine="true" + android:gravity="left|center_vertical" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="@color/detail_header_view_text_color" + style="@android:style/Widget.Holo.ActionBar.TabView" /> + + <TextView android:id="@+id/status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_marginTop="@dimen/detail_update_tab_vertical_margin" + android:paddingLeft="@dimen/detail_update_tab_side_padding" + android:paddingRight="@dimen/detail_update_tab_side_padding" + android:textAppearance="?android:attr/textAppearanceSmall" + android:maxLines="3"/> + + <TextView android:id="@+id/status_date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/status" + android:layout_marginBottom="@dimen/detail_update_tab_vertical_margin" + android:paddingLeft="@dimen/detail_update_tab_side_padding" + android:paddingRight="@dimen/detail_update_tab_side_padding" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorTertiary"/> +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/contact_detail_activity.xml b/res/layout/contact_detail_activity.xml index 9408f89bc..744f3436c 100644 --- a/res/layout/contact_detail_activity.xml +++ b/res/layout/contact_detail_activity.xml @@ -14,12 +14,19 @@ limitations under the License. --> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/contact_detail_view" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:orientation="vertical"> - <fragment class="com.android.contacts.detail.ContactDetailFragment" - android:id="@+id/contact_detail_fragment" - android:layout_width="match_parent" - android:layout_height="match_parent" /> -</FrameLayout> + <com.android.contacts.detail.ContactDetailTabCarousel + android:id="@+id/tab_carousel" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <android.support.v4.view.ViewPager + android:id="@+id/pager" + android:layout_width="match_parent" + android:layout_height="match_parent" /> +</LinearLayout> diff --git a/res/layout/contact_detail_tab_carousel.xml b/res/layout/contact_detail_tab_carousel.xml new file mode 100644 index 000000000..a7321ee91 --- /dev/null +++ b/res/layout/contact_detail_tab_carousel.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<HorizontalScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scrollbars="none"> + + <LinearLayout + android:id="@+id/tab_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <include + android:id="@+id/tab_about" + layout="@layout/carousel_about_tab" /> + + <include + android:id="@+id/tab_update" + layout="@layout/carousel_updates_tab" /> + + </LinearLayout> + +</HorizontalScrollView>
\ No newline at end of file diff --git a/res/layout/contact_detail_updates_fragment.xml b/res/layout/contact_detail_updates_fragment.xml new file mode 100644 index 000000000..7baba42a5 --- /dev/null +++ b/res/layout/contact_detail_updates_fragment.xml @@ -0,0 +1,33 @@ +<?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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/contact_detail" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView android:id="@+id/emptyText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/no_contact_details" + android:textSize="20sp" + android:textColor="?android:attr/textColorSecondary" + android:paddingLeft="10dip" + android:paddingRight="10dip" + android:paddingTop="10dip"/> +</LinearLayout> + diff --git a/res/layout/simple_contact_detail_header_view_list_item.xml b/res/layout/simple_contact_detail_header_view_list_item.xml new file mode 100644 index 000000000..117aef1a8 --- /dev/null +++ b/res/layout/simple_contact_detail_header_view_list_item.xml @@ -0,0 +1,43 @@ +<?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. +--> + +<!-- + This view temporarily holds the extra information that used to be in the + original contact detail header view, but now must move into the list because + of the new tab carousel. TODO: Integrate this better into the list as provided + by the mocks. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/phonetic_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="10dip" + android:textAppearance="?android:attr/textAppearanceSmall" /> + + <TextView + android:id="@+id/attribution" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="10dip" + android:textAppearance="?android:attr/textAppearanceSmall" /> + +</LinearLayout>
\ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index 9a6081504..60873b17d 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -49,6 +49,9 @@ <!-- Color of the text indicating the type of entry (e.g. Home, Work etc) --> <color name="detail_header_view_text_color">#FFFFFF</color> + <!-- Color of the background of the tabs on the contact detail page --> + <color name="detail_tab_background">#DBDBDB</color> + <!-- Color of the text foreground and background of Regular Sized ContactTile --> <color name="contact_tile_regular_text">#2B1B17</color> <color name="contact_tile_regular_text_background">#FFFFFF</color> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 68655d833..f36975271 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -70,6 +70,21 @@ <!-- Font size for the entries in a spinner in the contact editor. --> <dimen name="editor_field_spinner_text_size">10sp</dimen> + <!-- Height of the tab carousel on the contact detail page --> + <dimen name="detail_tab_carousel_height">150dip</dimen> + + <!-- Width of a tab in the tab carousel on the contact detail page --> + <dimen name="detail_tab_carousel_tab_width">240dip</dimen> + + <!-- Height of the tab text label in the tab carousel on the contact detail page --> + <dimen name="detail_tab_carousel_tab_label_height">40dip</dimen> + + <!-- Vertical margin of the text within the update tab in the tab carousel --> + <dimen name="detail_update_tab_vertical_margin">20dip</dimen> + + <!-- Left and right padding of the text within the update tab in the tab carousel --> + <dimen name="detail_update_tab_side_padding">10dip</dimen> + <!-- Left and right padding for a contact detail item --> <dimen name="detail_item_icon_margin">10dip</dimen> diff --git a/res/values/strings.xml b/res/values/strings.xml index f259ea702..c9b278761 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -78,6 +78,12 @@ creating a new contact. This string represents the built in way to create the contact. --> <string name="insertContactDescription">Create contact</string> + <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=11] --> + <string name="contactDetailAbout">About</string> + + <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=11] --> + <string name="contactDetailUpdates">Updates</string> + <!-- Hint text in the search box when the user hits the Search key while in the contacts app --> <string name="searchHint">Search contacts</string> diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java index 5992042a2..0dbc8dfe9 100644 --- a/src/com/android/contacts/activities/ContactDetailActivity.java +++ b/src/com/android/contacts/activities/ContactDetailActivity.java @@ -16,22 +16,36 @@ package com.android.contacts.activities; +import com.android.contacts.ContactLoader; import com.android.contacts.ContactSaveService; import com.android.contacts.ContactsActivity; import com.android.contacts.ContactsSearchManager; import com.android.contacts.R; +import com.android.contacts.detail.ContactDetailAboutFragment; import com.android.contacts.detail.ContactDetailFragment; +import com.android.contacts.detail.ContactDetailHeaderView; +import com.android.contacts.detail.ContactDetailTabCarousel; +import com.android.contacts.detail.ContactDetailUpdatesFragment; import com.android.contacts.interactions.ContactDeletionInteraction; +import com.android.contacts.list.ContactBrowseListFragment; import com.android.contacts.util.PhoneCapabilityTester; import android.accounts.Account; +import android.app.Fragment; +import android.app.FragmentManager; import android.content.ActivityNotFoundException; import android.content.ContentValues; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; import android.util.Log; import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; import android.widget.Toast; import java.util.ArrayList; @@ -39,7 +53,15 @@ import java.util.ArrayList; public class ContactDetailActivity extends ContactsActivity { private static final String TAG = "ContactDetailActivity"; - private ContactDetailFragment mFragment; + public static final int FRAGMENT_COUNT = 2; + + private ContactDetailAboutFragment mAboutFragment; + private ContactDetailUpdatesFragment mUpdatesFragment; + + private ContactDetailTabCarousel mTabCarousel; + private ViewPager mViewPager; + + private Uri mUri; @Override public void onCreate(Bundle savedState) { @@ -64,14 +86,29 @@ public class ContactDetailActivity extends ContactsActivity { setContentView(R.layout.contact_detail_activity); - mFragment = (ContactDetailFragment) getFragmentManager().findFragmentById( - R.id.contact_detail_fragment); - mFragment.setListener(mFragmentListener); - mFragment.loadUri(getIntent().getData()); + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager())); + mViewPager.setOnPageChangeListener(mOnPageChangeListener); + + mTabCarousel = (ContactDetailTabCarousel) findViewById(R.id.tab_carousel); + mTabCarousel.setListener(mTabCarouselListener); + mUri = getIntent().getData(); Log.i(TAG, getIntent().getData().toString()); } + + @Override + public void onAttachFragment(Fragment fragment) { + if (fragment instanceof ContactDetailAboutFragment) { + mAboutFragment = (ContactDetailAboutFragment) fragment; + mAboutFragment.setListener(mFragmentListener); + mAboutFragment.loadUri(mUri); + } else if (fragment instanceof ContactDetailUpdatesFragment) { + mUpdatesFragment = (ContactDetailUpdatesFragment) fragment; + } + } + @Override public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { @@ -84,7 +121,18 @@ public class ContactDetailActivity extends ContactsActivity { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mFragment.handleKeyDown(keyCode)) return true; + FragmentKeyListener mCurrentFragment; + switch (mViewPager.getCurrentItem()) { + case 0: + mCurrentFragment = (FragmentKeyListener) mAboutFragment; + break; + case 1: + mCurrentFragment = (FragmentKeyListener) mUpdatesFragment; + break; + default: + throw new IllegalStateException("Invalid current item for ViewPager"); + } + if (mCurrentFragment.handleKeyDown(keyCode)) return true; return super.onKeyDown(keyCode, event); } @@ -97,6 +145,11 @@ public class ContactDetailActivity extends ContactsActivity { } @Override + public void onDetailsLoaded(ContactLoader.Result result) { + mTabCarousel.loadData(result); + } + + @Override public void onEditRequested(Uri contactLookupUri) { startActivity(new Intent(Intent.ACTION_EDIT, contactLookupUri)); } @@ -127,4 +180,105 @@ public class ContactDetailActivity extends ContactsActivity { } }; + + public class ViewPagerAdapter extends FragmentPagerAdapter{ + + public ViewPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case 0: + return new ContactDetailAboutFragment(); + case 1: + return new ContactDetailUpdatesFragment(); + } + throw new IllegalStateException("No fragment at position " + position); + } + + @Override + public int getCount() { + return FRAGMENT_COUNT; + } + } + + private OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // The user is horizontally dragging the {@link ViewPager}, so send + // these scroll changes to the tab carousel. Ignore these events though if the carousel + // is actually controlling the {@link ViewPager} scrolls because it will already be + // in the correct position. + if (mViewPager.isFakeDragging()) { + return; + } + int x = (int) ((position + positionOffset) * mTabCarousel.getAllowedScrollLength()); + mTabCarousel.scrollTo(x, 0); + } + + @Override + public void onPageSelected(int position) { + // Since a new page has been selected by the {@link ViewPager}, + // update the tab selection in the carousel. + mTabCarousel.setCurrentTab(position); + } + + @Override + public void onPageScrollStateChanged(int state) {} + + }; + + private ContactDetailTabCarousel.Listener mTabCarouselListener = + new ContactDetailTabCarousel.Listener() { + + @Override + public void onTouchDown() { + // The user just started scrolling the carousel, so begin "fake dragging" the + // {@link ViewPager} if it's not already doing so. + if (mViewPager.isFakeDragging()) { + return; + } + mViewPager.beginFakeDrag(); + } + + @Override + public void onTouchUp() { + // The user just stopped scrolling the carousel, so stop "fake dragging" the + // {@link ViewPager} if was doing so before. + if (mViewPager.isFakeDragging()) { + mViewPager.endFakeDrag(); + } + } + + @Override + public void onScrollChanged(int l, int t, int oldl, int oldt) { + // The user is scrolling the carousel, so send the scroll deltas to the + // {@link ViewPager} so it can move in sync. + if (mViewPager.isFakeDragging()) { + mViewPager.fakeDragBy(oldl-l); + } + } + + @Override + public void onTabSelected(int position) { + // The user selected a tab, so update the {@link ViewPager} + mViewPager.setCurrentItem(position); + } + }; + + /** + * This interface should be implemented by {@link Fragment}s within this + * activity so that the activity can determine whether the currently + * displayed view is handling the key event or not. + */ + public interface FragmentKeyListener { + /** + * Returns true if the key down event will be handled by the implementing class, or false + * otherwise. + */ + public boolean handleKeyDown(int keyCode); + } } diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java index 53dd306f9..c247d4328 100644 --- a/src/com/android/contacts/activities/PeopleActivity.java +++ b/src/com/android/contacts/activities/PeopleActivity.java @@ -16,6 +16,7 @@ package com.android.contacts.activities; +import com.android.contacts.ContactLoader; import com.android.contacts.ContactSaveService; import com.android.contacts.ContactsActivity; import com.android.contacts.R; @@ -679,6 +680,11 @@ public class PeopleActivity extends ContactsActivity } @Override + public void onDetailsLoaded(ContactLoader.Result result) { + // Nothing needs to be done here + } + + @Override public void onEditRequested(Uri contactLookupUri) { startActivityForResult( new Intent(Intent.ACTION_EDIT, contactLookupUri), SUBACTIVITY_EDIT_CONTACT); diff --git a/src/com/android/contacts/detail/ContactDetailAboutFragment.java b/src/com/android/contacts/detail/ContactDetailAboutFragment.java new file mode 100644 index 000000000..a1377e83d --- /dev/null +++ b/src/com/android/contacts/detail/ContactDetailAboutFragment.java @@ -0,0 +1,78 @@ +/* + * 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.detail; + +import com.android.contacts.ContactLoader; +import com.android.contacts.R; + +import android.accounts.Account; +import android.app.ActionBar; +import android.app.Activity; +import android.content.ContentValues; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.ArrayList; + +public class ContactDetailAboutFragment extends ContactDetailFragment { + + private static final String TAG = "ContactDetailAboutFragment"; + + public ContactDetailAboutFragment() { + // Explicit constructor for inflation + } + + @Override + protected View createNewHeaderView(ViewGroup parent) { + ViewGroup headerView = (ViewGroup) inflate( + R.layout.simple_contact_detail_header_view_list_item, parent, false); + TextView phoneticNameView = (TextView) headerView.findViewById(R.id.phonetic_name); + TextView attributionView = (TextView) headerView.findViewById(R.id.attribution); + ContactDetailDisplayUtils.setPhoneticName(getContext(), getContactData(), phoneticNameView); + ContactDetailDisplayUtils.setAttribution(getContext(), getContactData(), attributionView); + return headerView; + } + + @Override + protected void bindData() { + ContactLoader.Result contactData = getContactData(); + if (contactData != null) { + // Setup the activity title and subtitle with contact name and company + Activity activity = getActivity(); + CharSequence displayName = ContactDetailDisplayUtils.getDisplayName(activity, + contactData); + String company = ContactDetailDisplayUtils.getCompany(activity, contactData); + + ActionBar actionBar = activity.getActionBar(); + actionBar.setTitle(displayName); + actionBar.setSubtitle(company); + + // Pass the contact loader result to the listener to finish setup + Listener listener = getListener(); + if (listener != null) { + listener.onDetailsLoaded(contactData); + } + } + + super.bindData(); + } +} diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java new file mode 100644 index 000000000..0f12de72e --- /dev/null +++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java @@ -0,0 +1,246 @@ +/* + * 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.detail; + +import com.android.contacts.ContactLoader; +import com.android.contacts.ContactLoader.Result; +import com.android.contacts.R; +import com.android.contacts.format.FormatUtils; +import com.android.contacts.preference.ContactsPreferences; +import com.android.contacts.util.ContactBadgeUtil; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.content.Entity.NamedContentValues; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Typeface; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.DisplayNameSources; +import android.text.Spanned; +import android.text.TextUtils; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AlphaAnimation; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * This class contains utility methods to bind high-level contact details + * (meaning name, phonetic name, job, and attribution) from a + * {@link ContactLoader.Result} data object to appropriate {@link View}s. + */ +public class ContactDetailDisplayUtils { + + private static final int PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS = 100; + + private ContactDetailDisplayUtils() { + // Disallow explicit creation of this class. + } + + /** + * Returns the display name of the contact. Depending on the preference for + * display name ordering, the contact's first name may be bolded if + * possible. Returns empty string if there is no display name. + */ + public static CharSequence getDisplayName(Context context, Result contactData) { + CharSequence displayName = contactData.getDisplayName(); + CharSequence altDisplayName = contactData.getAltDisplayName(); + ContactsPreferences prefs = new ContactsPreferences(context); + CharSequence styledName = ""; + if (!TextUtils.isEmpty(displayName) && !TextUtils.isEmpty(altDisplayName)) { + if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { + int overlapPoint = FormatUtils.overlapPoint( + displayName.toString(), altDisplayName.toString()); + if (overlapPoint > 0) { + styledName = FormatUtils.applyStyleToSpan(Typeface.BOLD, + displayName, 0, overlapPoint, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + styledName = displayName; + } + } else { + // Displaying alternate display name. + int overlapPoint = FormatUtils.overlapPoint( + altDisplayName.toString(), displayName.toString()); + if (overlapPoint > 0) { + styledName = FormatUtils.applyStyleToSpan(Typeface.BOLD, + altDisplayName, overlapPoint, altDisplayName.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + styledName = altDisplayName; + } + } + } + return styledName; + } + + /** + * Returns the phonetic name of the contact or null if there isn't one. + */ + public static String getPhoneticName(Context context, Result contactData) { + String phoneticName = contactData.getPhoneticName(); + if (!TextUtils.isEmpty(phoneticName)) { + return phoneticName; + } + return null; + } + + /** + * Returns the attribution string for the contact. This could either specify + * that this is a joined contact or specify the contact directory that the + * contact came from. Returns null if there is none applicable. + */ + public static String getAttribution(Context context, Result contactData) { + // Check if this is a joined contact + if (contactData.getEntities().size() > 1) { + return context.getString(R.string.indicator_joined_contact); + } else if (contactData.isDirectoryEntry()) { + // This contact is from a directory + String directoryDisplayName = contactData.getDirectoryDisplayName(); + String directoryType = contactData.getDirectoryType(); + String displayName = !TextUtils.isEmpty(directoryDisplayName) + ? directoryDisplayName + : directoryType; + return context.getString(R.string.contact_directory_description, displayName); + } + return null; + } + + /** + * Returns the organization of the contact. If several organizations are given, + * the first one is used. Returns null if not applicable. + */ + public static String getCompany(Context context, Result contactData) { + final boolean displayNameIsOrganization = contactData.getDisplayNameSource() + == DisplayNameSources.ORGANIZATION; + for (Entity entity : contactData.getEntities()) { + for (NamedContentValues subValue : entity.getSubValues()) { + final ContentValues entryValues = subValue.values; + final String mimeType = entryValues.getAsString(Data.MIMETYPE); + + if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { + final String company = entryValues.getAsString(Organization.COMPANY); + final String title = entryValues.getAsString(Organization.TITLE); + final String combined; + // We need to show company and title in a combined string. However, if the + // DisplayName is already the organization, it mirrors company or (if company + // is empty title). Make sure we don't show what's already shown as DisplayName + if (TextUtils.isEmpty(company)) { + combined = displayNameIsOrganization ? null : title; + } else { + if (TextUtils.isEmpty(title)) { + combined = displayNameIsOrganization ? null : company; + } else { + if (displayNameIsOrganization) { + combined = title; + } else { + combined = context.getString( + R.string.organization_company_and_title, + company, title); + } + } + } + + if (!TextUtils.isEmpty(combined)) { + return combined; + } + } + } + } + return null; + } + + + /** + * Sets the contact photo to display in the given {@link ImageView}. If bitmap is null, the + * default placeholder image is shown. + */ + public static void setPhoto(Context context, Result contactData, ImageView photoView) { + if (contactData.isLoadingPhoto()) { + photoView.setImageBitmap(null); + return; + } + byte[] photo = contactData.getPhotoBinaryData(); + Bitmap bitmap = photo != null ? BitmapFactory.decodeByteArray(photo, 0, photo.length) + : ContactBadgeUtil.loadPlaceholderPhoto(context); + boolean fadeIn = contactData.isDirectoryEntry(); + if (photoView.getDrawable() == null && fadeIn) { + AlphaAnimation animation = new AlphaAnimation(0, 1); + animation.setDuration(PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS); + animation.setInterpolator(new AccelerateInterpolator()); + photoView.startAnimation(animation); + } + photoView.setImageBitmap(bitmap); + } + + /** + * Sets the starred state of this contact. + */ + public static void setStarred(Result contactData, CheckBox starredView) { + // Check if the starred state should be visible + if (!contactData.isDirectoryEntry()) { + starredView.setVisibility(View.VISIBLE); + starredView.setChecked(contactData.getStarred()); + } else { + starredView.setVisibility(View.GONE); + } + } + + /** + * Set the social snippet text and date. If there isn't one, then set the view to gone. + */ + public static void setSocialSnippetAndDate(Context context, Result contactData, + TextView statusView, TextView dateView) { + setDataOrHideIfNone(contactData.getSocialSnippet(), statusView); + setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(contactData, context), dateView); + } + + /** + * Sets the phonetic name of this contact to the given {@link TextView}. If + * there is none, then set the view to gone. + */ + public static void setPhoneticName(Context context, Result contactData, TextView textView) { + setDataOrHideIfNone(getPhoneticName(context, contactData), textView); + } + + /** + * Sets the attribution contact to the given {@link TextView}. If + * there is none, then set the view to gone. + */ + public static void setAttribution(Context context, Result contactData, TextView textView) { + setDataOrHideIfNone(getAttribution(context, contactData), textView); + } + + /** + * Helper function to display the given text in the {@link TextView} or + * hides the {@link TextView} if the text is empty or null. + */ + private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) { + if (!TextUtils.isEmpty(textToDisplay)) { + textView.setText(textToDisplay); + textView.setVisibility(View.VISIBLE); + } else { + textView.setText(null); + textView.setVisibility(View.GONE); + } + } + +}
\ No newline at end of file diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java index 2a0085a27..599a12e11 100644 --- a/src/com/android/contacts/detail/ContactDetailFragment.java +++ b/src/com/android/contacts/detail/ContactDetailFragment.java @@ -26,6 +26,7 @@ import com.android.contacts.GroupMetaData; import com.android.contacts.NfcHandler; import com.android.contacts.R; import com.android.contacts.TypePrecedence; +import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener; import com.android.contacts.editor.SelectAccountDialogFragment; import com.android.contacts.model.AccountType; import com.android.contacts.model.AccountType.EditType; @@ -107,7 +108,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class ContactDetailFragment extends Fragment implements +public class ContactDetailFragment extends Fragment implements FragmentKeyListener, OnItemClickListener, OnItemLongClickListener, SelectAccountDialogFragment.Listener { private static final String TAG = "ContactDetailFragment"; @@ -250,10 +251,26 @@ public class ContactDetailFragment extends Fragment implements return mView; } + protected View inflate(int resource, ViewGroup root, boolean attachToRoot) { + return mInflater.inflate(resource, root, attachToRoot); + } + public void setListener(Listener value) { mListener = value; } + protected Context getContext() { + return mContext; + } + + protected Listener getListener() { + return mListener; + } + + protected ContactLoader.Result getContactData() { + return mContactData; + } + public Uri getUri() { return mLookupUri; } @@ -289,7 +306,7 @@ public class ContactDetailFragment extends Fragment implements } } - private void bindData() { + protected void bindData() { if (mView == null) { return; } @@ -986,11 +1003,7 @@ public class ContactDetailFragment extends Fragment implements if (mHeaderView != null) { return mHeaderView; } - mHeaderView = (ContactDetailHeaderView) mInflater.inflate( - R.layout.contact_detail_header_view_list_item, parent, false); - mHeaderView.setListener(mHeaderViewListener); - mHeaderView.loadData(mContactData); - return mHeaderView; + return createNewHeaderView(parent); } private View getSeparatorEntryView(View convertView, ViewGroup parent) { @@ -1172,6 +1185,16 @@ public class ContactDetailFragment extends Fragment implements } } + /** + * Returns a new header view for the top of the list of contact details. + */ + protected View createNewHeaderView(ViewGroup parent) { + mHeaderView = (ContactDetailHeaderView) inflate( + R.layout.contact_detail_header_view_list_item, parent, false); + mHeaderView.loadData(mContactData); + return mHeaderView; + } + @Override public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.view_contact, menu); @@ -1336,6 +1359,7 @@ public class ContactDetailFragment extends Fragment implements return true; } + @Override public boolean handleKeyDown(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_CALL: { @@ -1414,17 +1438,6 @@ public class ContactDetailFragment extends Fragment implements } }; - private ContactDetailHeaderView.Listener mHeaderViewListener = - new ContactDetailHeaderView.Listener() { - @Override - public void onDisplayNameClick(View view) { - } - - @Override - public void onPhotoClick(View view) { - } - }; - public static interface Listener { /** * Contact was not found, so somehow close this fragment. This is raised after a contact @@ -1433,6 +1446,11 @@ public class ContactDetailFragment extends Fragment implements public void onContactNotFound(); /** + * This contact's details have been loaded. + */ + public void onDetailsLoaded(ContactLoader.Result result); + + /** * User decided to go to Edit-Mode */ public void onEditRequested(Uri lookupUri); diff --git a/src/com/android/contacts/detail/ContactDetailHeaderView.java b/src/com/android/contacts/detail/ContactDetailHeaderView.java index 795ed62bf..63f8fbe90 100644 --- a/src/com/android/contacts/detail/ContactDetailHeaderView.java +++ b/src/com/android/contacts/detail/ContactDetailHeaderView.java @@ -56,6 +56,7 @@ import android.widget.Toast; * Header for displaying a title bar with contact info. You * can bind specific values by calling * {@link ContactDetailHeaderView#loadData(com.android.contacts.ContactLoader.Result)} + * TODO: Refactor to use {@link ContactDetailDisplayUtils} */ public class ContactDetailHeaderView extends FrameLayout implements View.OnClickListener, View.OnLongClickListener { diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java new file mode 100644 index 000000000..a8803f525 --- /dev/null +++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java @@ -0,0 +1,230 @@ +/* + * 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.detail; + +import com.android.contacts.ContactLoader; +import com.android.contacts.ContactSaveService; +import com.android.contacts.R; +import com.android.contacts.activities.ContactDetailActivity; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.CheckBox; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * This is a horizontally scrolling carousel with 2 tabs: one to see info about the contact and + * one to see updates from the contact. + * TODO: Create custom views for the tabs so their width can be programatically set as 2/3 of the + * screen width. + */ +public class ContactDetailTabCarousel extends HorizontalScrollView + implements View.OnClickListener, OnTouchListener { + private static final String TAG = "ContactDetailTabCarousel"; + + private CheckBox mStarredView; + private ImageView mPhotoView; + private TextView mStatusView; + private TextView mStatusDateView; + + private Uri mContactUri; + private Listener mListener; + + private View[] mTabs = new View[2]; + + private int mAllowedScrollLength; + + /** + * Interface for callbacks invoked when the user interacts with the carousel. + */ + public interface Listener { + public void onTouchDown(); + public void onTouchUp(); + public void onScrollChanged(int l, int t, int oldl, int oldt); + public void onTabSelected(int position); + } + + public ContactDetailTabCarousel(Context context) { + this(context, null); + } + + public ContactDetailTabCarousel(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ContactDetailTabCarousel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final LayoutInflater inflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.contact_detail_tab_carousel, this); + + setOnTouchListener(this); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + mListener.onScrollChanged(l, t, oldl, oldt); + } + + /** + * Returns the number of pixels that this view can be scrolled. + */ + public int getAllowedScrollLength() { + if (mAllowedScrollLength == 0) { + // Find the total length of two tabs side-by-side + int totalLength = 0; + for (int i=0; i < mTabs.length; i++) { + totalLength += mTabs[i].getWidth(); + } + // Find the allowed scrolling length by subtracting the current visible screen width + // from the total length of the tabs. + mAllowedScrollLength = totalLength - getWidth(); + } + return mAllowedScrollLength; + } + + /** + * Updates the tab selection. + */ + public void setCurrentTab(int position) { + if (position < 0 || position > mTabs.length) { + throw new IllegalStateException("Invalid position in array of tabs: " + position); + } + // TODO: Handle device rotation (saving and restoring state of the selected tab) + // This will take more work because there is no tab carousel in phone landscape + if (mTabs[position] == null) { + return; + } + mTabs[position].setSelected(true); + unselectAllOtherTabs(position); + } + + private void unselectAllOtherTabs(int position) { + for (int i = 0; i < mTabs.length; i++) { + if (position != i) { + mTabs[i].setSelected(false); + } + } + } + + /** + * Loads the data from the Loader-Result. This is the only function that has to be called + * from the outside to fully setup the View + */ + public void loadData(ContactLoader.Result contactData) { + mContactUri = contactData.getLookupUri(); + + View aboutView = findViewById(R.id.tab_about); + View updateView = findViewById(R.id.tab_update); + + TextView aboutTab = (TextView) aboutView.findViewById(R.id.label); + aboutTab.setText(mContext.getString(R.string.contactDetailAbout)); + aboutTab.setClickable(true); + aboutTab.setSelected(true); + + TextView updatesTab = (TextView) updateView.findViewById(R.id.label); + updatesTab.setText(mContext.getString(R.string.contactDetailUpdates)); + updatesTab.setClickable(true); + + aboutTab.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mListener.onTabSelected(0); + } + }); + updatesTab.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mListener.onTabSelected(1); + } + }); + + mTabs[0] = aboutTab; + mTabs[1] = updatesTab; + + // Retrieve the photo and star views for the "about" tab + mPhotoView = (ImageView) aboutView.findViewById(R.id.photo); + mStarredView = (CheckBox) aboutView.findViewById(R.id.star); + mStarredView.setOnClickListener(this); + + // Retrieve the social update views for the "updates" tab + mStatusView = (TextView) updateView.findViewById(R.id.status); + mStatusDateView = (TextView) updateView.findViewById(R.id.status_date); + + ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView); + ContactDetailDisplayUtils.setStarred(contactData, mStarredView); + ContactDetailDisplayUtils.setSocialSnippetAndDate(mContext, contactData, mStatusView, + mStatusDateView); + } + + /** + * Set the given {@link Listener} to handle carousel events. + */ + public void setListener(Listener listener) { + mListener = listener; + } + + // TODO: The starred icon needs to move to the action bar. + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.star: { + // Toggle "starred" state + // Make sure there is a contact + if (mContactUri != null) { + Intent intent = ContactSaveService.createSetStarredIntent( + getContext(), mContactUri, mStarredView.isChecked()); + getContext().startService(intent); + } + break; + } + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mListener.onTouchDown(); + return true; + case MotionEvent.ACTION_UP: + mListener.onTouchUp(); + return true; + } + return super.onTouchEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + boolean interceptTouch = super.onInterceptTouchEvent(ev); + if (interceptTouch) { + mListener.onTouchDown(); + } + return interceptTouch; + } +} diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java new file mode 100644 index 000000000..02678de45 --- /dev/null +++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java @@ -0,0 +1,45 @@ +/* + * 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.detail; + +import com.android.contacts.R; +import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +public class ContactDetailUpdatesFragment extends Fragment implements FragmentKeyListener { + + private static final String TAG = "ContactDetailUpdatesFragment"; + + public ContactDetailUpdatesFragment() { + // Explicit constructor for inflation + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + return inflater.inflate(R.layout.contact_detail_updates_fragment, container, false); + } + + @Override + public boolean handleKeyDown(int keyCode) { + return false; + } +} |