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