Adds Recent Interaction loading to QuickContacts.
This is launched after the Contact is loaded from CP2, during the bind stage.
Change-Id: I63290e0e94c476da1771f6e8b92a9c664f2fe9d3
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d077dd8..a87949a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -50,6 +50,7 @@
<!-- allow broadcasting secret code intents that reboot the phone -->
<uses-permission android:name="android.permission.REBOOT" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.READ_SMS" />
<application
android:name="com.android.contacts.ContactsApplication"
diff --git a/res/drawable-hdpi/ic_message_24dp.png b/res/drawable-hdpi/ic_message_24dp.png
new file mode 100644
index 0000000..48f008a
--- /dev/null
+++ b/res/drawable-hdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_message_24dp.png b/res/drawable-mdpi/ic_message_24dp.png
new file mode 100644
index 0000000..c18f225
--- /dev/null
+++ b/res/drawable-mdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_message_24dp.png b/res/drawable-xhdpi/ic_message_24dp.png
new file mode 100644
index 0000000..ee5021c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_message_24dp.png b/res/drawable-xxhdpi/ic_message_24dp.png
new file mode 100644
index 0000000..3b74d32
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/layout-land/quickcontact_activity.xml b/res/layout-land/quickcontact_activity.xml
index 43d26db..497e3dd 100644
--- a/res/layout-land/quickcontact_activity.xml
+++ b/res/layout-land/quickcontact_activity.xml
@@ -32,5 +32,10 @@
<com.android.contacts.quickcontact.ExpandingEntryCardView
style="@style/ExpandingEntryCardStyle"
android:id="@+id/communication_card"
- android:layout_marginTop="@dimen/communication_card_marginTop" />
+ android:layout_marginTop="@dimen/communication_card_marginTop"
+ android:visibility="gone" />
+ <com.android.contacts.quickcontact.ExpandingEntryCardView
+ style="@style/ExpandingEntryCardStyle"
+ android:id="@+id/recent_card"
+ android:visibility="gone" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw600dp-land/quickcontact_activity.xml b/res/layout-sw600dp-land/quickcontact_activity.xml
index fc2aee2..239c50c 100644
--- a/res/layout-sw600dp-land/quickcontact_activity.xml
+++ b/res/layout-sw600dp-land/quickcontact_activity.xml
@@ -28,5 +28,10 @@
<com.android.contacts.quickcontact.ExpandingEntryCardView
style="@style/ExpandingEntryCardStyle"
android:id="@+id/communication_card"
- android:layout_marginTop="@dimen/communication_card_marginTop" />
+ android:layout_marginTop="@dimen/communication_card_marginTop"
+ android:visibility="gone" />
+ <com.android.contacts.quickcontact.ExpandingEntryCardView
+ style="@style/ExpandingEntryCardStyle"
+ android:id="@+id/recent_card"
+ android:visibility="gone" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw600dp/quickcontact_activity.xml b/res/layout-sw600dp/quickcontact_activity.xml
index 88efc46..6c275b1 100644
--- a/res/layout-sw600dp/quickcontact_activity.xml
+++ b/res/layout-sw600dp/quickcontact_activity.xml
@@ -28,5 +28,10 @@
<com.android.contacts.quickcontact.ExpandingEntryCardView
style="@style/ExpandingEntryCardStyle"
android:id="@+id/communication_card"
- android:layout_marginTop="@dimen/communication_card_marginTop" />
+ android:layout_marginTop="@dimen/communication_card_marginTop"
+ android:visibility="gone" />
+ <com.android.contacts.quickcontact.ExpandingEntryCardView
+ style="@style/ExpandingEntryCardStyle"
+ android:id="@+id/recent_card"
+ android:visibility="gone" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw720dp-land/quickcontact_activity.xml b/res/layout-sw720dp-land/quickcontact_activity.xml
index fc2aee2..239c50c 100644
--- a/res/layout-sw720dp-land/quickcontact_activity.xml
+++ b/res/layout-sw720dp-land/quickcontact_activity.xml
@@ -28,5 +28,10 @@
<com.android.contacts.quickcontact.ExpandingEntryCardView
style="@style/ExpandingEntryCardStyle"
android:id="@+id/communication_card"
- android:layout_marginTop="@dimen/communication_card_marginTop" />
+ android:layout_marginTop="@dimen/communication_card_marginTop"
+ android:visibility="gone" />
+ <com.android.contacts.quickcontact.ExpandingEntryCardView
+ style="@style/ExpandingEntryCardStyle"
+ android:id="@+id/recent_card"
+ android:visibility="gone" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/quickcontact_activity.xml b/res/layout/quickcontact_activity.xml
index 573890f..9af1079 100644
--- a/res/layout/quickcontact_activity.xml
+++ b/res/layout/quickcontact_activity.xml
@@ -50,9 +50,15 @@
<com.android.contacts.quickcontact.ExpandingEntryCardView
style="@style/ExpandingEntryCardStyle"
android:id="@+id/communication_card"
- android:layout_marginTop="@dimen/communication_card_marginTop" />
+ android:layout_marginTop="@dimen/communication_card_marginTop"
+ android:visibility="gone" />
+
+ <com.android.contacts.quickcontact.ExpandingEntryCardView
+ style="@style/ExpandingEntryCardStyle"
+ android:id="@+id/recent_card"
+ android:visibility="gone" />
</LinearLayout>
</com.android.contacts.widget.TouchlessScrollView>
-</com.android.contacts.widget.MultiShrinkScroller>
+</com.android.contacts.widget.MultiShrinkScroller>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 06e52cf..84f3247 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -649,4 +649,11 @@
<!-- Title of communication card. [CHAR LIMIT=60] -->
<string name="communication_card_title">Contact</string>
+ <!-- Title of recent card. [CHAR LIMIT=60] -->
+ <string name="recent_card_title">Recent</string>
+
+ <!-- Timestamp string for interactions from yesterday. [CHAR LIMIT=40] -->
+ <string name="timestamp_string_yesterday">Yesterday</string>
+ <!-- Timestamp string for interactions from tomorrow. [CHAR LIMIT=40] -->
+ <string name="timestamp_string_tomorrow">Tomorrow</string>
</resources>
diff --git a/src/com/android/contacts/interactions/ContactInteraction.java b/src/com/android/contacts/interactions/ContactInteraction.java
new file mode 100644
index 0000000..a70a0a8
--- /dev/null
+++ b/src/com/android/contacts/interactions/ContactInteraction.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 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.interactions;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+/**
+ * Represents a default interaction between the phone's owner and a contact
+ */
+public interface ContactInteraction {
+ Intent getIntent();
+ String getViewDate(Context context);
+ long getInteractionDate();
+ String getViewHeader(Context context);
+ String getViewBody(Context context);
+ String getViewFooter(Context context);
+ Drawable getIcon(Context context);
+ Drawable getBodyIcon(Context context);
+ Drawable getFooterIcon(Context context);
+}
diff --git a/src/com/android/contacts/interactions/ContactInteractionUtil.java b/src/com/android/contacts/interactions/ContactInteractionUtil.java
new file mode 100644
index 0000000..453a5bd
--- /dev/null
+++ b/src/com/android/contacts/interactions/ContactInteractionUtil.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 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.interactions;
+
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import com.android.contacts.common.testing.NeededForTesting;
+
+import java.text.DateFormat;
+
+import java.util.Calendar;
+
+import com.android.contacts.common.R;
+
+
+/**
+ * Utility methods for interactions and their loaders
+ */
+public class ContactInteractionUtil {
+
+ /**
+ * @return a string like (?,?,?...) with {@param count} question marks.
+ */
+ @NeededForTesting
+ public static String questionMarks(int count) {
+ Preconditions.checkArgument(count > 0);
+ StringBuilder sb = new StringBuilder("(?");
+ for (int i = 1; i < count; i++) {
+ sb.append(",?");
+ }
+ return sb.append(")").toString();
+ }
+
+ /**
+ * Same as {@link formatDateStringFromTimestamp(long, Context, Calendar)} but uses the current
+ * time.
+ */
+ @NeededForTesting
+ public static String formatDateStringFromTimestamp(long timestamp, Context context) {
+ return formatDateStringFromTimestamp(timestamp, context, Calendar.getInstance());
+ }
+
+ /**
+ * Takes in a timestamp and outputs a human legible date. This checks the timestamp against
+ * compareCalendar.
+ * This formats the date based on a few conditions:
+ * 1. If the timestamp is today, the time is shown
+ * 2. If the timestamp occurs tomorrow or yesterday, that is displayed
+ * 3. Otherwise {Month Date} format is used
+ */
+ @NeededForTesting
+ public static String formatDateStringFromTimestamp(long timestamp, Context context,
+ Calendar compareCalendar) {
+ Calendar interactionCalendar = Calendar.getInstance();
+ interactionCalendar.setTimeInMillis(timestamp);
+
+ // compareCalendar is initialized to today
+ if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
+ return DateFormat.getTimeInstance(DateFormat.SHORT).format(
+ interactionCalendar.getTime());
+ }
+
+ // Turn compareCalendar to yesterday
+ compareCalendar.add(Calendar.DAY_OF_YEAR, -1);
+ if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
+ return context.getString(R.string.timestamp_string_yesterday);
+ }
+
+ // Turn compareCalendar to tomorrow
+ compareCalendar.add(Calendar.DAY_OF_YEAR, 2);
+ if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
+ return context.getString(R.string.timestamp_string_tomorrow);
+ }
+ return DateUtils.formatDateTime(context, interactionCalendar.getTimeInMillis(),
+ DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR);
+ }
+
+ /**
+ * Compares the day and year of two calendars.
+ */
+ private static boolean compareCalendarDayYear(Calendar c1, Calendar c2) {
+ return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) &&
+ c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR);
+ }
+}
diff --git a/src/com/android/contacts/interactions/SmsInteraction.java b/src/com/android/contacts/interactions/SmsInteraction.java
new file mode 100644
index 0000000..e922056
--- /dev/null
+++ b/src/com/android/contacts/interactions/SmsInteraction.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2014 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.interactions;
+
+import com.android.contacts.R;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.Telephony.Sms;
+
+/**
+ * Represents an sms interaction, wrapping the columns in
+ * {@link android.provider.Telephony.Sms}.
+ */
+public class SmsInteraction implements ContactInteraction {
+
+ private static final String URI_TARGET_PREFIX = "smsto:";
+ private static final int SMS_ICON_RES = R.drawable.ic_message_24dp;
+
+ private ContentValues mValues;
+
+ public SmsInteraction(ContentValues values) {
+ mValues = values;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return new Intent(Intent.ACTION_VIEW).setData(Uri.parse(URI_TARGET_PREFIX + getAddress()));
+ }
+
+ @Override
+ public String getViewDate(Context context) {
+ return ContactInteractionUtil.formatDateStringFromTimestamp(getDate(), context);
+ }
+
+ @Override
+ public long getInteractionDate() {
+ return getDate();
+ }
+
+ @Override
+ public String getViewHeader(Context context) {
+ return getBody();
+ }
+
+ @Override
+ public String getViewBody(Context context) {
+ return getAddress();
+ }
+
+ @Override
+ public String getViewFooter(Context context) {
+ return getViewDate(context);
+ }
+
+ @Override
+ public Drawable getIcon(Context context) {
+ return context.getResources().getDrawable(SMS_ICON_RES);
+ }
+
+ @Override
+ public Drawable getBodyIcon(Context context) {
+ return null;
+ }
+
+ @Override
+ public Drawable getFooterIcon(Context context) {
+ return null;
+ }
+
+ public String getAddress() {
+ return mValues.getAsString(Sms.ADDRESS);
+ }
+
+ public String getBody() {
+ return mValues.getAsString(Sms.BODY);
+ }
+
+ public long getDate() {
+ return mValues.getAsLong(Sms.DATE);
+ }
+
+
+ public long getDateSent() {
+ return mValues.getAsLong(Sms.DATE_SENT);
+ }
+
+ public int getErrorCode() {
+ return mValues.getAsInteger(Sms.ERROR_CODE);
+ }
+
+ public boolean getLocked() {
+ return mValues.getAsBoolean(Sms.LOCKED);
+ }
+
+ public int getPerson() {
+ return mValues.getAsInteger(Sms.PERSON);
+ }
+
+ public int getProtocol() {
+ return mValues.getAsInteger(Sms.PROTOCOL);
+ }
+
+ public boolean getRead() {
+ return mValues.getAsBoolean(Sms.READ);
+ }
+
+ public boolean getReplyPathPresent() {
+ return mValues.getAsBoolean(Sms.REPLY_PATH_PRESENT);
+ }
+
+ public boolean getSeen() {
+ return mValues.getAsBoolean(Sms.SEEN);
+ }
+
+ public String getServiceCenter() {
+ return mValues.getAsString(Sms.SERVICE_CENTER);
+ }
+
+ public int getStatus() {
+ return mValues.getAsInteger(Sms.STATUS);
+ }
+
+ public String getSubject() {
+ return mValues.getAsString(Sms.SUBJECT);
+ }
+
+ public int getThreadId() {
+ return mValues.getAsInteger(Sms.THREAD_ID);
+ }
+
+ public int getType() {
+ return mValues.getAsInteger(Sms.TYPE);
+ }
+}
diff --git a/src/com/android/contacts/interactions/SmsInteractionsLoader.java b/src/com/android/contacts/interactions/SmsInteractionsLoader.java
new file mode 100644
index 0000000..e0c8cf4
--- /dev/null
+++ b/src/com/android/contacts/interactions/SmsInteractionsLoader.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 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.interactions;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.provider.Telephony;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Loads the most recent sms between the passed in phone numbers.
+ *
+ * This is a two part process. The first step is retrieving the threadIds for each of the phone
+ * numbers using fuzzy matching. The next step is to run another query against these threadIds
+ * to retrieve the actual sms.
+ */
+public class SmsInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
+
+ private static final String TAG = SmsInteractionsLoader.class.getSimpleName();
+
+ private String[] mPhoneNums;
+ private int mMaxToRetrieve;
+ private List<ContactInteraction> mData;
+
+ /**
+ * Loads a list of SmsInteraction from the supplied phone numbers.
+ */
+ public SmsInteractionsLoader(Context context, String[] phoneNums,
+ int maxToRetrieve) {
+ super(context);
+ Log.v(TAG, "SmsInteractionsLoader");
+ mPhoneNums = phoneNums;
+ mMaxToRetrieve = maxToRetrieve;
+ }
+
+ @Override
+ public List<ContactInteraction> loadInBackground() {
+ Log.v(TAG, "loadInBackground");
+ // Confirm the device has Telephony and numbers were provided before proceeding
+ if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+ || mPhoneNums == null || mPhoneNums.length == 0) {
+ return Collections.emptyList();
+ }
+
+ // Retrieve the thread IDs
+ List<String> threadIdStrings = new ArrayList<>();
+ for (String phone : mPhoneNums) {
+ threadIdStrings.add(String.valueOf(
+ Telephony.Threads.getOrCreateThreadId(getContext(), phone)));
+ }
+
+ // Query the SMS database for the threads
+ Cursor cursor = getSmsCursorFromThreads(threadIdStrings);
+
+ List<ContactInteraction> interactions = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ ContentValues values = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, values);
+ interactions.add(new SmsInteraction(values));
+ }
+
+ Log.v(TAG, "end loadInBackground");
+ return interactions;
+ }
+
+ /**
+ * Return the most recent messages between a list of threads
+ */
+ private Cursor getSmsCursorFromThreads(List<String> threadIds) {
+ String selection = Telephony.Sms.THREAD_ID + " IN "
+ + ContactInteractionUtil.questionMarks(threadIds.size());
+
+ return getContext().getContentResolver().query(
+ Telephony.Sms.CONTENT_URI,
+ /* projection = */ null,
+ selection,
+ threadIds.toArray(new String[threadIds.size()]),
+ Telephony.Sms.DEFAULT_SORT_ORDER
+ + " LIMIT " + mMaxToRetrieve);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ super.onStartLoading();
+
+ if (mData != null) {
+ deliverResult(mData);
+ }
+
+ if (takeContentChanged() || mData == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ // Attempt to cancel the current load task if possible.
+ cancelLoad();
+ }
+
+ @Override
+ public void deliverResult(List<ContactInteraction> data) {
+ mData = data;
+ if (isStarted()) {
+ super.deliverResult(data);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+ if (mData != null) {
+ mData.clear();
+ }
+ }
+}
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 2ff34e1..dc6b5f2 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -65,9 +65,13 @@
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.model.dataitem.EmailDataItem;
import com.android.contacts.common.model.dataitem.ImDataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
import com.android.contacts.common.util.Constants;
import com.android.contacts.common.util.DataStatus;
import com.android.contacts.common.util.UriUtils;
+import com.android.contacts.interactions.ContactInteraction;
+import com.android.contacts.interactions.ContactInteractionUtil;
+import com.android.contacts.interactions.SmsInteractionsLoader;
import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
import com.android.contacts.util.ImageViewDrawableSetter;
import com.android.contacts.common.util.StopWatch;
@@ -79,9 +83,13 @@
import com.google.common.collect.Lists;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -111,8 +119,12 @@
private ImageView mEditOrAddContactImage;
private ImageView mStarImage;
private ExpandingEntryCardView mCommunicationCard;
+ private ExpandingEntryCardView mRecentCard;
private MultiShrinkScroller mScroller;
+ private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3;
+ private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
+
private Contact mContactData;
private ContactLoader mContactLoader;
@@ -148,8 +160,18 @@
private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
- /** Id for the background loader */
- private static final int LOADER_ID = 0;
+ /** Id for the background contact loader */
+ private static final int LOADER_CONTACT_ID = 0;
+
+ /** Id for the background Sms Loader */
+ private static final int LOADER_SMS_ID = 1;
+ private static final String KEY_LOADER_EXTRA_SMS_PHONES =
+ QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SMS_PHONES";
+ private static final int MAX_SMS_RETRIEVE = 3;
+
+ private static final int[] mRecentLoaderIds = new int[LOADER_SMS_ID];
+ private Map<Integer, List<ContactInteraction>> mRecentLoaderResults;
+
private StopWatch mStopWatch = ENABLE_STOPWATCH
? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch();
@@ -232,7 +254,7 @@
mStopWatch.lap("i"); // intent parsed
mContactLoader = (ContactLoader) getLoaderManager().initLoader(
- LOADER_ID, null, mLoaderCallbacks);
+ LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
mStopWatch.lap("ld"); // loader started
@@ -247,15 +269,20 @@
mEditOrAddContactImage = (ImageView) findViewById(R.id.contact_edit_image);
mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button);
mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
+ mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
- mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
if (mScroller != null) {
mScroller.initialize(mMultiShrinkScrollerListener);
}
mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler);
+
mCommunicationCard.setOnClickListener(mEntryClickHandler);
+ mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
+
+ mRecentCard.setOnClickListener(mEntryClickHandler);
+ mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
// find and prepare correct header view
mPhotoContainer = findViewById(R.id.photo_container);
@@ -314,7 +341,7 @@
/**
* Handle the result from the ContactLoader
*/
- private void bindData(Contact data) {
+ private void bindContactData(Contact data) {
mContactData = data;
final ResolveCache cache = ResolveCache.getInstance(this);
final Context context = this;
@@ -379,6 +406,8 @@
mStopWatch.lap("ph"); // Photo set
+ // Maintain a list of phone numbers to pass into SmsInteractionsLoader
+ List<String> phoneNumbers = new ArrayList<>();
for (RawContact rawContact : data.getRawContacts()) {
for (DataItem dataItem : rawContact.getDataItems()) {
final String mimeType = dataItem.getMimeType();
@@ -386,6 +415,10 @@
final DataKind dataKind = AccountTypeManager.getInstance(this)
.getKindOrFallback(accountType, mimeType);
+ if (dataItem instanceof PhoneDataItem) {
+ phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber());
+ }
+
// Skip this data item if MIME-type excluded
if (isMimeExcluded(mimeType)) continue;
@@ -463,8 +496,22 @@
entries.addAll(actionsToEntries(mActions.get(mimeType)));
}
}
- mCommunicationCard.initialize(entries, /* numInitialVisibleEntries = */ 2,
- /* isExpanded = */ false, /* themeColor = */ 0);
+
+ Bundle smsExtraBundle = new Bundle();
+ smsExtraBundle.putStringArray(KEY_LOADER_EXTRA_SMS_PHONES,
+ phoneNumbers.toArray(new String[phoneNumbers.size()]));
+ getLoaderManager().initLoader(
+ LOADER_SMS_ID,
+ smsExtraBundle,
+ mLoaderInteractionsCallbacks);
+
+ if (entries.size() > 0) {
+ mCommunicationCard.initialize(entries,
+ /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN,
+ /* isExpanded = */ false,
+ /* themeColor = */ 0);
+ mCommunicationCard.setVisibility(View.VISIBLE);
+ }
final boolean hasData = !mSortedActionMimeTypes.isEmpty();
mCommunicationCard.setVisibility(hasData ? View.VISIBLE: View.GONE);
@@ -572,7 +619,22 @@
return entries;
}
- private LoaderCallbacks<Contact> mLoaderCallbacks =
+ private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
+ List<Entry> entries = new ArrayList<>();
+ for (ContactInteraction interaction : interactions) {
+ entries.add(new Entry(interaction.getIcon(this),
+ interaction.getViewHeader(this),
+ interaction.getViewBody(this),
+ interaction.getBodyIcon(this),
+ interaction.getViewFooter(this),
+ interaction.getFooterIcon(this),
+ interaction.getIntent(),
+ /* isEditable = */ false));
+ }
+ return entries;
+ }
+
+ private LoaderCallbacks<Contact> mLoaderContactCallbacks =
new LoaderCallbacks<Contact>() {
@Override
public void onLoaderReset(Loader<Contact> loader) {
@@ -596,7 +658,7 @@
return;
}
- bindData(data);
+ bindContactData(data);
mStopWatch.lap("bd"); // bindData finished
@@ -631,6 +693,7 @@
false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
}
};
+
@Override
public void onBackPressed() {
if (mScroller != null) {
@@ -640,4 +703,70 @@
super.onBackPressed();
}
}
+
+ private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
+ new LoaderCallbacks<List<ContactInteraction>>() {
+
+ @Override
+ public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) {
+ Log.v(TAG, "onCreateLoader");
+ Loader<List<ContactInteraction>> loader = null;
+ switch (id) {
+ case LOADER_SMS_ID:
+ Log.v(TAG, "LOADER_SMS_ID");
+ loader = new SmsInteractionsLoader(
+ QuickContactActivity.this,
+ args.getStringArray(KEY_LOADER_EXTRA_SMS_PHONES),
+ MAX_SMS_RETRIEVE);
+ break;
+ }
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<ContactInteraction>> loader,
+ List<ContactInteraction> data) {
+ if (mRecentLoaderResults == null) {
+ mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>();
+ }
+ mRecentLoaderResults.put(loader.getId(), data);
+
+ if (isAllRecentDataLoaded()) {
+ bindRecentData();
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<ContactInteraction>> loader) {
+ mRecentLoaderResults.remove(loader.getId());
+ }
+
+ };
+
+ private boolean isAllRecentDataLoaded() {
+ return mRecentLoaderResults.size() == mRecentLoaderIds.length;
+ }
+
+ private void bindRecentData() {
+ List<ContactInteraction> allInteractions = new ArrayList<>();
+ for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
+ allInteractions.addAll(loaderInteractions);
+ }
+
+ // Sort the interactions by most recent
+ Collections.sort(allInteractions, new Comparator<ContactInteraction>() {
+ @Override
+ public int compare(ContactInteraction a, ContactInteraction b) {
+ return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1;
+ }
+ });
+
+ if (allInteractions.size() > 0) {
+ mRecentCard.initialize(contactInteractionsToEntries(allInteractions),
+ /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN,
+ /* isExpanded = */ false,
+ /* themeColor = */ 0);
+ mRecentCard.setVisibility(View.VISIBLE);
+ }
+ }
}
diff --git a/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
new file mode 100644
index 0000000..05ad9b5
--- /dev/null
+++ b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 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.interactions;
+
+import com.android.contacts.common.R;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.test.AndroidTestCase;
+import android.text.format.DateUtils;
+
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Tests for utility functions in {@link ContactInteractionUtil}
+ */
+public class ContactInteractionUtilTest extends AndroidTestCase {
+
+ private Locale mOriginalLocale;
+ private Calendar calendar;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ calendar = Calendar.getInstance();
+
+ // Time/Date utilities rely on specific locales. Forace US and set back in tearDown()
+ mOriginalLocale = Locale.getDefault();
+ setLocale(Locale.US);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ setLocale(mOriginalLocale);
+ super.tearDown();
+ }
+
+ public void testOneQuestionMark() {
+ assertEquals("(?)", ContactInteractionUtil.questionMarks(1));
+ }
+
+ public void testTwoQuestionMarks() {
+ assertEquals("(?,?)", ContactInteractionUtil.questionMarks(2));
+ }
+
+ public void testFiveQuestionMarks() {
+ assertEquals("(?,?,?,?,?)", ContactInteractionUtil.questionMarks(5));
+ }
+
+ public void testFormatDateStringFromTimestamp_todaySingleMinuteAm() {
+ // Test today scenario (time shown)
+ // Single digit minute & AM
+ calendar.set(Calendar.HOUR_OF_DAY, 8);
+ calendar.set(Calendar.MINUTE, 8);
+ long todayTimestamp = calendar.getTimeInMillis();
+ assertEquals("8:08 AM", ContactInteractionUtil.formatDateStringFromTimestamp(
+ calendar.getTimeInMillis(), getContext()));
+ }
+
+ public void testFormatDateStringFromTimestamp_todayDoubleMinutePm() {
+ // Double digit minute & PM
+ calendar.set(Calendar.HOUR_OF_DAY, 22);
+ calendar.set(Calendar.MINUTE, 18);
+ assertEquals("10:18 PM",
+ ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
+ getContext()));
+ }
+
+ public void testFormatDateStringFromTimestamp_yesterday() {
+ // Test yesterday and tomorrow (Yesterday or Tomorrow shown)
+ calendar.add(Calendar.DAY_OF_YEAR, -1);
+ assertEquals(getContext().getResources().getString(R.string.timestamp_string_yesterday),
+ ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
+ getContext()));
+ }
+
+ public void testFormatDateStringFromTimestamp_yesterdayLastYear() {
+ // Set to non leap year
+ calendar.set(Calendar.YEAR, 1999);
+ calendar.set(Calendar.DAY_OF_YEAR, 365);
+ long lastYear = calendar.getTimeInMillis();
+ calendar.add(Calendar.DAY_OF_YEAR, 1);
+
+ assertEquals(getContext().getResources().getString(R.string.timestamp_string_yesterday),
+ ContactInteractionUtil.formatDateStringFromTimestamp(lastYear,
+ getContext(), calendar));
+ }
+
+ public void testFormatDateStringFromTimestamp_tomorrow() {
+ calendar.add(Calendar.DAY_OF_YEAR, 1);
+ assertEquals(getContext().getResources().getString(R.string.timestamp_string_tomorrow),
+ ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
+ getContext()));
+ }
+
+ public void testFormatDateStringFromTimestamp_tomorrowNewYear() {
+ calendar.set(Calendar.DAY_OF_YEAR, 1);
+ long thisYear = calendar.getTimeInMillis();
+ calendar.add(Calendar.DAY_OF_YEAR, -1);
+
+ assertEquals(getContext().getResources().getString(R.string.timestamp_string_tomorrow),
+ ContactInteractionUtil.formatDateStringFromTimestamp(thisYear,
+ getContext(), calendar));
+ }
+
+ public void testFormatDateStringFromTimestamp_other() {
+ // Test other (Month Date)
+ calendar.set(
+ /* year = */ 1991,
+ /* month = */ Calendar.MONTH,
+ /* day = */ 11);
+ assertEquals("March 11",
+ ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
+ getContext()));
+ }
+
+ private void setLocale(Locale locale) {
+ Locale.setDefault(locale);
+ Resources res = getContext().getResources();
+ Configuration config = res.getConfiguration();
+ config.locale = locale;
+ res.updateConfiguration(config, res.getDisplayMetrics());
+ }
+}
\ No newline at end of file