Merge "Resolve baseline call audio routing" into main
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
index d80cfa3..693d727 100644
--- a/flags/telecom_call_filtering_flags.aconfig
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -7,4 +7,15 @@
   namespace: "telecom"
   description: "Gates whether to still perform Dnd filter when phone account has skip_filter call extra."
   bug: "222333869"
-}
\ No newline at end of file
+}
+
+# OWNER=tjstuart TARGET=25Q1
+flag {
+  name: "check_completed_filters_on_timeout"
+  namespace: "telecom"
+  description: "If the Filtering Graph times out, combine the finished results"
+  bug: "364946812"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 1e6d2bc..04f93fc 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -913,7 +913,7 @@
         DndCallFilter dndCallFilter = new DndCallFilter(incomingHfpCall, mRinger);
         IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(
                 incomingHfpCall,
-                this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
+                this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mFeatureFlags, mLock);
         graph.addFilter(dndCallFilter);
         mGraphHandlerThreads.add(graph.getHandlerThread());
         return graph;
@@ -932,7 +932,7 @@
         ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
 
         IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall,
-                this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
+                this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mFeatureFlags, mLock);
         DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
                 mCallerInfoLookupHelper);
         BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
index d79e80e..a606a4d 100644
--- a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
@@ -27,6 +27,7 @@
 import com.android.server.telecom.LogUtils;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -55,6 +56,7 @@
     private CallFilteringResult mCurrentResult;
     private Context mContext;
     private Timeouts.Adapter mTimeoutsAdapter;
+    private final FeatureFlags mFeatureFlags;
 
     private class PostFilterTask {
         private final CallFilter mFilter;
@@ -84,11 +86,12 @@
     }
 
     public IncomingCallFilterGraph(Call call, CallFilterResultCallback listener, Context context,
-            Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock) {
+            Timeouts.Adapter timeoutsAdapter, FeatureFlags featureFlags,
+            TelecomSystem.SyncRoot lock) {
         mListener = listener;
         mCall = call;
         mFiltersList = new ArrayList<>();
-
+        mFeatureFlags = featureFlags;
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
@@ -121,8 +124,8 @@
             @Override
             public void loggedRun() {
                 if (!mFinished) {
-                    Log.i(this, "Graph timed out when performing filtering.");
                     Log.addEvent(mCall, LogUtils.Events.FILTERING_TIMED_OUT);
+                    mCurrentResult = onTimeoutCombineFinishedFilters(mFiltersList, mCurrentResult);
                     mListener.onCallFilteringComplete(mCall, mCurrentResult, true);
                     mFinished = true;
                     mHandlerThread.quit();
@@ -137,6 +140,28 @@
         }.prepare(), mTimeoutsAdapter.getCallScreeningTimeoutMillis(mContext.getContentResolver()));
     }
 
+    /**
+     * This helper takes all the call filters that were added to the graph, checks if filters have
+     * finished, and combines the results.
+     *
+     * @param filtersList   all the CallFilters that were added to the call
+     * @param currentResult the current call filter result
+     * @return CallFilterResult of the combined finished Filters.
+     */
+    private CallFilteringResult onTimeoutCombineFinishedFilters(
+            List<CallFilter> filtersList,
+            CallFilteringResult currentResult) {
+        if (!mFeatureFlags.checkCompletedFiltersOnTimeout()) {
+            return currentResult;
+        }
+        for (CallFilter filter : filtersList) {
+            if (filter.result != null) {
+                currentResult = currentResult.combine(filter.result);
+            }
+        }
+        return currentResult;
+    }
+
     private void scheduleFilter(CallFilter filter) {
         CallFilteringResult result = new CallFilteringResult.Builder()
                 .setShouldAllowCall(true)
@@ -147,6 +172,9 @@
                 .setDndSuppressed(false)
                 .build();
         for (CallFilter dependencyFilter : filter.getDependencies()) {
+            // When sequential nodes are completed, they are combined progressively.
+            // ex.) node_a --> node_b  --> node_c
+            // node_a will combine with node_b before starting node_c
             result = result.combine(dependencyFilter.getResult());
         }
         mCurrentResult = result;
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
index 1501280..4424178 100644
--- a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
@@ -21,6 +21,7 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.FeatureFlags;
 
 /**
  * Interface to provide a {@link IncomingCallFilterGraph}. This class serve for unit test purpose
@@ -35,10 +36,13 @@
      * @param listener Callback object to trigger when filtering is done.
      * @param context An android context.
      * @param timeoutsAdapter Adapter to provide timeout value for call filtering.
+     * @param featureFlags Telecom flags
      * @param lock Telecom lock.
      * @return
      */
     IncomingCallFilterGraph createGraph(Call call, CallFilterResultCallback listener,
             Context context,
-            Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock);
+            Timeouts.Adapter timeoutsAdapter,
+            FeatureFlags featureFlags,
+            TelecomSystem.SyncRoot lock);
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index baf0208..74f33c3 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -392,7 +392,8 @@
                 mBluetoothDeviceManager,
                 mFeatureFlags,
                 mTelephonyFlags,
