Merge "Added implementation of call filter graph to perform call filters following certain DAG flow." am: 74ab5e36c4 am: 625d084747
am: f11d32788f
Change-Id: I1540f0b67c66fbaeed0ffdd29dfc50975b50e09f
diff --git a/src/com/android/server/telecom/callfiltering/CallFilter.java b/src/com/android/server/telecom/callfiltering/CallFilter.java
new file mode 100644
index 0000000..4b79439
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/CallFilter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.server.telecom.callfiltering;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class CallFilter {
+ private List<CallFilter> mDependencies;
+ private List<CallFilter> mFollowings;
+ private int mIndegree;
+ public CallFilteringResult mPriorStageResult;
+ public CallFilteringResult result;
+ private CompletableFuture<CallFilteringResult> mResultFuture;
+
+ public CallFilter() {
+ mDependencies = new ArrayList<>();
+ mFollowings = new ArrayList<>();
+ mPriorStageResult = null;
+ }
+
+ public CompletionStage<CallFilteringResult> startFilterLookup(
+ CallFilteringResult priorStageResult) {
+ return CompletableFuture.completedFuture(priorStageResult);
+ }
+
+ List<CallFilter> getDependencies() {
+ return mDependencies;
+ }
+
+ void addDependency(CallFilter filter) {
+ synchronized (this) {
+ mDependencies.add(filter);
+ mIndegree = mDependencies.size();
+ }
+ }
+
+ List<CallFilter> getFollowings() {
+ return mFollowings;
+ }
+
+ void addFollowings(CallFilter filter) {
+ mFollowings.add(filter);
+ }
+
+ int decrementAndGetIndegree() {
+ synchronized (this) {
+ mIndegree--;
+ return mIndegree;
+ }
+ }
+
+ public CallFilteringResult getResult() {
+ if (result == null) {
+ throw new NullPointerException("Result of this filter is null. This filter hasn't "
+ + "finished performing");
+ } else {
+ return result;
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/callfiltering/CallFilteringResult.java b/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
index c17a256..4bad7b3 100644
--- a/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
+++ b/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
@@ -79,8 +79,9 @@
public CallFilteringResult build() {
return new CallFilteringResult(mShouldAllowCall, mShouldReject, mShouldSilence,
- mShouldAddToCallLog, mShouldShowNotification, mShouldScreenViaAudio,
- mCallBlockReason, mCallScreeningAppName, mCallScreeningComponentName);
+ mShouldAddToCallLog, mShouldShowNotification, mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName,
+ mShouldScreenViaAudio);
}
}
@@ -95,9 +96,9 @@
public String mCallScreeningComponentName;
private CallFilteringResult(boolean shouldAllowCall, boolean shouldReject, boolean
- shouldSilence, boolean shouldAddToCallLog, boolean shouldShowNotification,
- boolean shouldScreenViaAudio, int callBlockReason, CharSequence callScreeningAppName,
- String callScreeningComponentName) {
+ shouldSilence, boolean shouldAddToCallLog, boolean shouldShowNotification, int
+ callBlockReason, CharSequence callScreeningAppName, String callScreeningComponentName,
+ boolean shouldScreenViaAudio) {
this.shouldAllowCall = shouldAllowCall;
this.shouldReject = shouldReject;
this.shouldSilence = shouldSilence;
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
new file mode 100644
index 0000000..fededed
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 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.server.telecom.callfiltering;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.telecom.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+public class IncomingCallFilterGraph {
+ //TODO: Add logging for control flow.
+ public static final String TAG = "IncomingCallFilterGraph";
+ public static final CallFilteringResult DEFAULT_SCREENING_RESULT =
+ new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldReject(false)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true)
+ .build();
+
+ private final CallFilterResultCallback mListener;
+ private final Call mCall;
+ private final Handler mHandler;
+ private final HandlerThread mHandlerThread;
+ private final TelecomSystem.SyncRoot mLock;
+ private List<CallFilter> mFiltersList;
+ private Executor mExecutor;
+ private CallFilter mDummyComplete;
+ private boolean mFinished;
+ private CallFilteringResult mCurrentResult;
+ private Context mContext;
+ private Timeouts.Adapter mTimeoutsAdapter;
+
+ private class PostFilterTask {
+ private final CallFilter mFilter;
+
+ public PostFilterTask(final CallFilter filter) {
+ mFilter = filter;
+ }
+
+ public CallFilteringResult whenDone(CallFilteringResult result) {
+ mFilter.result = result;
+ for (CallFilter filter : mFilter.getFollowings()) {
+ if (filter.decrementAndGetIndegree() == 0) {
+ scheduleFilter(filter);
+ }
+ }
+ if (mFilter.equals(mDummyComplete)) {
+ synchronized (mLock) {
+ mFinished = true;
+ mListener.onCallFilteringComplete(mCall, result);
+ }
+ mHandlerThread.quit();
+ }
+ return result;
+ }
+ }
+
+ public IncomingCallFilterGraph(Call call, CallFilterResultCallback listener, Context context,
+ Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock) {
+ mListener = listener;
+ mCall = call;
+ mFiltersList = new ArrayList<>();
+
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mExecutor = mHandler::post;
+ mFinished = false;
+ mContext = context;
+ mTimeoutsAdapter = timeoutsAdapter;
+ mCurrentResult = DEFAULT_SCREENING_RESULT;
+ mLock = lock;
+ }
+
+ public void addFilter(CallFilter filter) {
+ mFiltersList.add(filter);
+ }
+
+ public void performFiltering() {
+
+ CallFilter dummyStart = new CallFilter();
+ mDummyComplete = new CallFilter();
+
+ for (CallFilter filter : mFiltersList) {
+ addEdge(dummyStart, filter);
+ }
+ for (CallFilter filter : mFiltersList) {
+ addEdge(filter, mDummyComplete);
+ }
+ addEdge(dummyStart, mDummyComplete);
+
+ scheduleFilter(dummyStart);
+ mHandler.postDelayed(() -> {
+ synchronized(mLock) {
+ if (!mFinished) {
+ Log.i(this, "Graph timed out when perform filtering.");
+ mListener.onCallFilteringComplete(mCall, mCurrentResult);
+ mFinished = true;
+ mHandlerThread.quit();
+ }
+ }}, mTimeoutsAdapter.getCallScreeningTimeoutMillis(mContext.getContentResolver()));
+ }
+
+ private void scheduleFilter(CallFilter filter) {
+ CallFilteringResult result = new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldReject(false)
+ .setShouldSilence(false)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true)
+ .build();
+ for (CallFilter dependencyFilter : filter.getDependencies()) {
+ result = result.combine(dependencyFilter.getResult());
+ }
+ mCurrentResult = result;
+ final CallFilteringResult input = result;
+
+ CompletableFuture<CallFilteringResult> startFuture =
+ CompletableFuture.completedFuture(input);
+ PostFilterTask postFilterTask = new PostFilterTask(filter);
+
+ startFuture.thenComposeAsync(filter::startFilterLookup, mExecutor)
+ .thenApplyAsync(postFilterTask::whenDone, mExecutor);
+ }
+
+ public static void addEdge(CallFilter before, CallFilter after) {
+ before.addFollowings(after);
+ after.addDependency(before);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
new file mode 100644
index 0000000..8c0adfb
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2019 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.server.telecom.tests;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callfiltering.CallFilter;
+import com.android.server.telecom.callfiltering.CallFilterResultCallback;
+import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.junit.Test;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class IncomingCallFilterGraphTest extends TelecomTestCase {
+ @Mock private Call mCall;
+ @Mock private Context mContext;
+ @Mock private Timeouts.Adapter mTimeoutsAdapter;
+ private TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
+
+ private static final CallFilteringResult PASS_CALL_RESULT = new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldReject(false)
+ .setShouldSilence(false)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true).build();
+ private static final CallFilteringResult REJECT_CALL_RESULT = new CallFilteringResult.Builder()
+ .setShouldAllowCall(false)
+ .setShouldReject(true)
+ .setShouldSilence(false)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true).build();
+ private final long FILTER_TIMEOUT = 5000;
+ private final long TEST_TIMEOUT = 7000;
+ private final long TIMEOUT_FILTER_SLEEP_TIME = 10000;
+
+ private class AllowFilter extends CallFilter {
+ @Override
+ public CompletionStage<CallFilteringResult> startFilterLookup(
+ CallFilteringResult priorStageResult) {
+ return CompletableFuture.completedFuture(PASS_CALL_RESULT);
+ }
+ }
+
+ private class DisallowFilter extends CallFilter {
+ @Override
+ public CompletionStage<CallFilteringResult> startFilterLookup(
+ CallFilteringResult priorStageResult) {
+ return CompletableFuture.completedFuture(REJECT_CALL_RESULT);
+ }
+ }
+
+ private class TimeoutFilter extends CallFilter {
+ @Override
+ public CompletionStage<CallFilteringResult> startFilterLookup(
+ CallFilteringResult priorStageResult) {
+ HandlerThread handlerThread = new HandlerThread("TimeoutFilter");
+ handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
+
+ CompletableFuture<CallFilteringResult> resultFuture = new CompletableFuture<>();
+ handler.postDelayed(() -> resultFuture.complete(PASS_CALL_RESULT),
+ TIMEOUT_FILTER_SLEEP_TIME);
+ return CompletableFuture.completedFuture(PASS_CALL_RESULT);
+ }
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ when(mContext.getContentResolver()).thenReturn(null);
+ when(mTimeoutsAdapter.getCallScreeningTimeoutMillis(nullable(ContentResolver.class)))
+ .thenReturn(FILTER_TIMEOUT);
+
+ }
+
+ @SmallTest
+ @Test
+ public void testEmptyGraph() throws Exception {
+ CompletableFuture<CallFilteringResult> testResult = new CompletableFuture<>();
+ CallFilterResultCallback listener = (call, result) -> testResult.complete(result);
+
+ IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
+ mTimeoutsAdapter, mLock);
+ graph.performFiltering();
+
+ assertEquals(PASS_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ @SmallTest
+ @Test
+ public void testFiltersPerformOrder() throws Exception {
+ CompletableFuture<CallFilteringResult> testResult = new CompletableFuture<>();
+ CallFilterResultCallback listener = (call, result) -> testResult.complete(result);
+
+ IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
+ mTimeoutsAdapter, mLock);
+ AllowFilter allowFilter = new AllowFilter();
+ DisallowFilter disallowFilter = new DisallowFilter();
+ graph.addFilter(allowFilter);
+ graph.addFilter(disallowFilter);
+ IncomingCallFilterGraph.addEdge(allowFilter, disallowFilter);
+ graph.performFiltering();
+
+ assertEquals(REJECT_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ @SmallTest
+ @Test
+ public void testFiltersPerformInParallel() throws Exception {
+ CompletableFuture<CallFilteringResult> testResult = new CompletableFuture<>();
+ CallFilterResultCallback listener = (call, result) -> testResult.complete(result);
+
+ IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
+ mTimeoutsAdapter, mLock);
+ AllowFilter allowFilter1 = new AllowFilter();
+ AllowFilter allowFilter2 = new AllowFilter();
+ DisallowFilter disallowFilter = new DisallowFilter();
+ graph.addFilter(allowFilter1);
+ graph.addFilter(allowFilter2);
+ graph.addFilter(disallowFilter);
+ graph.performFiltering();
+
+ assertEquals(REJECT_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ @SmallTest
+ @Test
+ public void testFiltersTimeout() throws Exception {
+ CompletableFuture<CallFilteringResult> testResult = new CompletableFuture<>();
+ CallFilterResultCallback listener = (call, result) -> testResult.complete(result);
+
+ IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
+ mTimeoutsAdapter, mLock);
+ DisallowFilter disallowFilter = new DisallowFilter();
+ TimeoutFilter timeoutFilter = new TimeoutFilter();
+ graph.addFilter(disallowFilter);
+ graph.addFilter(timeoutFilter);
+ IncomingCallFilterGraph.addEdge(disallowFilter, timeoutFilter);
+ graph.performFiltering();
+
+ assertEquals(REJECT_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+}