diff options
5 files changed, 408 insertions, 11 deletions
diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java index a27efec76..252c7016d 100644 --- a/InCallUI/src/com/android/incallui/Call.java +++ b/InCallUI/src/com/android/incallui/Call.java @@ -21,6 +21,7 @@ import android.hardware.camera2.CameraCharacteristics; 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.contacts.common.testing.NeededForTesting; 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 @@ import java.util.Objects; */ @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 @@ public class Call { 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 @@ public class Call { 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 class Call { 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 189049c47..4442cbcd2 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.filterednumber.FilteredNumbersUtil; 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 @@ public class InCallPresenter implements CallList.Listener, 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 @@ public class InCallPresenter implements CallList.Listener, // 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 @@ public class InCallPresenter implements CallList.Listener, 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 000000000..b97f4d099 --- /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 000000000..fb0b460b6 --- /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 34b2f0ea9..b95d58e26 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 @@ public class CallLogAsyncTaskUtil { 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 @@ public class CallLogAsyncTaskUtil { void onGetCallDetails(PhoneCallDetails[] details); } + public interface OnGetNumberInCallHistoryListener { + void onComplete(boolean inCallHistory); + } + public interface OnCallLogQueryFinishedListener { void onQueryFinished(boolean hasEntry); } @@ -456,6 +462,42 @@ public class CallLogAsyncTaskUtil { }); } + /** + * 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; |