Adds Call Log interactions to the recent card
Change-Id: I2ade43cee543c706a90da81a4c4bd256b71411f8
diff --git a/res/drawable-hdpi/ic_call_arrow.png b/res/drawable-hdpi/ic_call_arrow.png
new file mode 100644
index 0000000..14a33e3
--- /dev/null
+++ b/res/drawable-hdpi/ic_call_arrow.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_arrow.png b/res/drawable-mdpi/ic_call_arrow.png
new file mode 100644
index 0000000..169cf29
--- /dev/null
+++ b/res/drawable-mdpi/ic_call_arrow.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_arrow.png b/res/drawable-xhdpi/ic_call_arrow.png
new file mode 100644
index 0000000..6f13660
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call_arrow.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_arrow.png b/res/drawable-xxhdpi/ic_call_arrow.png
new file mode 100644
index 0000000..0364ee0
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 0f5997b..962fe97 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -49,4 +49,7 @@
<!-- Color of the margin for cards -->
<color name="card_margin_color">#ffbbbbbb</color>
+
+ <color name="call_arrow_green">#2aad6f</color>
+ <color name="call_arrow_red">#ff2e58</color>
</resources>
diff --git a/src/com/android/contacts/interactions/CallLogInteraction.java b/src/com/android/contacts/interactions/CallLogInteraction.java
new file mode 100644
index 0000000..8607974
--- /dev/null
+++ b/src/com/android/contacts/interactions/CallLogInteraction.java
@@ -0,0 +1,168 @@
+/*
+ * 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 com.android.contacts.common.util.BitmapUtil;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.util.Log;
+
+/**
+ * Represents a call log event interaction, wrapping the columns in
+ * {@link android.provider.CallLog.Calls}.
+ *
+ * This class does not return log entries related to voicemail or SIP calls. Additionally,
+ * this class ignores number presentation. Number presentation affects how to identify phone
+ * numbers. Since, we already know the identity of the phone number owner we can ignore number
+ * presentation.
+ *
+ * As a result of ignoring voicemail and number presentation, we don't need to worry about API
+ * version.
+ */
+public class CallLogInteraction implements ContactInteraction {
+
+ private static final String URI_TARGET_PREFIX = "tel:";
+ private static final int CALL_LOG_ICON_RES = R.drawable.ic_phone_24dp;
+ private static final int CALL_ARROW_ICON_RES = R.drawable.ic_call_arrow;
+
+ private ContentValues mValues;
+
+ public CallLogInteraction(ContentValues values) {
+ mValues = values;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return new Intent(Intent.ACTION_CALL).setData(Uri.parse(URI_TARGET_PREFIX + getNumber()));
+ }
+
+ @Override
+ public String getViewHeader(Context context) {
+ return getNumber();
+ }
+
+ @Override
+ public long getInteractionDate() {
+ return getDate();
+ }
+
+ @Override
+ public String getViewBody(Context context) {
+ int numberType = getCachedNumberType();
+ if (numberType == -1) {
+ return null;
+ }
+ return Phone.getTypeLabel(context.getResources(), getCachedNumberType(),
+ getCachedNumberLabel()).toString();
+ }
+
+ @Override
+ public String getViewFooter(Context context) {
+ return ContactInteractionUtil.formatDateStringFromTimestamp(getDate(), context);
+ }
+
+ @Override
+ public Drawable getIcon(Context context) {
+ return context.getResources().getDrawable(CALL_LOG_ICON_RES);
+ }
+
+ @Override
+ public Drawable getBodyIcon(Context context) {
+ return null;
+ }
+
+ @Override
+ public Drawable getFooterIcon(Context context) {
+ Drawable callArrow = null;
+ Resources res = context.getResources();
+ switch (getType()) {
+ case Calls.INCOMING_TYPE:
+ callArrow = res.getDrawable(CALL_ARROW_ICON_RES);
+ callArrow.setColorFilter(res.getColor(R.color.call_arrow_green),
+ PorterDuff.Mode.MULTIPLY);
+ break;
+ case Calls.MISSED_TYPE:
+ callArrow = res.getDrawable(CALL_ARROW_ICON_RES);
+ callArrow.setColorFilter(res.getColor(R.color.call_arrow_red),
+ PorterDuff.Mode.MULTIPLY);
+ break;
+ case Calls.OUTGOING_TYPE:
+ callArrow = BitmapUtil.getRotatedDrawable(res, CALL_ARROW_ICON_RES, 180f);
+ callArrow.setColorFilter(res.getColor(R.color.call_arrow_green),
+ PorterDuff.Mode.MULTIPLY);
+ break;
+ }
+ return callArrow;
+ }
+
+ public String getCachedName() {
+ return mValues.getAsString(Calls.CACHED_NAME);
+ }
+
+ public String getCachedNumberLabel() {
+ return mValues.getAsString(Calls.CACHED_NUMBER_LABEL);
+ }
+
+ public int getCachedNumberType() {
+ Integer type = mValues.getAsInteger(Calls.CACHED_NUMBER_TYPE);
+ return type != null ? type : -1;
+ }
+
+ public long getDate() {
+ return mValues.getAsLong(Calls.DATE);
+ }
+
+ public long getDuration() {
+ return mValues.getAsLong(Calls.DURATION);
+ }
+
+ public boolean getIsRead() {
+ return mValues.getAsBoolean(Calls.IS_READ);
+ }
+
+ public int getLimitParamKey() {
+ return mValues.getAsInteger(Calls.LIMIT_PARAM_KEY);
+ }
+
+ public boolean getNew() {
+ return mValues.getAsBoolean(Calls.NEW);
+ }
+
+ public String getNumber() {
+ return mValues.getAsString(Calls.NUMBER);
+ }
+
+ public int getNumberPresentation() {
+ return mValues.getAsInteger(Calls.NUMBER_PRESENTATION);
+ }
+
+ public int getOffsetParamKey() {
+ return mValues.getAsInteger(Calls.OFFSET_PARAM_KEY);
+ }
+
+ public int getType() {
+ return mValues.getAsInteger(Calls.TYPE);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/interactions/CallLogInteractionsLoader.java b/src/com/android/contacts/interactions/CallLogInteractionsLoader.java
new file mode 100644
index 0000000..8172232
--- /dev/null
+++ b/src/com/android/contacts/interactions/CallLogInteractionsLoader.java
@@ -0,0 +1,161 @@
+/*
+ * 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.net.Uri;
+import android.provider.CallLog.Calls;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
+
+ private final String[] mPhoneNumbers;
+ private final int mMaxToRetrieve;
+ private List<ContactInteraction> mData;
+
+ public CallLogInteractionsLoader(Context context, String[] phoneNumbers,
+ int maxToRetrieve) {
+ super(context);
+ mPhoneNumbers = phoneNumbers;
+ mMaxToRetrieve = maxToRetrieve;
+ }
+
+ @Override
+ public List<ContactInteraction> loadInBackground() {
+ if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+ || mPhoneNumbers == null || mPhoneNumbers.length == 0) {
+ return Collections.emptyList();
+ }
+
+ final List<ContactInteraction> interactions = new ArrayList<>();
+ for (String number : mPhoneNumbers) {
+ interactions.addAll(getCallLogInteractions(number));
+ }
+ // Sort the call log interactions by date for duplicate removal
+ Collections.sort(interactions, new Comparator<ContactInteraction>() {
+ @Override
+ public int compare(ContactInteraction i1, ContactInteraction i2) {
+ if (i2.getInteractionDate() - i1.getInteractionDate() > 0) {
+ return 1;
+ } else if (i2.getInteractionDate() == i1.getInteractionDate()) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+ });
+
+ return pruneDuplicateCallLogInteractions(interactions, mMaxToRetrieve);
+ }
+
+ /**
+ * Two different phone numbers can match the same call log entry (since phone number
+ * matching is inexact). Therefore, we need to remove duplicates. In a reasonable call log,
+ * every entry should have a distinct date. Therefore, we can assume duplicate entries are
+ * adjacent entries.
+ * @param interactions The interaction list potentially containing duplicates
+ * @return The list with duplicates removed
+ */
+ @VisibleForTesting
+ static List<ContactInteraction> pruneDuplicateCallLogInteractions(
+ List<ContactInteraction> interactions, int maxToRetrieve) {
+ final List<ContactInteraction> subsetInteractions = new ArrayList<>();
+ for (int i = 0; i < interactions.size(); i++) {
+ if (i >= 1 && interactions.get(i).getInteractionDate() ==
+ interactions.get(i-1).getInteractionDate()) {
+ continue;
+ }
+ subsetInteractions.add(interactions.get(i));
+ if (subsetInteractions.size() >= maxToRetrieve) {
+ break;
+ }
+ }
+ return subsetInteractions;
+ }
+
+ private List<ContactInteraction> getCallLogInteractions(String phoneNumber) {
+ final Uri uri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, phoneNumber);
+ final String orderBy = Calls.DATE + " DESC";
+ final Cursor cursor = getContext().getContentResolver().query(uri, null, null, null,
+ orderBy);
+ try {
+ if (cursor == null || cursor.getCount() < 1) {
+ return Collections.emptyList();
+ }
+ cursor.moveToPosition(-1);
+ List<ContactInteraction> interactions = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ final ContentValues values = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, values);
+ interactions.add(new CallLogInteraction(values));
+ }
+ return interactions;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ @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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 26dbebe..9533d75 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -82,6 +82,7 @@
import com.android.contacts.detail.ContactDetailDisplayUtils;
import com.android.contacts.common.util.UriUtils;
import com.android.contacts.interactions.CalendarInteractionsLoader;
+import com.android.contacts.interactions.CallLogInteractionsLoader;
import com.android.contacts.interactions.ContactDeletionInteraction;
import com.android.contacts.interactions.ContactInteraction;
import com.android.contacts.interactions.SmsInteractionsLoader;
@@ -191,14 +192,17 @@
/** Id for the background contact loader */
private static final int LOADER_CONTACT_ID = 0;
+ private static final String KEY_LOADER_EXTRA_PHONES =
+ QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES";
+
/** 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;
+
+ /** Id for the back Calendar Loader */
private static final int LOADER_CALENDAR_ID = 2;
- private static final String KEY_LOADER_EXTRA_CALENDAR_EMAILS =
- QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_CALENDAR_EMAILS";
+ private static final String KEY_LOADER_EXTRA_EMAILS =
+ QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS";
private static final int MAX_PAST_CALENDAR_RETRIEVE = 3;
private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3;
private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
@@ -206,7 +210,15 @@
private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
36L * 60L * 60L * 1000L /* 36 hours */;
- private static final int[] mRecentLoaderIds = new int[]{LOADER_SMS_ID, LOADER_CALENDAR_ID};
+ /** Id for the background Call Log Loader */
+ private static final int LOADER_CALL_LOG_ID = 3;
+ private static final int MAX_CALL_LOG_RETRIEVE = 3;
+
+
+ private static final int[] mRecentLoaderIds = new int[]{
+ LOADER_SMS_ID,
+ LOADER_CALENDAR_ID,
+ LOADER_CALL_LOG_ID};
private Map<Integer, List<ContactInteraction>> mRecentLoaderResults;
private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
@@ -482,22 +494,29 @@
Set<String> emailAddresses,
List<String> sortedActionMimeTypes) {
Trace.beginSection("start sms loader");
- final Bundle smsExtraBundle = new Bundle();
- smsExtraBundle.putStringArray(KEY_LOADER_EXTRA_SMS_PHONES,
+ final Bundle phonesExtraBundle = new Bundle();
+ phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES,
phoneNumbers.toArray(new String[phoneNumbers.size()]));
getLoaderManager().initLoader(
LOADER_SMS_ID,
- smsExtraBundle,
+ phonesExtraBundle,
+ mLoaderInteractionsCallbacks);
+ Trace.endSection();
+
+ Trace.beginSection("start call log loader");
+ getLoaderManager().initLoader(
+ LOADER_CALL_LOG_ID,
+ phonesExtraBundle,
mLoaderInteractionsCallbacks);
Trace.endSection();
Trace.beginSection("start calendar loader");
- final Bundle calendarExtraBundle = new Bundle();
- calendarExtraBundle.putStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS,
+ final Bundle emailsExtraBundle = new Bundle();
+ emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS,
emailAddresses.toArray(new String[emailAddresses.size()]));
getLoaderManager().initLoader(
LOADER_CALENDAR_ID,
- calendarExtraBundle,
+ emailsExtraBundle,
mLoaderInteractionsCallbacks);
Trace.endSection();
@@ -886,19 +905,25 @@
Log.v(TAG, "LOADER_SMS_ID");
loader = new SmsInteractionsLoader(
QuickContactActivity.this,
- args.getStringArray(KEY_LOADER_EXTRA_SMS_PHONES),
+ args.getStringArray(KEY_LOADER_EXTRA_PHONES),
MAX_SMS_RETRIEVE);
break;
case LOADER_CALENDAR_ID:
Log.v(TAG, "LOADER_CALENDAR_ID");
loader = new CalendarInteractionsLoader(
QuickContactActivity.this,
- Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS)),
+ Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)),
MAX_FUTURE_CALENDAR_RETRIEVE,
MAX_PAST_CALENDAR_RETRIEVE,
FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR,
PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR);
break;
+ case LOADER_CALL_LOG_ID:
+ Log.v(TAG, "LOADER_CALL_LOG_ID");
+ loader = new CallLogInteractionsLoader(
+ QuickContactActivity.this,
+ args.getStringArray(KEY_LOADER_EXTRA_PHONES),
+ MAX_CALL_LOG_RETRIEVE);
}
return loader;
}
diff --git a/tests/src/com/android/contacts/interactions/CallLogInteractionsLoaderTest.java b/tests/src/com/android/contacts/interactions/CallLogInteractionsLoaderTest.java
new file mode 100644
index 0000000..079411f
--- /dev/null
+++ b/tests/src/com/android/contacts/interactions/CallLogInteractionsLoaderTest.java
@@ -0,0 +1,65 @@
+package com.android.contacts.interactions;
+
+import android.content.ContentValues;
+import android.provider.CallLog.Calls;
+import android.test.AndroidTestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests {@link CallLogInteractionsLoader}
+ */
+public class CallLogInteractionsLoaderTest extends AndroidTestCase {
+
+ public void testCallLogInteractions_pruneDuplicates_containsDuplicates() {
+ List<ContactInteraction> interactions = new ArrayList<>();
+ int maxToRetrieve = 5;
+
+ ContentValues interactionOneValues = new ContentValues();
+ interactionOneValues.put(Calls.DATE, 1L);
+ interactions.add(new CallLogInteraction(interactionOneValues));
+
+ ContentValues interactionTwoValues = new ContentValues();
+ interactionTwoValues.put(Calls.DATE, 1L);
+ interactions.add(new CallLogInteraction(interactionTwoValues));
+
+ interactions = CallLogInteractionsLoader.pruneDuplicateCallLogInteractions(interactions,
+ maxToRetrieve);
+ assertEquals(1, interactions.size());
+ }
+
+ public void testCallLogInteractions_pruneDuplicates_containsNoDuplicates() {
+ List<ContactInteraction> interactions = new ArrayList<>();
+ int maxToRetrieve = 5;
+
+ ContentValues interactionOneValues = new ContentValues();
+ interactionOneValues.put(Calls.DATE, 1L);
+ interactions.add(new CallLogInteraction(interactionOneValues));
+
+ ContentValues interactionTwoValues = new ContentValues();
+ interactionTwoValues.put(Calls.DATE, 5L);
+ interactions.add(new CallLogInteraction(interactionTwoValues));
+
+ interactions = CallLogInteractionsLoader.pruneDuplicateCallLogInteractions(interactions,
+ maxToRetrieve);
+ assertEquals(2, interactions.size());
+ }
+
+ public void testCallLogInteractions_maxToRetrieve() {
+ List<ContactInteraction> interactions = new ArrayList<>();
+ int maxToRetrieve = 1;
+
+ ContentValues interactionOneValues = new ContentValues();
+ interactionOneValues.put(Calls.DATE, 1L);
+ interactions.add(new CallLogInteraction(interactionOneValues));
+
+ ContentValues interactionTwoValues = new ContentValues();
+ interactionTwoValues.put(Calls.DATE, 5L);
+ interactions.add(new CallLogInteraction(interactionTwoValues));
+
+ interactions = CallLogInteractionsLoader.pruneDuplicateCallLogInteractions(interactions,
+ maxToRetrieve);
+ assertEquals(1, interactions.size());
+ }
+}