Adding missed call badge.

Bug: 10861718
Change-Id: I3a889a71cff7abac578da83d09dd7af23f3f88ca
diff --git a/res/drawable-hdpi/ic_call_log_blue.png b/res/drawable-hdpi/ic_call_log_blue.png
new file mode 100644
index 0000000..92af15f
--- /dev/null
+++ b/res/drawable-hdpi/ic_call_log_blue.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_log_blue.png b/res/drawable-mdpi/ic_call_log_blue.png
new file mode 100644
index 0000000..b9209ad
--- /dev/null
+++ b/res/drawable-mdpi/ic_call_log_blue.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_log_blue.png b/res/drawable-xhdpi/ic_call_log_blue.png
new file mode 100644
index 0000000..9d92573
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call_log_blue.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_log_blue.png b/res/drawable-xxhdpi/ic_call_log_blue.png
new file mode 100644
index 0000000..0a55a75
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_call_log_blue.png
Binary files differ
diff --git a/res/layout/call_log_list_item.xml b/res/layout/call_log_list_item.xml
index c49b4b0..1bd448c 100644
--- a/res/layout/call_log_list_item.xml
+++ b/res/layout/call_log_list_item.xml
@@ -140,4 +140,10 @@
         android:paddingTop="@dimen/call_log_inner_margin"
         android:paddingBottom="@dimen/call_log_inner_margin" />
 
+    <!-- Displays the extra link section -->
+    <ViewStub android:id="@+id/link_stub"
+              android:layout="@layout/call_log_list_item_extra"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"/>
+
 </view>
diff --git a/res/layout/call_log_list_item_extra.xml b/res/layout/call_log_list_item_extra.xml
new file mode 100644
index 0000000..672abf1
--- /dev/null
+++ b/res/layout/call_log_list_item_extra.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2013 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
+  -->
+
+<!-- Can't use merge here because this is referenced via a ViewStub -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="wrap_content"
+             android:id="@+id/badge_container">
+
+    <View android:layout_width="match_parent"
+          android:layout_height="1px"
+          android:layout_marginStart="@dimen/call_log_outer_margin"
+          android:layout_marginEnd="@dimen/call_log_outer_margin"
+          android:background="@color/favorite_contacts_separator_color"/>
+
+    <LinearLayout android:id="@+id/badge_link_container"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:paddingStart="@dimen/call_log_outer_margin"
+                  android:paddingEnd="@dimen/call_log_outer_margin"
+                  android:paddingTop="4dip"
+                  android:paddingBottom="4dip"
+                  android:background="?android:attr/selectableItemBackground"
+                  android:clickable="true">
+        <ImageView android:layout_width="wrap_content"
+                   android:layout_height="wrap_content"
+                   android:id="@+id/badge_image"
+                   android:padding="@dimen/call_log_outer_margin"/>
+        <TextView android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:id="@+id/badge_text"
+                  android:textColor="@color/dialpad_primary_text_color"
+                  android:layout_gravity="center_vertical"/>
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 37dbdf3..b2493cf 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -18,6 +18,7 @@
 
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
@@ -29,7 +30,10 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.TextView;
 
 import com.android.common.widget.GroupingListAdapter;
 import com.android.contacts.common.ContactPhotoManager;
