Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5442ec21 authored by Grace Jia's avatar Grace Jia Committed by android-build-merger
Browse files

Merge "Added implementation of call filter graph to perform call filters...

Merge "Added implementation of call filter graph to perform call filters following certain DAG flow." am: 74ab5e36 am: 625d0847
am: f11d3278

Change-Id: I1540f0b67c66fbaeed0ffdd29dfc50975b50e09f
parents fa5d5c63 f11d3278
Loading
Loading
Loading
Loading
+77 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+6 −5
Original line number Diff line number Diff line
@@ -79,8 +79,9 @@ public class CallFilteringResult {

        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 class CallFilteringResult {
    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;
+154 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+178 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}