-                (call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);
+                (call, listener, context, timeoutsAdapter,
+                        mFeatureFlags, lock) -> mIncomingCallFilterGraph);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
index 66ac553..d7905b2 100644
--- a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
@@ -17,22 +17,28 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Log;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
+import com.android.server.telecom.Ringer;
 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.DndCallFilter;
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
 
 import org.junit.Before;
@@ -47,6 +53,7 @@
 
 @RunWith(JUnit4.class)
 public class IncomingCallFilterGraphTest extends TelecomTestCase {
+    private final String TAG = IncomingCallFilterGraphTest.class.getSimpleName();
     @Mock private Call mCall;
     @Mock private Context mContext;
     @Mock private Timeouts.Adapter mTimeoutsAdapter;
@@ -88,13 +95,15 @@
         @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);
+            Log.i(TAG, "TimeoutFilter: startFilterLookup: about to sleep");
+            try {
+                // Currently, there are no tools to fake a timeout with [CompletableFuture]s
+                // in the Android Platform. Thread sleep is the best option for an end-to-end test.
+                Thread.sleep(FILTER_TIMEOUT); // Simulate a filter timeout
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            Log.i(TAG, "TimeoutFilter: startFilterLookup: continuing test");
             return CompletableFuture.completedFuture(PASS_CALL_RESULT);
         }
     }
@@ -116,7 +125,7 @@
         CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
 
         IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
-                mTimeoutsAdapter, mLock);
+                mTimeoutsAdapter, mFeatureFlags, mLock);
         graph.performFiltering();
 
         assertEquals(PASS_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
@@ -129,7 +138,7 @@
         CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
 
         IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
-                mTimeoutsAdapter, mLock);
+                mTimeoutsAdapter, mFeatureFlags, mLock);
         AllowFilter allowFilter = new AllowFilter();
         DisallowFilter disallowFilter = new DisallowFilter();
         graph.addFilter(allowFilter);
@@ -147,7 +156,7 @@
         CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
 
         IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
-                mTimeoutsAdapter, mLock);
+                mTimeoutsAdapter, mFeatureFlags, mLock);
         AllowFilter allowFilter1 = new AllowFilter();
         AllowFilter allowFilter2 = new AllowFilter();
         DisallowFilter disallowFilter = new DisallowFilter();
@@ -166,7 +175,7 @@
         CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
 
         IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
-                mTimeoutsAdapter, mLock);
+                mTimeoutsAdapter, mFeatureFlags, mLock);
         DisallowFilter disallowFilter = new DisallowFilter();
         TimeoutFilter timeoutFilter = new TimeoutFilter();
         graph.addFilter(disallowFilter);
@@ -176,4 +185,57 @@
 
         assertEquals(REJECT_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
     }
+
+    /**
+     * Verify that when the Call Filtering Graph times out, already completed filters are combined.
+     * Graph being tested:
+     *
+     * startFilterLookup --> [ ALLOW_FILTER ]
+     *                            |
+     *         ---------------------------------
+     *        |                                |
+     *        |                                |
+     *    [DND_FILTER]                  [TIMEOUT_FILTER]
+     *        |                                |
+     *        |                        * timeout at 5 seconds *
+     *        |
+     *        |
+     *       --------[ CallFilteringResult ]
+     */
+    @SmallTest
+    @Test
+    public void testFilterTimesOutWithDndFilterComputedAlready() throws Exception {
+        // GIVEN: a graph that is set up like the above diagram in the test comment
+        Ringer mockRinger = mock(Ringer.class);
+        CompletableFuture<CallFilteringResult> testResult = new CompletableFuture<>();
+        IncomingCallFilterGraph graph = new IncomingCallFilterGraph(
+                mCall,
+                (call, result, timeout) -> testResult.complete(result),
+                mContext,
+                mTimeoutsAdapter,
+                mFeatureFlags,
+                mLock);
+        // create the filters / nodes  for the graph
+        TimeoutFilter timeoutFilter = new TimeoutFilter();
+        DndCallFilter dndCallFilter = new DndCallFilter(mCall, mockRinger);
+        AllowFilter allowFilter1 = new AllowFilter();
+        // adding them to the graph does not create the edges
+        graph.addFilter(allowFilter1);
+        graph.addFilter(timeoutFilter);
+        graph.addFilter(dndCallFilter);
+        // set up the graph so that the DND filter can process in parallel to the timeout
+        IncomingCallFilterGraph.addEdge(allowFilter1, dndCallFilter);
+        IncomingCallFilterGraph.addEdge(allowFilter1, timeoutFilter);
+
+        // WHEN:  DND is on and the caller cannot interrupt and the graph is processed
+        when(mockRinger.shouldRingForContact(mCall)).thenReturn(false);
+        when(mFeatureFlags.checkCompletedFiltersOnTimeout()).thenReturn(true);
+        dndCallFilter.startFilterLookup(IncomingCallFilterGraph.DEFAULT_RESULT);
+        graph.performFiltering();
+
+        // THEN: assert shouldSuppressCallDueToDndStatus is true!
+        assertFalse(IncomingCallFilterGraph.DEFAULT_RESULT.shouldSuppressCallDueToDndStatus);
+        assertTrue(testResult.get(TIMEOUT_FILTER_SLEEP_TIME,
+                TimeUnit.MILLISECONDS).shouldSuppressCallDueToDndStatus);
+    }
 }