Stream items UI.
This is still pretty rough at this point, and does not fully implement
the UI. It handles loading in the stream items and photo metadata in
the contact loader, and displaying those items in a scrollable view
in the updates pane.
Change-Id: I3e796a6141ffa385aa2acc769cf6dd11f37aa39c
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 0d868e9..71eb9ac 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 b90898d..99a1561 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);
+ }
+}