Merge "Stream items UI."
diff --git a/res/layout-sw580dp-w1000dp/contact_detail_container.xml b/res/layout-sw580dp-w1000dp/contact_detail_container.xml
index b6a6319..be91296 100644
--- a/res/layout-sw580dp-w1000dp/contact_detail_container.xml
+++ b/res/layout-sw580dp-w1000dp/contact_detail_container.xml
@@ -30,12 +30,11 @@
android:id="@+id/about_fragment"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="3" />
+ android:layout_weight="1" />
<fragment class="com.android.contacts.detail.ContactDetailUpdatesFragment"
android:id="@+id/updates_fragment"
- android:layout_width="0dip"
- android:layout_height="match_parent"
- android:layout_weight="2" />
+ android:layout_width="306dip"
+ android:layout_height="match_parent" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw580dp/people_activity.xml b/res/layout-sw580dp/people_activity.xml
index d3d7c32..87bb3b5 100644
--- a/res/layout-sw580dp/people_activity.xml
+++ b/res/layout-sw580dp/people_activity.xml
@@ -69,7 +69,7 @@
ex:layout_narrowParentWidth="800dip"
ex:layout_narrowMarginRight="0dip"
ex:layout_wideParentWidth="1280dip"
- ex:layout_wideMarginRight="48dip"
+ ex:layout_wideMarginRight="0dip"
ex:clipMarginLeft="0dip"
ex:clipMarginTop="3dip"
ex:clipMarginRight="3dip"
diff --git a/res/layout/carousel_updates_tab.xml b/res/layout/carousel_updates_tab.xml
index d235280..d453dae 100644
--- a/res/layout/carousel_updates_tab.xml
+++ b/res/layout/carousel_updates_tab.xml
@@ -21,13 +21,22 @@
android:layout_weight="1"
android:background="@color/detail_tab_background">
- <!-- Transparent view to overlay on the contact's photo
+ <ImageView android:id="@+id/status_photo"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:visibility="gone" />
+
+
+ <!-- Transparent view to overlay on the update 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:layout_above="@id/status_photo"
android:background="@android:color/black"
android:alpha=".25"/>
@@ -37,6 +46,7 @@
android:layout_height="@dimen/detail_tab_carousel_tab_label_height"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
+ android:layout_above="@id/status_photo"
android:paddingLeft="@dimen/detail_item_side_margin"
android:singleLine="true"
android:gravity="left|center_vertical"
diff --git a/res/layout/contact_detail_updates_fragment.xml b/res/layout/contact_detail_updates_fragment.xml
index 36a40c3..92f3575 100644
--- a/res/layout/contact_detail_updates_fragment.xml
+++ b/res/layout/contact_detail_updates_fragment.xml
@@ -19,40 +19,31 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <!--
- TODO: Make this a list of social updates instead of just showing one update. Wait
- until the social integration is done in the provider so that we know the
- best way to setup the adapter.
- -->
- <LinearLayout
- android:orientation="vertical"
+ <ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/detail_tab_carousel_height"
- android:background="@color/background_primary">
+ android:background="@color/background_social_updates">
- <include
- android:id="@+id/title"
- layout="@layout/contact_detail_kind_title_entry_view"
- android:paddingTop="@dimen/detail_item_vertical_margin" />
-
- <TextView android:id="@+id/status"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingTop="@dimen/detail_item_vertical_margin"
- android:paddingLeft="@dimen/detail_item_side_margin"
- android:paddingRight="@dimen/detail_item_side_margin"
- android:textAppearance="?android:attr/textAppearanceMedium"/>
+ android:paddingTop="@dimen/detail_update_section_top_padding">
- <TextView android:id="@+id/status_date"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingLeft="@dimen/detail_item_side_margin"
- android:paddingRight="@dimen/detail_item_side_margin"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:attr/textColorTertiary"/>
+ <include
+ android:id="@+id/title"
+ layout="@layout/contact_detail_kind_title_entry_view" />
- </LinearLayout>
+ <LinearLayout
+ android:id="@+id/update_list"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/detail_update_section_side_padding"
+ android:paddingRight="@dimen/detail_update_section_side_padding" />
+ </LinearLayout>
+ </ScrollView>
<View
android:id="@+id/alpha_overlay"
diff --git a/res/layout/stream_item_one_column.xml b/res/layout/stream_item_one_column.xml
new file mode 100644
index 0000000..014e3f1
--- /dev/null
+++ b/res/layout/stream_item_one_column.xml
@@ -0,0 +1,36 @@
+<?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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/detail_update_section_item_vertical_padding"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/stream_item_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/detail_update_section_item_vertical_padding"
+ android:paddingLeft="@dimen/detail_update_section_item_left_padding"
+ android:orientation="vertical" />
+
+ <View
+ android:id="@+id/horizontal_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="?android:attr/dividerHorizontal" />
+</LinearLayout>
diff --git a/res/layout/stream_item_pair.xml b/res/layout/stream_item_pair.xml
new file mode 100644
index 0000000..61b248c
--- /dev/null
+++ b/res/layout/stream_item_pair.xml
@@ -0,0 +1,32 @@
+<?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:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <FrameLayout android:id="@+id/stream_pair_first"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <FrameLayout android:id="@+id/stream_pair_second"
+ android:paddingLeft="@dimen/detail_update_section_internal_padding"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/stream_item_text.xml b/res/layout/stream_item_text.xml
new file mode 100644
index 0000000..601dfb9
--- /dev/null
+++ b/res/layout/stream_item_text.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:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/stream_item_html"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textColor="@color/social_update_text_color" />
+
+ <TextView android:id="@+id/stream_item_attribution"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/social_update_attribution_color"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values-sw580dp-w1000dp/dimens.xml b/res/values-sw580dp-w1000dp/dimens.xml
index dfa1b81..f08abae 100644
--- a/res/values-sw580dp-w1000dp/dimens.xml
+++ b/res/values-sw580dp-w1000dp/dimens.xml
@@ -22,4 +22,5 @@
<dimen name="detail_header_view_margin">16dip</dimen>
<dimen name="detail_header_attribution_height">56dip</dimen>
<dimen name="detail_tab_carousel_height">0dip</dimen>
+ <dimen name="detail_update_section_top_padding">48dip</dimen>
</resources>
diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml
index bcaf1d2..e671229 100644
--- a/res/values-sw580dp/dimens.xml
+++ b/res/values-sw580dp/dimens.xml
@@ -33,5 +33,6 @@
<dimen name="list_section_height">37dip</dimen>
<dimen name="directory_header_height">56dip</dimen>
<dimen name="detail_tab_carousel_height">256dip</dimen>
+ <dimen name="detail_update_section_item_vertical_padding">32dip</dimen>
<dimen name="search_view_width">400dip</dimen>
</resources>
diff --git a/res/values-w470dp/strings.xml b/res/values-w470dp/strings.xml
new file mode 100644
index 0000000..8dc9c09
--- /dev/null
+++ b/res/values-w470dp/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="recent_updates">Updates</string>
+</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 7f4e237..8a644b1 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -29,6 +29,12 @@
<color name="translucent_search_background">#cc000000</color>
+ <color name="background_social_updates">#ffeeeeee</color>
+
+ <color name="social_update_text_color">#ff333333</color>
+
+ <color name="social_update_attribution_color">#ff777777</color>
+
<!-- Color used for the letter in the A-Z section header -->
<color name="section_header_text_color">#ff999999</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c3b1fee..8d19a3e 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -104,6 +104,21 @@
<!-- Left and right padding of the text within the update tab in the tab carousel -->
<dimen name="detail_update_tab_side_padding">24dip</dimen>
+ <!-- Top padding of the update section in the contact detail card -->
+ <dimen name="detail_update_section_top_padding">8dip</dimen>
+
+ <!-- Left and right padding of the update section in the contact detail card -->
+ <dimen name="detail_update_section_side_padding">16dip</dimen>
+
+ <!-- Vertical padding above and below individual stream items -->
+ <dimen name="detail_update_section_item_vertical_padding">16dip</dimen>
+
+ <!-- Left-side padding for individual stream items -->
+ <dimen name="detail_update_section_item_left_padding">8dip</dimen>
+
+ <!-- Horizontal padding between content sections within a stream item -->
+ <dimen name="detail_update_section_internal_padding">16dip</dimen>
+
<!-- Margin around the contact's photo on the contact card -->
<dimen name="detail_contact_photo_margin">15dip</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bf9d403..87307ae 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -86,10 +86,10 @@
creating a new group. This string represents the built in way to create the group. [CHAR LIMIT=NONE] -->
<string name="insertGroupDescription">Create group</string>
- <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=11] -->
+ <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=15] -->
<string name="contactDetailAbout">About</string>
- <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=11] -->
+ <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=15] -->
<string name="contactDetailUpdates">Updates</string>
<!-- Hint text in the search box when the user hits the Search key while in the contacts app -->
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index f10e6b8..fa0ffb2 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -17,6 +17,8 @@
package com.android.contacts;
import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.StreamItemEntry;
+import com.android.contacts.util.StreamItemPhotoEntry;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentResolver;
@@ -42,6 +44,8 @@
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StreamItems;
+import android.provider.ContactsContract.StreamItemPhotos;
import android.text.TextUtils;
import android.util.Log;
@@ -51,8 +55,12 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Loads a single Contact and all it constituent RawContacts.
@@ -62,6 +70,7 @@
private Uri mLookupUri;
private boolean mLoadGroupMetaData;
+ private boolean mLoadStreamItems;
private Result mContact;
private ForceLoadContentObserver mObserver;
private boolean mDestroyed;
@@ -102,11 +111,8 @@
private final boolean mStarred;
private final Integer mPresence;
private final ArrayList<Entity> mEntities;
+ private ArrayList<StreamItemEntry> mStreamItems;
private final HashMap<Long, DataStatus> mStatuses;
- private final String mStatus;
- private final Long mStatusTimestamp;
- private final Integer mStatusLabel;
- private final String mStatusResPackage;
private String mDirectoryDisplayName;
private String mDirectoryType;
@@ -130,6 +136,7 @@
mLookupKey = null;
mId = -1;
mEntities = null;
+ mStreamItems = new ArrayList<StreamItemEntry>();
mStatuses = null;
mNameRawContactId = -1;
mDisplayNameSource = DisplayNameSources.UNDEFINED;
@@ -140,10 +147,6 @@
mPhoneticName = null;
mStarred = false;
mPresence = null;
- mStatus = null;
- mStatusTimestamp = null;
- mStatusLabel = null;
- mStatusResPackage = null;
}
/**
@@ -152,14 +155,14 @@
private Result(Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id,
long nameRawContactId, int displayNameSource, long photoId, String photoUri,
String displayName, String altDisplayName, String phoneticName, boolean starred,
- Integer presence, String status, Long statusTimestamp, Integer statusLabel,
- String statusResPackage) {
+ Integer presence) {
mLookupUri = lookupUri;
mUri = uri;
mDirectoryId = directoryId;
mLookupKey = lookupKey;
mId = id;
mEntities = new ArrayList<Entity>();
+ mStreamItems = new ArrayList<StreamItemEntry>();
mStatuses = new HashMap<Long, DataStatus>();
mNameRawContactId = nameRawContactId;
mDisplayNameSource = displayNameSource;
@@ -170,10 +173,6 @@
mPhoneticName = phoneticName;
mStarred = starred;
mPresence = presence;
- mStatus = status;
- mStatusTimestamp = statusTimestamp;
- mStatusLabel = statusLabel;
- mStatusResPackage = statusResPackage;
}
private Result(Result from) {
@@ -192,11 +191,8 @@
mStarred = from.mStarred;
mPresence = from.mPresence;
mEntities = from.mEntities;
+ mStreamItems = from.mStreamItems;
mStatuses = from.mStatuses;
- mStatus = from.mStatus;
- mStatusTimestamp = from.mStatusTimestamp;
- mStatusLabel = from.mStatusLabel;
- mStatusResPackage = from.mStatusResPackage;
mDirectoryDisplayName = from.mDirectoryDisplayName;
mDirectoryType = from.mDirectoryType;
@@ -283,26 +279,14 @@
return mPresence;
}
- public String getSocialSnippet() {
- return mStatus;
- }
-
- public Long getStatusTimestamp() {
- return mStatusTimestamp;
- }
-
- public Integer getStatusLabel() {
- return mStatusLabel;
- }
-
- public String getStatusResPackage() {
- return mStatusResPackage;
- }
-
public ArrayList<Entity> getEntities() {
return mEntities;
}
+ public ArrayList<StreamItemEntry> getStreamItems() {
+ return mStreamItems;
+ }
+
public HashMap<Long, DataStatus> getStatuses() {
return mStatuses;
}
@@ -387,8 +371,11 @@
}
}
+ /**
+ * Projection used for the query that loads all data for the entire contact (except for
+ * social stream items).
+ */
private static class ContactQuery {
- // Projection used for the query that loads all data for the entire contact.
final static String[] COLUMNS = new String[] {
Contacts.NAME_RAW_CONTACT_ID,
Contacts.DISPLAY_NAME_SOURCE,
@@ -524,8 +511,10 @@
public final static int PHOTO_URI = 59;
}
+ /**
+ * Projection used for the query that loads all data for the entire contact.
+ */
private static class DirectoryQuery {
- // Projection used for the query that loads all data for the entire contact.
final static String[] COLUMNS = new String[] {
Directory.DISPLAY_NAME,
Directory.PACKAGE_NAME,
@@ -575,6 +564,9 @@
} else if (mLoadGroupMetaData) {
loadGroupMetaData(result);
}
+ if (mLoadStreamItems) {
+ loadStreamItems(result);
+ }
loadPhotoBinaryData(result);
}
return result;
@@ -748,15 +740,6 @@
final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
? null
: cursor.getInt(ContactQuery.CONTACT_PRESENCE);
- final String status = cursor.getString(ContactQuery.CONTACT_STATUS);
- final Long statusTimestamp = cursor.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
- ? null
- : cursor.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
- final Integer statusLabel = cursor.isNull(ContactQuery.CONTACT_STATUS_LABEL)
- ? null
- : cursor.getInt(ContactQuery.CONTACT_STATUS_LABEL);
- final String statusResPackage = cursor.getString(
- ContactQuery.CONTACT_STATUS_RES_PACKAGE);
Uri lookupUri;
if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
@@ -768,8 +751,7 @@
return new Result(contactUri, lookupUri, directoryId, lookupKey, contactId,
nameRawContactId, displayNameSource, photoId, photoUri, displayName,
- altDisplayName, phoneticName, starred, presence, status, statusTimestamp,
- statusLabel, statusResPackage);
+ altDisplayName, phoneticName, starred, presence);
}
/**
@@ -937,6 +919,60 @@
}
}
+ /**
+ * Loads all stream items and stream item photos belonging to this contact.
+ */
+ private void loadStreamItems(Result result) {
+ Cursor cursor = getContext().getContentResolver().query(
+ Contacts.CONTENT_LOOKUP_URI.buildUpon()
+ .appendPath(result.getLookupKey())
+ .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
+ null, null, null, null);
+ Map<Long, StreamItemEntry> streamItemsById = new HashMap<Long, StreamItemEntry>();
+ ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
+ try {
+ while (cursor.moveToNext()) {
+ StreamItemEntry streamItem = new StreamItemEntry(cursor);
+ streamItemsById.put(streamItem.getId(), streamItem);
+ streamItems.add(streamItem);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Now retrieve any photo records associated with the stream items.
+ String[] streamItemIdArr = new String[streamItems.size()];
+ StringBuilder streamItemPhotoSelection = new StringBuilder();
+ if (!streamItems.isEmpty()) {
+ streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN (");
+ for (int i = 0; i < streamItems.size(); i++) {
+ if (i > 0) {
+ streamItemPhotoSelection.append(",");
+ }
+ streamItemPhotoSelection.append("?");
+ streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId());
+ }
+ streamItemPhotoSelection.append(")");
+ cursor = getContext().getContentResolver().query(StreamItems.CONTENT_PHOTO_URI,
+ null, streamItemPhotoSelection.toString(), streamItemIdArr,
+ StreamItemPhotos.STREAM_ITEM_ID);
+ try {
+ while (cursor.moveToNext()) {
+ long streamItemId = cursor.getLong(
+ cursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID));
+ StreamItemEntry streamItem = streamItemsById.get(streamItemId);
+ streamItem.addPhoto(new StreamItemPhotoEntry(cursor));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ // Set the sorted stream items on the result.
+ Collections.sort(streamItems);
+ result.mStreamItems.addAll(streamItems);
+ }
+
@Override
protected void onPostExecute(Result result) {
unregisterObserver();
@@ -1022,13 +1058,15 @@
}
public ContactLoader(Context context, Uri lookupUri) {
- this(context, lookupUri, false);
+ this(context, lookupUri, false, false);
}
- public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData) {
+ public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
+ boolean loadStreamItems) {
super(context);
mLookupUri = lookupUri;
mLoadGroupMetaData = loadGroupMetaData;
+ mLoadStreamItems = loadStreamItems;
}
public Uri getLookupUri() {
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 14378d9..4d04ac2 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -283,7 +283,7 @@
public void run() {
mContactData = result;
mLookupUri = result.getLookupUri();
- mContactHasUpdates = result.getSocialSnippet() != null;
+ mContactHasUpdates = !result.getStreamItems().isEmpty();
invalidateOptionsMenu();
setupTitle();
if (mContactHasUpdates) {
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index 3f28b58..e9d75ef 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -18,10 +18,13 @@
import com.android.contacts.ContactLoader;
import com.android.contacts.ContactLoader.Result;
+import com.android.contacts.ContactPhotoManager;
import com.android.contacts.R;
import com.android.contacts.format.FormatUtils;
import com.android.contacts.preference.ContactsPreferences;
import com.android.contacts.util.ContactBadgeUtil;
+import com.android.contacts.util.StreamItemEntry;
+import com.android.contacts.util.StreamItemPhotoEntry;
import android.content.ContentValues;
import android.content.Context;
@@ -30,19 +33,26 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;
+import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.DisplayNameSources;
+import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.widget.CheckBox;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.TextView;
+import java.util.List;
+
/**
* This class contains utility methods to bind high-level contact details
* (meaning name, phonetic name, job, and attribution) from a
@@ -207,23 +217,135 @@
/**
* Set the social snippet text. If there isn't one, then set the view to gone.
*/
- public static void setSocialSnippet(Context context, Result contactData, TextView statusView) {
+ public static void setSocialSnippet(Context context, Result contactData, TextView statusView,
+ ImageView statusPhotoView) {
if (statusView == null) {
return;
}
- setDataOrHideIfNone(contactData.getSocialSnippet(), statusView);
+
+ String snippet = null;
+ String photoUri = null;
+ if (!contactData.getStreamItems().isEmpty()) {
+ StreamItemEntry firstEntry = contactData.getStreamItems().get(0);
+ snippet = firstEntry.getText();
+ if (!firstEntry.getPhotos().isEmpty()) {
+ StreamItemPhotoEntry firstPhoto = firstEntry.getPhotos().get(0);
+ photoUri = firstPhoto.getPhotoUri();
+
+ // If displaying an image, hide the snippet text.
+ snippet = null;
+ }
+ }
+ setDataOrHideIfNone(snippet, statusView);
+ if (photoUri != null) {
+ ContactPhotoManager.getInstance(context).loadPhoto(
+ statusPhotoView, Uri.parse(photoUri));
+ statusPhotoView.setVisibility(View.VISIBLE);
+ } else {
+ statusPhotoView.setVisibility(View.GONE);
+ }
}
/**
- * Set the social snippet text and date. If there isn't one, then set the view to gone.
+ * Displays the social stream items under the given layout.
*/
- public static void setSocialSnippetAndDate(Context context, Result contactData,
- TextView statusView, TextView dateView) {
- if (statusView == null || dateView == null) {
- return;
+ public static void showSocialStreamItems(LayoutInflater inflater, Context context,
+ Result contactData, LinearLayout streamContainer) {
+ if (streamContainer != null) {
+ streamContainer.removeAllViews();
+ List<StreamItemEntry> streamItems = contactData.getStreamItems();
+ for (StreamItemEntry streamItem : streamItems) {
+ addStreamItemToContainer(inflater, context, streamItem, streamContainer);
+ }
}
- setDataOrHideIfNone(contactData.getSocialSnippet(), statusView);
- setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(contactData, context), dateView);
+ }
+
+ public static void addStreamItemToContainer(LayoutInflater inflater, Context context,
+ StreamItemEntry streamItem, LinearLayout streamContainer) {
+ View oneColumnView = inflater.inflate(R.layout.stream_item_one_column,
+ streamContainer, false);
+ ViewGroup contentBox = (ViewGroup) oneColumnView.findViewById(R.id.stream_item_content);
+ int internalPadding = context.getResources().getDimensionPixelSize(
+ R.dimen.detail_update_section_internal_padding);
+
+ // TODO: This is not the correct layout for a stream item with photos. Photos should be
+ // displayed first, then the update text either to the right of the final image (if there
+ // are an odd number of images) or below the last row of images (if there are an even
+ // number of images). Since this is designed as a two-column grid, we should also consider
+ // using a TableLayout instead of the series of nested LinearLayouts that we have now.
+ // See the Updates section of the Contacts Architecture document for details.
+
+ // If there are no photos, just display the text in a single column.
+ List<StreamItemPhotoEntry> photos = streamItem.getPhotos();
+ if (photos.isEmpty()) {
+ addStreamItemText(inflater, context, streamItem, contentBox);
+ } else {
+ // If the first photo is square or portrait mode, show the text alongside it.
+ boolean isFirstPhotoAlongsideText = false;
+ StreamItemPhotoEntry firstPhoto = photos.get(0);
+ isFirstPhotoAlongsideText = firstPhoto.getHeight() >= firstPhoto.getWidth();
+ if (isFirstPhotoAlongsideText) {
+ View twoColumnView = inflater.inflate(R.layout.stream_item_pair, contentBox, false);
+ addStreamItemPhoto(inflater, context, firstPhoto,
+ (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_first));
+ addStreamItemText(inflater, context, streamItem,
+ (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_second));
+ contentBox.addView(twoColumnView);
+ } else {
+ // Just add the stream item text at the top of the entry.
+ addStreamItemText(inflater, context, streamItem, contentBox);
+ }
+ for (int i = isFirstPhotoAlongsideText ? 1 : 0; i < photos.size(); i++) {
+ StreamItemPhotoEntry photo = photos.get(i);
+
+ // If the photo is landscape, show it at full-width.
+ if (photo.getWidth() > photo.getHeight()) {
+ View photoView = addStreamItemPhoto(inflater, context, photo, contentBox);
+ photoView.setPadding(0, internalPadding, 0, 0);
+ } else {
+ // If this photo and the next are both square or portrait, show them as a pair.
+ StreamItemPhotoEntry nextPhoto = i + 1 < photos.size()
+ ? photos.get(i + 1) : null;
+ if (nextPhoto != null && nextPhoto.getHeight() >= nextPhoto.getWidth()) {
+ View twoColumnView = inflater.inflate(R.layout.stream_item_pair,
+ contentBox, false);
+ addStreamItemPhoto(inflater, context, photo,
+ (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_first));
+ addStreamItemPhoto(inflater, context, nextPhoto,
+ (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_second));
+ twoColumnView.setPadding(0, internalPadding, 0, 0);
+ contentBox.addView(twoColumnView);
+ i++;
+ } else {
+ View photoView = addStreamItemPhoto(inflater, context, photo, contentBox);
+ photoView.setPadding(0, internalPadding, 0, 0);
+ }
+ }
+ }
+ }
+
+ streamContainer.addView(oneColumnView);
+ }
+
+ private static View addStreamItemText(LayoutInflater inflater, Context context,
+ StreamItemEntry streamItem, ViewGroup parent) {
+ View textUpdate = inflater.inflate(R.layout.stream_item_text, parent, false);
+ TextView htmlView = (TextView) textUpdate.findViewById(R.id.stream_item_html);
+ TextView attributionView = (TextView) textUpdate.findViewById(
+ R.id.stream_item_attribution);
+ htmlView.setText(Html.fromHtml(streamItem.getText()));
+ attributionView.setText(ContactBadgeUtil.getSocialDate(streamItem, context));
+ parent.addView(textUpdate);
+ return textUpdate;
+ }
+
+ private static View addStreamItemPhoto(LayoutInflater inflater, Context context,
+ StreamItemPhotoEntry streamItemPhoto, ViewGroup parent) {
+ ImageView image = new ImageView(context);
+ ContactPhotoManager.getInstance(context).loadPhoto(
+ image, Uri.parse(streamItemPhoto.getPhotoUri()));
+ parent.addView(image);
+ return image;
}
/**
diff --git a/src/com/android/contacts/detail/ContactDetailHeaderView.java b/src/com/android/contacts/detail/ContactDetailHeaderView.java
deleted file mode 100644
index 63f8fbe..0000000
--- a/src/com/android/contacts/detail/ContactDetailHeaderView.java
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * 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.detail;
-
-import com.android.contacts.ContactLoader;
-import com.android.contacts.ContactLoader.Result;
-import com.android.contacts.ContactSaveService;
-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.ClipData;
-import android.content.ClipboardManager;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Typeface;
-import android.net.Uri;
-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.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.AlphaAnimation;
-import android.widget.CheckBox;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-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 {
- private static final String TAG = "ContactDetailHeaderView";
-
- private static final int PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS = 100;
-
- private TextView mDisplayNameView;
- private TextView mPhoneticNameView;
- private TextView mOrganizationTextView;
- private CheckBox mStarredView;
- private ImageView mPhotoView;
- private View mStatusContainerView;
- private TextView mStatusView;
- private TextView mStatusDateView;
- private TextView mAttributionView;
-
- private Uri mContactUri;
- private Listener mListener;
-
- /**
- * Interface for callbacks invoked when the user interacts with a header.
- */
- public interface Listener {
- public void onPhotoClick(View view);
- public void onDisplayNameClick(View view);
- }
-
- public ContactDetailHeaderView(Context context) {
- this(context, null);
- }
-
- public ContactDetailHeaderView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ContactDetailHeaderView(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_header_view, this);
-
- mDisplayNameView = (TextView) findViewById(R.id.name);
- mDisplayNameView.setOnLongClickListener(this);
-
- mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
- mPhoneticNameView.setOnLongClickListener(this);
-
- mOrganizationTextView = (TextView) findViewById(R.id.organization);
- mOrganizationTextView.setOnLongClickListener(this);
-
- mStarredView = (CheckBox)findViewById(R.id.star);
- mStarredView.setOnClickListener(this);
-
- mPhotoView = (ImageView) findViewById(R.id.photo);
-
- mStatusContainerView = findViewById(R.id.status_container);
- mStatusView = (TextView)findViewById(R.id.status);
- mStatusDateView = (TextView)findViewById(R.id.status_date);
-
- mAttributionView = (TextView) findViewById(R.id.attribution);
- }
-
- /**
- * 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();
-
- setDisplayName(contactData.getDisplayName(), contactData.getAltDisplayName(),
- contactData.getPhoneticName());
- setCompany(contactData);
- if (contactData.isLoadingPhoto()) {
- setPhoto(null, false);
- } else {
- byte[] photo = contactData.getPhotoBinaryData();
- setPhoto(photo != null ? BitmapFactory.decodeByteArray(photo, 0, photo.length)
- : ContactBadgeUtil.loadPlaceholderPhoto(mContext),
- contactData.isDirectoryEntry());
- }
-
- setStared(!contactData.isDirectoryEntry(), contactData.getStarred());
- setSocialSnippet(contactData.getSocialSnippet());
- setSocialDate(ContactBadgeUtil.getSocialDate(contactData, getContext()));
- setAttribution(contactData.getEntities().size() > 1, contactData.isDirectoryEntry(),
- contactData.getDirectoryDisplayName(), contactData.getDirectoryType());
- }
-
- /**
- * Set the given {@link Listener} to handle header events.
- */
- public void setListener(Listener listener) {
- mListener = listener;
- }
-
- private void performPhotoClick() {
- if (mListener != null) {
- mListener.onPhotoClick(mPhotoView);
- }
- }
-
- private void performDisplayNameClick() {
- if (mListener != null) {
- mListener.onDisplayNameClick(mDisplayNameView);
- }
- }
-
- /**
- * Set the starred state of this header widget.
- */
- private void setStared(boolean visible, boolean starred) {
- if (visible) {
- mStarredView.setVisibility(View.VISIBLE);
- mStarredView.setChecked(starred);
- } else {
- mStarredView.setVisibility(View.GONE);
- }
- }
-
- /**
- * Set the photo to display in the header. If bitmap is null, the default placeholder
- * image is shown
- */
- private void setPhoto(Bitmap bitmap, boolean fadeIn) {
- if (mPhotoView.getDrawable() == null && fadeIn) {
- AlphaAnimation animation = new AlphaAnimation(0, 1);
- animation.setDuration(PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS);
- animation.setInterpolator(new AccelerateInterpolator());
- mPhotoView.startAnimation(animation);
- }
- mPhotoView.setImageBitmap(bitmap);
- }
-
- /**
- * Set the display name and phonetic name to show in the header.
- */
- private void setDisplayName(CharSequence displayName, CharSequence altDisplayName,
- CharSequence phoneticName) {
-
- // Check the preference for display name ordering, and bold the contact's first name if
- // possible.
- ContactsPreferences prefs = new ContactsPreferences(getContext());
- 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;
- }
- }
- }
- mDisplayNameView.setText(styledName);
-
- if (TextUtils.isEmpty(phoneticName)) {
- mPhoneticNameView.setVisibility(View.GONE);
- } else {
- mPhoneticNameView.setText(phoneticName);
- mPhoneticNameView.setVisibility(View.VISIBLE);
- }
- }
-
- /**
- * Sets the organization info. If several organizations are given, the first one is used
- */
- private void setCompany(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 = getResources().getString(
- R.string.organization_company_and_title,
- company, title);
- }
- }
- }
-
- if (TextUtils.isEmpty(combined)) {
- mOrganizationTextView.setVisibility(GONE);
- } else {
- mOrganizationTextView.setVisibility(VISIBLE);
- mOrganizationTextView.setText(combined);
- }
-
- return;
- }
- }
- }
- mOrganizationTextView.setVisibility(GONE);
- }
-
- /**
- * Set the social snippet text to display in the header.
- */
- private void setSocialSnippet(CharSequence snippet) {
- if (TextUtils.isEmpty(snippet)) {
- // No status info. Hide everything
- if (mStatusContainerView != null) mStatusContainerView.setVisibility(View.GONE);
- mStatusView.setVisibility(View.GONE);
- mStatusDateView.setVisibility(View.GONE);
- } else {
- // We have status info. Show the bubble
- if (mStatusContainerView != null) mStatusContainerView.setVisibility(View.VISIBLE);
- mStatusView.setVisibility(View.VISIBLE);
- mStatusView.setText(snippet);
- }
- }
-
- /**
- * Set the status attribution text to display in the header.
- */
-
- private void setSocialDate(CharSequence dateText) {
- if (TextUtils.isEmpty(dateText)) {
- mStatusDateView.setVisibility(View.GONE);
- } else {
- mStatusDateView.setText(dateText);
- mStatusDateView.setVisibility(View.VISIBLE);
- }
- }
-
- private void setAttribution(boolean isJoinedContact, boolean isDirectoryEntry,
- String directoryDisplayName, String directoryType) {
- if (isJoinedContact) {
- mAttributionView.setText(R.string.indicator_joined_contact);
- mAttributionView.setVisibility(View.VISIBLE);
- } else if (isDirectoryEntry) {
- String displayName = !TextUtils.isEmpty(directoryDisplayName)
- ? directoryDisplayName
- : directoryType;
- String text = getContext().getString(
- R.string.contact_directory_description, displayName);
- mAttributionView.setText(text);
- mAttributionView.setVisibility(View.VISIBLE);
- } else {
- mAttributionView.setVisibility(View.INVISIBLE);
- }
- }
-
- @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;
- }
- case R.id.photo: {
- performPhotoClick();
- break;
- }
- case R.id.name: {
- performDisplayNameClick();
- break;
- }
- }
- }
-
- @Override
- public boolean onLongClick(View v) {
- if (!(v instanceof TextView)) {
- return false;
- }
-
- CharSequence text = ((TextView)v).getText();
-
- if (TextUtils.isEmpty(text)) {
- return false;
- }
-
- ClipboardManager cm = (ClipboardManager) getContext().getSystemService(
- Context.CLIPBOARD_SERVICE);
- cm.setPrimaryClip(ClipData.newPlainText(null, text));
- Toast.makeText(getContext(), R.string.toast_text_copied, Toast.LENGTH_SHORT).show();
- return true;
- }
-}
diff --git a/src/com/android/contacts/detail/ContactDetailLayoutController.java b/src/com/android/contacts/detail/ContactDetailLayoutController.java
index dda3884..d93edea 100644
--- a/src/com/android/contacts/detail/ContactDetailLayoutController.java
+++ b/src/com/android/contacts/detail/ContactDetailLayoutController.java
@@ -18,6 +18,7 @@
import com.android.contacts.ContactLoader;
import com.android.contacts.activities.PeopleActivity.ContactDetailFragmentListener;
+import com.android.contacts.util.StreamItemEntry;
import android.app.Fragment;
import android.app.FragmentManager;
@@ -124,7 +125,7 @@
public void setContactData(ContactLoader.Result data) {
mContactData = data;
- if (mContactData.getSocialSnippet() != null) {
+ if (!data.getStreamItems().isEmpty()) {
showContactWithUpdates();
} else {
showContactWithoutUpdates();
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 6e1b199..26987f6 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -45,6 +45,7 @@
private ImageView mPhotoView;
private TextView mStatusView;
+ private ImageView mStatusPhotoView;
private Listener mListener;
@@ -115,6 +116,7 @@
// Retrieve the social update views for the "updates" tab
mStatusView = (TextView) updateView.findViewById(R.id.status);
+ mStatusPhotoView = (ImageView) updateView.findViewById(R.id.status_photo);
}
@Override
@@ -196,7 +198,8 @@
}
ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView);
- ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView);
+ ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView,
+ mStatusPhotoView);
}
/**
diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
index 85dcc1d..daeae00 100644
--- a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
@@ -27,6 +27,7 @@
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
import android.widget.TextView;
public class ContactDetailUpdatesFragment extends Fragment
@@ -37,8 +38,10 @@
private ContactLoader.Result mContactData;
private Uri mLookupUri;
- private TextView mStatusView;
- private TextView mStatusDateView;
+ private LayoutInflater mInflater;
+
+ // The linear layout that contains all the stream items.
+ private LinearLayout mStreamContainer;
/**
* This optional view adds an alpha layer over the entire fragment.
@@ -57,22 +60,22 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
- View rootView = inflater.inflate(R.layout.contact_detail_updates_fragment, container,
+ mInflater = inflater;
+ View rootView = mInflater.inflate(R.layout.contact_detail_updates_fragment, container,
false);
TextView titleTextView = (TextView) rootView.findViewById(R.id.kind);
titleTextView.setText(getString(R.string.recent_updates).toUpperCase());
- mStatusView = (TextView) rootView.findViewById(R.id.status);
- mStatusDateView = (TextView) rootView.findViewById(R.id.status_date);
+ mStreamContainer = (LinearLayout) rootView.findViewById(R.id.update_list);
// It is possible that the contact data was set to the fragment when it was first attached
// to the activity, but before this method was called because the fragment was not
// visible on screen yet (i.e. using a {@link ViewPager}), so display the data if we already
// have it.
if (mContactData != null) {
- ContactDetailDisplayUtils.setSocialSnippetAndDate(getActivity(), mContactData,
- mStatusView, mStatusDateView);
+ ContactDetailDisplayUtils.showSocialStreamItems(inflater, getActivity(), mContactData,
+ mStreamContainer);
}
mAlphaLayer = rootView.findViewById(R.id.alpha_overlay);
@@ -87,8 +90,8 @@
}
mLookupUri = lookupUri;
mContactData = result;
- ContactDetailDisplayUtils.setSocialSnippetAndDate(getActivity(), mContactData,
- mStatusView, mStatusDateView);
+ ContactDetailDisplayUtils.showSocialStreamItems(mInflater, getActivity(), mContactData,
+ mStreamContainer);
}
@Override
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
index 0dc83ef..034a8cc 100644
--- a/src/com/android/contacts/detail/ContactLoaderFragment.java
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -170,7 +170,8 @@
@Override
public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
- return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */);
+ return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */,
+ true /* loadStreamItems */);
}
@Override
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
index 92118c0..d59aebd 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
@@ -20,6 +20,7 @@
import com.android.contacts.R;
import com.android.contacts.util.ContactBadgeUtil;
import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.StreamItemEntry;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
@@ -44,6 +45,7 @@
import android.widget.RemoteViews;
import java.util.HashMap;
+import java.util.List;
public class SocialWidgetProvider extends AppWidgetProvider {
private static final String TAG = "SocialWidgetProvider";
@@ -109,7 +111,7 @@
// Not yet set-up (this can happen while the Configuration activity is visible)
return;
}
- final ContactLoader contactLoader = new ContactLoader(context, contactUri);
+ final ContactLoader contactLoader = new ContactLoader(context, contactUri, false, true);
contactLoader.registerListener(0,
new ContactLoader.OnLoadCompleteListener<ContactLoader.Result>() {
@Override
@@ -140,8 +142,13 @@
setPhoto(views, photo != null
? BitmapFactory.decodeByteArray(photo, 0, photo.length)
: ContactBadgeUtil.loadPlaceholderPhoto(context));
- setStatusAttribution(views, ContactBadgeUtil.getSocialDate(
- contactData, context));
+
+ // TODO: Rotate between all the stream items?
+ StreamItemEntry streamItem = null;
+ if (!contactData.getStreamItems().isEmpty()) {
+ streamItem = contactData.getStreamItems().get(0);
+ setStatusAttribution(views, ContactBadgeUtil.getSocialDate(streamItem, context));
+ }
// OnClick launch QuickContact
final Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
@@ -157,7 +164,7 @@
views.setOnClickPendingIntent(R.id.border, pendingIntent);
setDisplayNameAndSnippet(context, views, contactData.getDisplayName(),
- contactData.getPhoneticName(), contactData.getStatuses(), pendingIntent);
+ contactData.getPhoneticName(), contactData.getStreamItems(), pendingIntent);
}
// Configure UI
@@ -174,7 +181,7 @@
*/
private static void setDisplayNameAndSnippet(Context context, RemoteViews views,
CharSequence displayName, CharSequence phoneticName,
- HashMap<Long, DataStatus> statuses, PendingIntent defaultIntent) {
+ List<StreamItemEntry> streamItems, PendingIntent defaultIntent) {
SpannableStringBuilder sb = new SpannableStringBuilder();
CharSequence name = displayName;
@@ -190,27 +197,15 @@
sb.setSpan(sizeSpan, 0, name.length(), 0);
sb.setSpan(styleSpan, 0, name.length(), 0);
- long latestStatusId = 0;
- DataStatus latestStatus = null;
- if (statuses != null) {
- for (HashMap.Entry<Long, DataStatus> entry : statuses.entrySet()) {
- DataStatus status = entry.getValue();
- if (!TextUtils.isEmpty(status.getStatus())
- && (latestStatus == null
- || latestStatus.getTimestamp() < status.getTimestamp())) {
- latestStatusId = entry.getKey();
- latestStatus = status;
- }
- }
- }
-
- if (latestStatus == null) {
+ if (streamItems.isEmpty()) {
views.setTextViewText(R.id.name, sb);
views.setViewVisibility(R.id.name, View.VISIBLE);
views.setViewVisibility(R.id.name_and_snippet, View.GONE);
views.setOnClickPendingIntent(R.id.widget_container, defaultIntent);
} else {
- CharSequence status = latestStatus.getStatus();
+ // TODO: Rotate between all the stream items?
+ StreamItemEntry streamItem = streamItems.get(0);
+ CharSequence status = streamItem.getText();
if (status.length() <= SHORT_SNIPPET_LENGTH) {
sb.append("\n");
} else {
@@ -220,11 +215,13 @@
views.setTextViewText(R.id.name_and_snippet, sb);
views.setViewVisibility(R.id.name, View.GONE);
views.setViewVisibility(R.id.name_and_snippet, View.VISIBLE);
- final Intent intent = new Intent(Intent.ACTION_VIEW,
- ContentUris.withAppendedId(Data.CONTENT_URI, latestStatusId));
-
- views.setOnClickPendingIntent(R.id.name_and_snippet_container,
- PendingIntent.getActivity(context, 0, intent, 0));
+ if (!TextUtils.isEmpty(streamItem.getAction())
+ && !TextUtils.isEmpty(streamItem.getActionUri())) {
+ final Intent intent = new Intent(streamItem.getAction(),
+ Uri.parse(streamItem.getActionUri()));
+ views.setOnClickPendingIntent(R.id.name_and_snippet_container,
+ PendingIntent.getActivity(context, 0, intent, 0));
+ }
}
}
diff --git a/src/com/android/contacts/util/ContactBadgeUtil.java b/src/com/android/contacts/util/ContactBadgeUtil.java
index 65025f8..2f37b4c 100644
--- a/src/com/android/contacts/util/ContactBadgeUtil.java
+++ b/src/com/android/contacts/util/ContactBadgeUtil.java
@@ -37,17 +37,11 @@
private static final String TAG = "ContactBadgeUtil";
/**
- * Returns the social snippet attribution, including the date
+ * Returns the social snippet attribution for the given stream item entry, including the date.
*/
- public static CharSequence getSocialDate(ContactLoader.Result contactData,
- Context context) {
- if (TextUtils.isEmpty(contactData.getSocialSnippet())) {
- return null;
- }
-
+ public static CharSequence getSocialDate(StreamItemEntry streamItem, Context context) {
final CharSequence timestampDisplayValue;
-
- final Long statusTimestamp = contactData.getStatusTimestamp();
+ final Long statusTimestamp = streamItem.getTimestamp();
if (statusTimestamp != null) {
// Set the date/time field by mixing relative and absolute
// times.
@@ -63,8 +57,8 @@
String labelDisplayValue = null;
- final Integer statusLabel = contactData.getStatusLabel();
- final String statusResPackage = contactData.getStatusResPackage();
+ final Integer statusLabel = streamItem.getLabelRes();
+ final String statusResPackage = streamItem.getResPackage();
if (statusLabel != null) {
Resources resources;
if (TextUtils.isEmpty(statusResPackage)) {
diff --git a/src/com/android/contacts/util/StreamItemEntry.java b/src/com/android/contacts/util/StreamItemEntry.java
new file mode 100644
index 0000000..dc54229
--- /dev/null
+++ b/src/com/android/contacts/util/StreamItemEntry.java
@@ -0,0 +1,141 @@
+/*
+ * 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.util;
+
+import android.database.Cursor;
+import android.provider.ContactsContract.StreamItems;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Data object for a social stream item. Social stream items may contain multiple
+ * mPhotos. Social stream item entries are comparable; entries with more recent
+ * timestamps will be displayed on top.
+ */
+public class StreamItemEntry implements Comparable<StreamItemEntry> {
+
+ // Basic stream item fields.
+ private final long mId;
+ private final String mText;
+ private final String mComments;
+ private final long mTimestamp;
+ private final String mAction;
+ private final String mActionUri;
+
+ // Package references for label and icon resources.
+ private final String mResPackage;
+ private final int mIconRes;
+ private final int mLabelRes;
+
+ // Photos associated with this stream item.
+ private List<StreamItemPhotoEntry> mPhotos;
+
+ public StreamItemEntry(long id, String text, String comments, long timestamp, String action,
+ String actionUri, String resPackage, int iconRes, int labelRes) {
+ mId = id;
+ mText = text;
+ mComments = comments;
+ mTimestamp = timestamp;
+ mAction = action;
+ mActionUri = actionUri;
+ mResPackage = resPackage;
+ mIconRes = iconRes;
+ mLabelRes = labelRes;
+ mPhotos = new ArrayList<StreamItemPhotoEntry>();
+ }
+
+ public StreamItemEntry(Cursor cursor) {
+ // This is expected to be populated via a cursor containing all StreamItems columns in
+ // its projection.
+ mId = getLong(cursor, StreamItems._ID);
+ mText = getString(cursor, StreamItems.TEXT);
+ mComments = getString(cursor, StreamItems.COMMENTS);
+ mTimestamp = getLong(cursor, StreamItems.TIMESTAMP);
+ mAction = getString(cursor, StreamItems.ACTION);
+ mActionUri = getString(cursor, StreamItems.ACTION_URI);
+ mResPackage = getString(cursor, StreamItems.RES_PACKAGE);
+ mIconRes = getInt(cursor, StreamItems.RES_ICON, -1);
+ mLabelRes = getInt(cursor, StreamItems.RES_LABEL, -1);
+ mPhotos = new ArrayList<StreamItemPhotoEntry>();
+ }
+
+ public void addPhoto(StreamItemPhotoEntry photoEntry) {
+ mPhotos.add(photoEntry);
+ }
+
+ @Override
+ public int compareTo(StreamItemEntry other) {
+ return mTimestamp == other.mTimestamp ? 0 : mTimestamp > other.mTimestamp ? -1 : 1;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ public String getComments() {
+ return mComments;
+ }
+
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ public String getAction() {
+ return mAction;
+ }
+
+ public String getActionUri() {
+ return mActionUri;
+ }
+
+ public String getResPackage() {
+ return mResPackage;
+ }
+
+ public int getIconRes() {
+ return mIconRes;
+ }
+
+ public int getLabelRes() {
+ return mLabelRes;
+ }
+
+ public List<StreamItemPhotoEntry> getPhotos() {
+ Collections.sort(mPhotos);
+ return mPhotos;
+ }
+
+ private static String getString(Cursor cursor, String columnName) {
+ return cursor.getString(cursor.getColumnIndex(columnName));
+ }
+
+ private static int getInt(Cursor cursor, String columnName, int missingValue) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+ }
+
+ private static long getLong(Cursor cursor, String columnName) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return cursor.getLong(columnIndex);
+ }
+}
diff --git a/src/com/android/contacts/util/StreamItemPhotoEntry.java b/src/com/android/contacts/util/StreamItemPhotoEntry.java
new file mode 100644
index 0000000..6527454
--- /dev/null
+++ b/src/com/android/contacts/util/StreamItemPhotoEntry.java
@@ -0,0 +1,139 @@
+/*
+ * 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.util;
+
+import android.database.Cursor;
+import android.provider.ContactsContract.PhotoFiles;
+import android.provider.ContactsContract.StreamItemPhotos;
+
+/**
+ * Data object for a photo associated with a social stream item. These are comparable;
+ * entries with a lower sort index will be displayed on top (with the ID used as a
+ * tie-breaker).
+ */
+public class StreamItemPhotoEntry implements Comparable<StreamItemPhotoEntry> {
+ private final long mId;
+ private final int mSortIndex;
+ private final long mPhotoFileId;
+ private final String mPhotoUri;
+ private final int mHeight;
+ private final int mWidth;
+ private final int mFileSize;
+ private final String mAction;
+ private final String mActionUri;
+
+ public StreamItemPhotoEntry(long id, int sortIndex, long photoFileId, String photoUri,
+ int height, int width, int fileSize, String action, String actionUri) {
+ mId = id;
+ mSortIndex = sortIndex;
+ mPhotoFileId = photoFileId;
+ mPhotoUri = photoUri;
+ mHeight = height;
+ mWidth = width;
+ mFileSize = fileSize;
+ mAction = action;
+ mActionUri = actionUri;
+ }
+
+ public StreamItemPhotoEntry(Cursor cursor) {
+ // This is expected to be populated via a cursor containing a join of all
+ // StreamItemPhotos columns and all PhotoFiles columns (except for ID).
+ mId = getLong(cursor, StreamItemPhotos._ID);
+ mSortIndex = getInt(cursor, StreamItemPhotos.SORT_INDEX, -1);
+ mPhotoFileId = getLong(cursor, StreamItemPhotos.PHOTO_FILE_ID);
+ mPhotoUri = getString(cursor, StreamItemPhotos.PHOTO_URI);
+ mHeight = getInt(cursor, PhotoFiles.HEIGHT, -1);
+ mWidth = getInt(cursor, PhotoFiles.WIDTH, -1);
+ mFileSize = getInt(cursor, PhotoFiles.FILESIZE, -1);
+ mAction = getString(cursor, StreamItemPhotos.ACTION);
+ mActionUri = getString(cursor, StreamItemPhotos.ACTION_URI);
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public int getSortIndex() {
+ return mSortIndex;
+ }
+
+ public long getPhotoFileId() {
+ return mPhotoFileId;
+ }
+
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getFileSize() {
+ return mFileSize;
+ }
+
+ public String getAction() {
+ return mAction;
+ }
+
+ public String getActionUri() {
+ return mActionUri;
+ }
+
+ @Override
+ public int compareTo(StreamItemPhotoEntry streamItemPhotoEntry) {
+ // Sort index is used to compare, falling back to ID if neither entry has a
+ // sort index specified (entries without a sort index are sorted after entries
+ // that have one).
+ if (mSortIndex == streamItemPhotoEntry.mSortIndex) {
+ if (mSortIndex == -1) {
+ return mId == streamItemPhotoEntry.mId ? 0
+ : mId < streamItemPhotoEntry.mId ? -1 : 1;
+ } else {
+ return 0;
+ }
+ } else {
+ if (mSortIndex == -1) {
+ return 1;
+ }
+ if (streamItemPhotoEntry.mSortIndex == -1) {
+ return -1;
+ }
+ return mSortIndex == streamItemPhotoEntry.mSortIndex ? 0
+ : mSortIndex < streamItemPhotoEntry.mSortIndex ? -1 : 1;
+ }
+ }
+
+ private static String getString(Cursor cursor, String columnName) {
+ return cursor.getString(cursor.getColumnIndex(columnName));
+ }
+
+ private static int getInt(Cursor cursor, String columnName, int missingValue) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+ }
+
+ private static long getLong(Cursor cursor, String columnName) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return cursor.getLong(columnIndex);
+ }
+}