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());
+    }
+}