Merge "Call correct number from missing call notification."
diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java
index a27efec..252c701 100644
--- a/InCallUI/src/com/android/incallui/Call.java
+++ b/InCallUI/src/com/android/incallui/Call.java
@@ -21,6 +21,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Trace;
+import android.support.annotation.IntDef;
 import android.telecom.Call.Details;
 import android.telecom.Connection;
 import android.telecom.DisconnectCause;
@@ -41,6 +42,8 @@
 import com.android.dialer.util.IntentUtil;
 import com.android.incallui.util.TelecomCallUtil;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -51,6 +54,19 @@
  */
 @NeededForTesting
 public class Call {
+
+    /**
+     * Specifies whether a number is in the call history or not.
+     * {@link #CALL_HISTORY_STATUS_UNKNOWN} means there is no result.
+     */
+    @IntDef({CALL_HISTORY_STATUS_UNKNOWN, CALL_HISTORY_STATUS_PRESENT,
+            CALL_HISTORY_STATUS_NOT_PRESENT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CallHistoryStatus {}
+    public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
+    public static final int CALL_HISTORY_STATUS_PRESENT = 1;
+    public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+
     /* Defines different states of this call */
     public static class State {
         public static final int INVALID = 0;
@@ -381,6 +397,8 @@
     private String mLastForwardedNumber;
     private String mCallSubject;
     private PhoneAccountHandle mPhoneAccountHandle;
+    @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
+    private boolean mIsSpam;
 
     /**
      * Indicates whether the phone account associated with this call supports specifying a call
@@ -392,16 +410,6 @@
 
     private LogState mLogState = new LogState();
 
-    private boolean mIsSpam;
-
-    public void setSpam(boolean isSpam) {
-        mIsSpam = isSpam;
-    }
-
-    public boolean isSpam() {
-        return mIsSpam;
-    }
-
     /**
      * Used only to create mock calls for testing
      */
@@ -987,4 +995,21 @@
     public String toSimpleString() {
         return super.toString();
     }
+
+    public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
+        mCallHistoryStatus = callHistoryStatus;
+    }
+
+    @CallHistoryStatus
+    public int getCallHistoryStatus() {
+        return mCallHistoryStatus;
+    }
+
+    public void setSpam(boolean isSpam) {
+        mIsSpam = isSpam;
+    }
+
+    public boolean isSpam() {
+        return mIsSpam;
+    }
 }
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index 189049c..4442cbc 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -56,6 +56,7 @@
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.util.TelecomUtil;
+import com.android.incallui.spam.SpamCallListListener;
 import com.android.incallui.util.TelecomCallUtil;
 import com.android.incalluibind.ObjectFactory;
 
@@ -123,6 +124,7 @@
     private InCallCameraManager mInCallCameraManager = null;
     private AnswerPresenter mAnswerPresenter = new AnswerPresenter();
     private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
+    private CallList.Listener mSpamCallListListener;
 
     /**
      * Whether or not we are currently bound and waiting for Telecom to send us a new call.
@@ -345,6 +347,10 @@
         // will kick off an update and the whole process can start.
         mCallList.addListener(this);
 
+        // Create spam call list listener and add it to the list of listeners
+        mSpamCallListListener = new SpamCallListListener(context);
+        mCallList.addListener(mSpamCallListListener);
+
         VideoPauseController.getInstance().setUp(this);
         InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
 
@@ -1532,6 +1538,7 @@
 
             if (mCallList != null) {
                 mCallList.removeListener(this);
+                mCallList.removeListener(mSpamCallListListener);
             }
             mCallList = null;
 
diff --git a/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java b/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java
new file mode 100644
index 0000000..b97f4d0
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2016 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.incallui.spam;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.telecom.DisconnectCause;
+import android.text.TextUtils;
+
+import com.android.dialer.calllog.CallLogAsyncTaskUtil;
+import com.android.incallui.Call;
+import com.android.incallui.CallList;
+import com.android.incallui.Log;
+
+public class SpamCallListListener implements CallList.Listener {
+    private static final String TAG  = "SpamCallListListener";
+
+    private final Context mContext;
+
+    public SpamCallListListener(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public void onIncomingCall(final Call call) {
+        String number = call.getNumber();
+        if (TextUtils.isEmpty(number)) {
+            return;
+        }
+        CallLogAsyncTaskUtil.getNumberInCallHistory(mContext, number,
+                new CallLogAsyncTaskUtil.OnGetNumberInCallHistoryListener() {
+                    @Override
+                    public void onComplete(boolean inCallHistory) {
+                        call.setCallHistoryStatus(inCallHistory ?
+                                Call.CALL_HISTORY_STATUS_PRESENT
+                                : Call.CALL_HISTORY_STATUS_NOT_PRESENT);
+                    }
+                });
+    }
+
+    @Override
+    public void onUpgradeToVideo(Call call) {}
+
+    @Override
+    public void onCallListChange(CallList callList) {}
+
+    @Override
+    public void onDisconnect(Call call) {
+        if (shouldShowAfterCallNotification(call)) {
+            showNotification(call.getNumber());
+        }
+    }
+
+    /**
+     * Posts the intent for displaying the after call spam notification to the user.
+     */
+    @VisibleForTesting
+    /* package */ void showNotification(String number) {
+        //TODO(mhashmi): build and show notifications here
+    }
+
+    /**
+     * Determines if the after call notification should be shown for the specified call.
+     */
+    private boolean shouldShowAfterCallNotification(Call call) {
+        String number = call.getNumber();
+        if (TextUtils.isEmpty(number)) {
+            return false;
+        }
+
+        Call.LogState logState = call.getLogState();
+        if (!logState.isIncoming) {
+            return false;
+        }
+
+        if (logState.duration <= 0) {
+            return false;
+        }
+
+        if (logState.contactLookupResult != Call.LogState.LOOKUP_NOT_FOUND
+                && logState.contactLookupResult != Call.LogState.LOOKUP_UNKNOWN) {
+            return false;
+        }
+
+        int callHistoryStatus = call.getCallHistoryStatus();
+        if (callHistoryStatus == Call.CALL_HISTORY_STATUS_PRESENT) {
+            return false;
+        } else if (callHistoryStatus == Call.CALL_HISTORY_STATUS_UNKNOWN) {
+            Log.i(TAG, "Call history status is unknown, returning false");
+            return false;
+        }
+
+        // Check if call disconnected because of either user hanging up
+        int disconnectCause = call.getDisconnectCause().getCode();
+        if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) {
+            return false;
+        }
+
+        Log.i(TAG, "shouldShowAfterCallNotification, returning true");
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java b/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java
new file mode 100644
index 0000000..fb0b460
--- /dev/null
+++ b/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2016 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.incallui.spam;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.any;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.CallLog;
+import android.telecom.DisconnectCause;
+import android.test.InstrumentationTestCase;
+
+import com.android.contacts.common.test.mocks.ContactsMockContext;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.android.dialer.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.util.AsyncTaskExecutors;
+import com.android.dialer.util.FakeAsyncTaskExecutor;
+import com.android.dialer.util.TelecomUtil;
+import com.android.incallui.Call;
+import com.android.incallui.Call.CallHistoryStatus;
+import com.android.incallui.Call.LogState;
+
+public class SpamCallListListenerTest extends InstrumentationTestCase {
+    private static final String NUMBER = "+18005657862";
+    private static final int DURATION = 100;
+
+    private TestSpamCallListListener mListener;
+    private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
+    private ContactsMockContext mContext;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mContext = new ContactsMockContext(getInstrumentation().getContext(), CallLog.AUTHORITY);
+        mListener = new TestSpamCallListListener(mContext);
+        mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
+        AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        AsyncTaskExecutors.setFactoryForTest(null);
+        CallLogAsyncTaskUtil.resetForTest();
+        super.tearDown();
+    }
+
+    public void testOutgoingCall() {
+        Call call = getMockCall(NUMBER, false, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE);
+        mListener.onDisconnect(call);
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_UnknownNumber() {
+        Call call = getMockCall(null, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE);
+        mListener.onDisconnect(call);
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_Rejected() {
+        Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.REJECTED);
+        mListener.onDisconnect(call);
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+    public void testIncomingCall_HangUpLocal() {
+        Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL);
+        mListener.onDisconnect(call);
+        assertTrue(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_HangUpRemote() {
+        Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE);
+        mListener.onDisconnect(call);
+        assertTrue(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_ValidNumber_NotInCallHistory_InContacts() {
+        Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_LOCAL_CONTACT, DisconnectCause.REJECTED);
+        mListener.onDisconnect(call);
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_ValidNumber_InCallHistory_InContacts() {
+        Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_PRESENT,
+                LogState.LOOKUP_LOCAL_CONTACT, DisconnectCause.REJECTED);
+        mListener.onDisconnect(call);
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_ValidNumber_InCallHistory_NotInContacts() {
+        Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.REJECTED);
+        mListener.onDisconnect(call);
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_ValidNumber_NotInCallHistory_NotInContacts() throws Throwable {
+        Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL);
+        mListener.onDisconnect(call);
+        assertTrue(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_CheckCallHistory_NumberExists() throws Throwable {
+        final Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_UNKNOWN,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL);
+        expectCallLogQuery(NUMBER, true);
+        incomingCall(call);
+        verify(call).setCallHistoryStatus(eq(Call.CALL_HISTORY_STATUS_PRESENT));
+        assertFalse(mListener.mShowNotificationCalled);
+    }
+
+    public void testIncomingCall_CheckCallHistory_NumberNotExists() throws Throwable {
+        final Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_UNKNOWN,
+                LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL);
+        expectCallLogQuery(NUMBER, false);
+        incomingCall(call);
+        verify(call).setCallHistoryStatus(eq(Call.CALL_HISTORY_STATUS_NOT_PRESENT));
+        assertTrue(mListener.mShowNotificationCalled);
+    }
+
+    private void incomingCall(final Call call) throws Throwable {
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mListener.onIncomingCall(call);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+        mFakeAsyncTaskExecutor.runTask(CallLogAsyncTaskUtil.Tasks.GET_NUMBER_IN_CALL_HISTORY);
+        mListener.onDisconnect(call);
+    }
+
+    private void expectCallLogQuery(String number, boolean inCallHistory) {
+        MockContentProvider.Query query = mContext.getContactsProvider()
+                .expectQuery(TelecomUtil.getCallLogUri(mContext))
+                .withSelection(CallLog.Calls.NUMBER + " = ?", number)
+                .withProjection(CallLog.Calls._ID)
+                .withAnySortOrder();
+        ContentValues values = new ContentValues();
+        values.put(CallLog.Calls.NUMBER, number);
+        if (inCallHistory) {
+            query.returnRow(values);
+        } else {
+            query.returnEmptyCursor();
+        }
+    }
+
+    private static Call getMockCall(String number,
+                                    boolean isIncoming,
+                                    int duration,
+                                    @CallHistoryStatus int callHistoryStatus,
+                                    int contactLookupResult,
+                                    int disconnectCause) {
+        Call call = mock(Call.class);
+        LogState logState = new LogState();
+        logState.isIncoming = isIncoming;
+        logState.duration = duration;
+        logState.contactLookupResult = contactLookupResult;
+        when(call.getDisconnectCause()).thenReturn(new DisconnectCause(disconnectCause));
+        when(call.getLogState()).thenReturn(logState);
+        when(call.getNumber()).thenReturn(number);
+        doCallRealMethod().when(call).setCallHistoryStatus(anyInt());
+        when(call.getCallHistoryStatus()).thenCallRealMethod();
+        call.setCallHistoryStatus(callHistoryStatus);
+        return call;
+    }
+
+    private static class TestSpamCallListListener extends SpamCallListListener {
+        private boolean mShowNotificationCalled;
+
+        public TestSpamCallListListener(Context context) {
+            super(context);
+        }
+
+        void showNotification(String number) {
+            mShowNotificationCalled = true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
index 34b2f0e..b95d58e 100644
--- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -17,6 +17,7 @@
 package com.android.dialer.calllog;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -58,7 +59,8 @@
         MARK_VOICEMAIL_READ,
         MARK_CALL_READ,
         GET_CALL_DETAILS,
-        UPDATE_DURATION
+        UPDATE_DURATION,
+        GET_NUMBER_IN_CALL_HISTORY
     }
 
     private static final class CallDetailQuery {
@@ -122,6 +124,10 @@
         void onGetCallDetails(PhoneCallDetails[] details);
     }
 
+    public interface OnGetNumberInCallHistoryListener {
+        void onComplete(boolean inCallHistory);
+    }
+
     public interface OnCallLogQueryFinishedListener {
         void onQueryFinished(boolean hasEntry);
     }
@@ -456,6 +462,42 @@
         });
     }
 
+    /**
+     * Checks if the number is in the call history.
+     */
+    public static void getNumberInCallHistory(
+            final Context context,
+            final String number,
+            final OnGetNumberInCallHistoryListener listener) {
+        Preconditions.checkNotNull(listener);
+        if (!PermissionsUtil.hasPhonePermissions(context)) {
+            return;
+        }
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.GET_NUMBER_IN_CALL_HISTORY,
+                new AsyncTask<Void, Void, Boolean>() {
+                    @Override
+                    public Boolean doInBackground(Void... params) {
+                        try (Cursor cursor = context.getContentResolver().query(
+                                TelecomUtil.getCallLogUri(context),
+                                new String[] {CallLog.Calls._ID},
+                                CallLog.Calls.NUMBER + " = ?",
+                                new String[] {number},
+                                null)) {
+                            return cursor != null && cursor.getCount() > 0;
+                        }
+                    }
+
+                    @Override
+                    public void onPostExecute(Boolean inCallHistory) {
+                        listener.onComplete(inCallHistory);
+                    }
+                });
+    }
+
     @VisibleForTesting
     public static void resetForTest() {
         sAsyncTaskExecutor = null;