@@ -48,6 +52,7 @@
  */
 public class CallLogAdapter extends GroupingListAdapter
         implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+
     /** Interface used to initiate a refresh of the content. */
     public interface CallFetcher {
         public void fetchCalls();
@@ -182,6 +187,15 @@
      * action should be set to call a number instead of opening the detail page. */
     private boolean mUseCallAsPrimaryAction = false;
 
+    private boolean mIsCallLog = true;
+    private int mNumMissedCalls = 0;
+    private int mNumMissedCallsShown = 0;
+    private Uri mCurrentPhotoUri;
+
+    private View mBadgeContainer;
+    private ImageView mBadgeImageView;
+    private TextView mBadgeText;
+
     /** Listener for the primary action in the list, opens the call details. */
     private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
         @Override
@@ -232,13 +246,15 @@
     };
 
     public CallLogAdapter(Context context, CallFetcher callFetcher,
-            ContactInfoHelper contactInfoHelper, boolean useCallAsPrimaryAction) {
+            ContactInfoHelper contactInfoHelper, boolean useCallAsPrimaryAction,
+            boolean isCallLog) {
         super(context);
 
         mContext = context;
         mCallFetcher = callFetcher;
         mContactInfoHelper = contactInfoHelper;
         mUseCallAsPrimaryAction = useCallAsPrimaryAction;
+        mIsCallLog = isCallLog;
 
         mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
         mRequests = new LinkedList<ContactInfoRequest>();
@@ -614,12 +630,107 @@
             mViewTreeObserver.addOnPreDrawListener(this);
         }
 
-        postBindView(views, info, details);
+        bindBadge(view, info, details, callType);
     }
 
-    protected void postBindView(CallLogListItemViews views, ContactInfo info,
+    protected void bindBadge(View view, ContactInfo info, PhoneCallDetails details, int callType) {
+
+        // Do not show badge in call log.
+        if (!mIsCallLog) {
+            final int numMissed = getNumMissedCalls(callType);
+            final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub);
+            if (shouldShowBadge(numMissed, info, details)) {
+
+                // stub will be null if it was already inflated.
+                if (stub != null) {
+                    final View inflated = stub.inflate();
+                    inflated.setVisibility(View.VISIBLE);
+                    mBadgeContainer = inflated.findViewById(R.id.badge_link_container);
+                    mBadgeImageView = (ImageView) inflated.findViewById(R.id.badge_image);
+                    mBadgeText = (TextView) inflated.findViewById(R.id.badge_text);
+                }
+
+                mBadgeContainer.setOnClickListener(getBadgeClickListener());
+                mBadgeImageView.setImageResource(getBadgeImageResId());
+                mBadgeText.setText(getBadgeText(numMissed));
+
+                mNumMissedCallsShown = numMissed;
+            } else {
+                // Hide badge if it was previously shown.
+                if (stub == null) {
+                    final View container = view.findViewById(R.id.badge_container);
+                    if (container != null) {
+                        container.setVisibility(View.GONE);
+                    }
+                }
+            }
+        }
+    }
+
+    public void setMissedCalls(Cursor data) {
+        final int missed;
+        if (data == null) {
+            missed = 0;
+        } else {
+            missed = data.getCount();
+        }
+        // Only need to update if the number of calls changed.
+        if (missed != mNumMissedCalls) {
+            mNumMissedCalls = missed;
+            notifyDataSetChanged();
+        }
+    }
+
+    protected View.OnClickListener getBadgeClickListener() {
+        return new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final Intent intent = new Intent(mContext, CallLogActivity.class);
+                mContext.startActivity(intent);
+            }
+        };
+    }
+
+    /**
+     * Get the resource id for the image to be shown for the badge.
+     */
+    protected int getBadgeImageResId() {
+        return R.drawable.ic_call_log_blue;
+    }
+
+    /**
+     * Get the text to be shown for the badge.
+     *
+     * @param numMissed The number of missed calls.
+     */
+    protected String getBadgeText(int numMissed) {
+        return mContext.getResources().getString(R.string.num_missed_calls, numMissed);
+    }
+
+    /**
+     * Whether to show the badge.
+     *
+     * @param numMissedCalls The number of missed calls.
+     * @param info The contact info.
+     * @param details The call detail.
+     * @return {@literal true} if badge should be shown.  {@literal false} otherwise.
+     */
+    protected boolean shouldShowBadge(int numMissedCalls, ContactInfo info,
             PhoneCallDetails details) {
-        // no-op
+        // Do not process if the data has not changed (optimization since bind view is called
+        // multiple times due to contact lookup).
+        if (numMissedCalls == mNumMissedCallsShown) {
+            return false;
+        }
+        return numMissedCalls > 0;
+    }
+
+    private int getNumMissedCalls(int callType) {
+        if (callType == Calls.MISSED_TYPE) {
+            // Exclude the current missed call shown in the shortcut.
+            return mNumMissedCalls - 1;
+        }
+        return mNumMissedCalls;
     }
 
     /** Checks whether the contact info from the call log matches the one from the contacts db. */
@@ -733,11 +844,19 @@
     }
 
     private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
+        mCurrentPhotoUri = null;
         views.quickContactView.assignContactUri(contactUri);
         mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */);
     }
 
     private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri) {
+        if (photoUri.equals(mCurrentPhotoUri)) {
+            // photo manager will perform a fade in transition.  To avoid flicker, do not set the
+            // same photo multiple times.
+            return;
+        }
+
+        mCurrentPhotoUri = photoUri;
         views.quickContactView.assignContactUri(contactUri);
         mContactPhotoManager.loadDirectoryPhoto(views.quickContactView, photoUri,
                 false /* darkTheme */);
diff --git a/src/com/android/dialer/list/PhoneFavoriteFragment.java b/src/com/android/dialer/list/PhoneFavoriteFragment.java
index d731786..32ec71c 100644
--- a/src/com/android/dialer/list/PhoneFavoriteFragment.java
+++ b/src/com/android/dialer/list/PhoneFavoriteFragment.java
@@ -30,6 +30,7 @@
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
+import android.provider.CallLog;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -81,6 +82,7 @@
      * Used with LoaderManager.
      */
     private static int LOADER_ID_CONTACT_TILE = 1;
+    private static int MISSED_CALL_LOADER = 2;
 
     private static final String KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE =
             "key_last_dismissed_call_shortcut_date";
@@ -94,6 +96,27 @@
         public void onCallNumberDirectly(String phoneNumber);
     }
 
+    private class MissedCallLogLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
+
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            final Uri uri = CallLog.Calls.CONTENT_URI;
+            final String[] projection = new String[] {CallLog.Calls.TYPE};
+            final String selection = CallLog.Calls.TYPE + " = " + CallLog.Calls.MISSED_TYPE +
+                    " AND " + CallLog.Calls.IS_READ + " = 0";
+            return new CursorLoader(getActivity(), uri, projection, selection, null, null);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
+            mCallLogAdapter.setMissedCalls(data);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> cursorLoader) {
+        }
+    }
+
     private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
         @Override
         public CursorLoader onCreateLoader(int id, Bundle args) {
@@ -299,6 +322,7 @@
         // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
         // be called, on which we'll check if "all" contacts should be reloaded again or not.
         getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
+        getLoaderManager().initLoader(MISSED_CALL_LOADER, null, new MissedCallLogLoaderListener());
     }
 
     /**
diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java
index 6286c05..c43dffc 100644
--- a/src/com/android/dialerbind/ObjectFactory.java
+++ b/src/com/android/dialerbind/ObjectFactory.java
@@ -37,6 +37,7 @@
     public static CallLogAdapter newCallLogAdapter(Context context, CallFetcher callFetcher,
             ContactInfoHelper contactInfoHelper, boolean useCallAsPrimaryAction,
             boolean isCallLog) {
-        return new CallLogAdapter(context, callFetcher, contactInfoHelper, useCallAsPrimaryAction);
+        return new CallLogAdapter(context, callFetcher, contactInfoHelper, useCallAsPrimaryAction,
+                isCallLog);
     }
 }
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index 55e4224..12cdb2b 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -212,7 +212,7 @@
 
         public TestCallLogAdapter(Context context, CallFetcher callFetcher,
                 ContactInfoHelper contactInfoHelper) {
-            super(context, callFetcher, contactInfoHelper, false);
+            super(context, callFetcher, contactInfoHelper, false, false);
         }
 
         @Override