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

Commit 53ab7297 authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Call Screening and Caller ID changes

- Fix bug where call screening service doesn't log calls.
Ensure dialer and 3p app screened calls are logged.
- Add ability to bind to a call screening service for outgoing
calls in order for it to provide outgoing caller id.
- Update test call screening service to support incoming and
outgoing calls.
- Refactor some call screening service filter code to make better
reuse of binding logic.

Bug: 63966743
Test: Manual, CTS
Merged-In: I86d440fb45c08da01ce2159441d2b9efc53eb27c
Change-Id: I86d440fb45c08da01ce2159441d2b9efc53eb27c
parent e3c69d31
Loading
Loading
Loading
Loading
+247 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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;

import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.CallIdentification;
import android.telecom.CallScreeningService;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.text.TextUtils;

import com.android.internal.telecom.ICallScreeningAdapter;
import com.android.internal.telecom.ICallScreeningService;

import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * Helper class for performing operations with {@link CallScreeningService}s.
 */
public class CallScreeningServiceHelper {
    private static final String TAG = CallScreeningServiceHelper.class.getSimpleName();

    /**
     * Abstracts away dependency on the {@link PackageManager} required to fetch the label for an
     * app.
     */
    public interface AppLabelProxy {
        String getAppLabel(String packageName);
    }

    /**
     * Implementation of {@link CallScreeningService} adapter AIDL; provides a means for responses
     * from the call screening service to be handled.
     */
    private class CallScreeningAdapter extends ICallScreeningAdapter.Stub {
        @Override
        public void allowCall(String s) throws RemoteException {
            // no-op; we don't allow this on outgoing calls.
        }

        @Override
        public void disallowCall(String s, boolean b, boolean b1, boolean b2,
                ComponentName componentName) throws RemoteException {
            // no-op; we don't allow this on outgoing calls.
        }

        @Override
        public void provideCallIdentification(String callId, CallIdentification callIdentification)
                throws RemoteException {
            Log.startSession("CSA.pCI");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mTelecomLock) {
                    if (mCall != null && mCall.getId().equals(callId)) {
                        Log.i(TAG, "provideCallIdentification - got call ID");
                        callIdentification.setCallScreeningAppName(mAppLabelProxy.getAppLabel(
                                mPackageName));
                        callIdentification.setCallScreeningPackageName(mPackageName);
                        mFuture.complete(callIdentification);
                    }
                }
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
            mFuture.complete(null);
        }
    }

    private final ParcelableCallUtils.Converter mParcelableCallUtilsConverter;
    private final TelecomSystem.SyncRoot mTelecomLock;
    private final Call mCall;
    private final UserHandle mUserHandle;
    private final Context mContext;
    private final AppLabelProxy mAppLabelProxy;
    private final Session mLoggingSession;
    private CompletableFuture<CallIdentification> mFuture;
    private String mPackageName;

    public CallScreeningServiceHelper(Context context, TelecomSystem.SyncRoot telecomLock,
            String packageName, ParcelableCallUtils.Converter converter,
            UserHandle userHandle, Call call, AppLabelProxy appLabelProxy) {
        mContext = context;
        mTelecomLock = telecomLock;
        mParcelableCallUtilsConverter = converter;
        mCall = call;
        mUserHandle = userHandle;
        mPackageName = packageName;
        mAppLabelProxy = appLabelProxy;
        mLoggingSession = Log.createSubsession();
    }

    /**
     * Builds a {@link CompletableFuture} which performs a bind to a {@link CallScreeningService}
     * @return
     */
    public CompletableFuture<CallIdentification> process() {
        Log.d(this, "process");
        return bindAndGetCallIdentification();
    }

    public CompletableFuture<CallIdentification> bindAndGetCallIdentification() {
        Log.d(this, "bindAndGetCallIdentification");
        if (mPackageName == null) {
            return CompletableFuture.completedFuture(null);
        }

        mFuture = new CompletableFuture<>();

        ServiceConnection serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                ICallScreeningService screeningService =
                        ICallScreeningService.Stub.asInterface(service);
                Log.continueSession(mLoggingSession, "CSSH.oSC");
                try {
                    try {
                        screeningService.screenCall(new CallScreeningAdapter(),
                                mParcelableCallUtilsConverter.toParcelableCallForScreening(mCall));
                    } catch (RemoteException e) {
                        Log.w(CallScreeningServiceHelper.this,
                                "Cancelling call id due to remote exception");
                        mFuture.complete(null);
                    }
                } finally {
                    Log.endSession();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                // No locking needed -- CompletableFuture only lets one thread call complete.
                Log.continueSession(mLoggingSession, "CSSH.oSD");
                try {
                    if (!mFuture.isDone()) {
                        Log.w(CallScreeningServiceHelper.this,
                                "Cancelling outgoing call screen due to service disconnect.");
                    }
                    mFuture.complete(null);
                } finally {
                    Log.endSession();
                }
            }
        };

        if (!bindCallScreeningService(mContext, mUserHandle, mPackageName, serviceConnection)) {
            Log.i(this, "bindAndGetCallIdentification - bind failed");
            Log.addEvent(mCall, LogUtils.Events.BIND_SCREENING, mPackageName);
            mFuture.complete(null);
        }

        // Set up a timeout so that we're not waiting forever for the caller ID information.
        Handler handler = new Handler();
        handler.postDelayed(() -> {
                    // No locking needed -- CompletableFuture only lets one thread call complete.
                    Log.continueSession(mLoggingSession, "CSSH.timeout");
                    try {
                        if (!mFuture.isDone()) {
                            Log.w(TAG, "Cancelling call id process due to timeout");
                        }
                        mFuture.complete(null);
                    } finally {
                        Log.endSession();
                    }
                },
                Timeouts.getCallScreeningTimeoutMillis(mContext.getContentResolver()));
        return mFuture;
    }

    /**
     * Binds to a {@link CallScreeningService}.
     * @param context The current context.
     * @param userHandle User to bind as.
     * @param packageName Package name of the {@link CallScreeningService}.
     * @param serviceConnection The {@link ServiceConnection} to be notified of binding.
     * @return {@code true} if binding succeeds, {@code false} otherwise.
     */
    public static boolean bindCallScreeningService(Context context, UserHandle userHandle,
            String packageName, ServiceConnection serviceConnection) {
        if (TextUtils.isEmpty(packageName)) {
            Log.i(TAG, "PackageName is empty. Not performing call screening.");
            return false;
        }

        Intent intent = new Intent(CallScreeningService.SERVICE_INTERFACE)
                .setPackage(packageName);
        List<ResolveInfo> entries = context.getPackageManager().queryIntentServicesAsUser(
                intent, 0, userHandle.getIdentifier());
        if (entries.isEmpty()) {
            Log.i(TAG, packageName + " has no call screening service defined.");
            return false;
        }

        ResolveInfo entry = entries.get(0);
        if (entry.serviceInfo == null) {
            Log.w(TAG, packageName + " call screening service has invalid service info");
            return false;
        }

        if (entry.serviceInfo.permission == null || !entry.serviceInfo.permission.equals(
                Manifest.permission.BIND_SCREENING_SERVICE)) {
            Log.w(TAG, "CallScreeningService must require BIND_SCREENING_SERVICE permission: " +
                    entry.serviceInfo.packageName);
            return false;
        }

        ComponentName componentName =
                new ComponentName(entry.serviceInfo.packageName, entry.serviceInfo.name);
        intent.setComponent(componentName);
        if (context.bindServiceAsUser(
                intent,
                serviceConnection,
                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
                UserHandle.CURRENT)) {
            Log.d(TAG, "bindService, found service, waiting for it to connect");
            return true;
        }

        return false;
    }
}
+43 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
import android.text.TextUtils;
import android.util.Pair;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.CallerInfo;
@@ -37,6 +38,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class CallerInfoLookupHelper {
    public interface OnQueryCompleteListener {
@@ -77,6 +79,47 @@ public class CallerInfoLookupHelper {
        mLock = lock;
    }

    /**
     * Generates a CompletableFuture which performs a contacts lookup asynchronously.  The future
     * returns a {@link Pair} containing the original handle which is being looked up and any
     * {@link CallerInfo} which was found.
     * @param handle
     * @return {@link CompletableFuture} to perform the contacts lookup.
     */
    public CompletableFuture<Pair<Uri, CallerInfo>> startLookup(final Uri handle) {
        // Create the returned future and
        final CompletableFuture<Pair<Uri, CallerInfo>> callerInfoFuture = new CompletableFuture<>();

        final String number = handle.getSchemeSpecificPart();
        if (TextUtils.isEmpty(number)) {
            // Nothing to do here, just finish.
            Log.d(CallerInfoLookupHelper.this, "onCallerInfoQueryComplete - no number; end early");
            callerInfoFuture.complete(new Pair<>(handle, null));
            return callerInfoFuture;
        }

        // Setup a query complete listener which will get the results of the contacts lookup.
        OnQueryCompleteListener listener = new OnQueryCompleteListener() {
            @Override
            public void onCallerInfoQueryComplete(Uri handle, CallerInfo info) {
                Log.d(CallerInfoLookupHelper.this, "onCallerInfoQueryComplete - found info for %s",
                        Log.piiHandle(handle));
                // Got something, so complete the future.
                callerInfoFuture.complete(new Pair<>(handle, info));
            }

            @Override
            public void onContactPhotoQueryComplete(Uri handle, CallerInfo info) {
                // No-op for now; not something this future cares about.
            }
        };

        // Start async lookup.
        startLookup(handle, listener);

        return callerInfoFuture;
    }

    public void startLookup(final Uri handle, OnQueryCompleteListener listener) {
        if (handle == null) {
            listener.onCallerInfoQueryComplete(handle, null);
+74 −1
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import android.provider.BlockedNumberContract.SystemContract;
import android.provider.CallLog.Calls;
import android.provider.Settings;
import android.telecom.CallAudioState;
import android.telecom.CallIdentification;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -614,7 +615,7 @@ public class CallsManager extends Call.ListenerBase
        filters.add(new CallScreeningServiceController(mContext, this, mPhoneAccountRegistrar,
                new ParcelableCallUtils.Converter(), mLock,
                new TelecomServiceImpl.SettingsSecureAdapterImpl(), mCallerInfoLookupHelper,
                new CallScreeningServiceController.AppLabelProxy() {
                new CallScreeningServiceHelper.AppLabelProxy() {
                    @Override
                    public String getAppLabel(String packageName) {
                        PackageManager pm = mContext.getPackageManager();
@@ -1447,6 +1448,34 @@ public class CallsManager extends Call.ListenerBase
                            return mPendingAccountSelection;
                        }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSPA"));

        // Potentially perform call identification for dialed TEL scheme numbers.
        if (PhoneAccount.SCHEME_TEL.equals(handle.getScheme())) {
            // Perform an asynchronous contacts lookup in this stage; ensure post-dial digits are
            // not included.
            CompletableFuture<Pair<Uri, CallerInfo>> contactLookupFuture =
                    mCallerInfoLookupHelper.startLookup(Uri.fromParts(handle.getScheme(),
                            PhoneNumberUtils.extractNetworkPortion(handle.getSchemeSpecificPart()),
                            null));

            // Once the phone account selection stage has completed, we can handle the results from
            // that with the contacts lookup in order to determine if we should lookup bind to the
            // CallScreeningService in order for it to potentially provide caller ID.
            dialerSelectPhoneAccountFuture.thenAcceptBothAsync(contactLookupFuture,
                    (callPhoneAccountHandlePair, uriCallerInfoPair) -> {
                        Call theCall = callPhoneAccountHandlePair.first;
                        boolean isInContacts = uriCallerInfoPair.second != null
                                && uriCallerInfoPair.second.contactExists;
                        Log.d(CallsManager.this, "outgoingCallIdStage: isInContacts=%s",
                                isInContacts);

                        // We only want to provide a CallScreeningService with a call if its not in
                        // contacts.
                        if (!isInContacts) {
                            performCallIdentification(theCall);
                        }
            }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.pCSB"));
        }

        // Finally, after all user interaction is complete, we execute this code to finish setting
        // up the outgoing call. The inner method always returns a completed future containing the
        // call that we've finished setting up.
@@ -1508,6 +1537,50 @@ public class CallsManager extends Call.ListenerBase
        return mLatestPostSelectionProcessingFuture;
    }

    /**
     * Performs call identification for an outgoing phone call.
     * @param theCall The outgoing call to perform identification.
     */
    private void performCallIdentification(Call theCall) {
        // Find the user chosen call screening app.
        String callScreeningApp =
                mRoleManagerAdapter.getDefaultCallScreeningApp();

        CompletableFuture<CallIdentification> future =
                new CallScreeningServiceHelper(mContext,
                mLock,
                callScreeningApp,
                new ParcelableCallUtils.Converter(),
                mCurrentUserHandle,
                theCall,
                new CallScreeningServiceHelper.AppLabelProxy() {
                    @Override
                    public String getAppLabel(String packageName) {
                        PackageManager pm = mContext.getPackageManager();
                        try {
                            ApplicationInfo info = pm.getApplicationInfo(
                                    packageName, 0);
                            return (String) pm.getApplicationLabel(info);
                        } catch (PackageManager.NameNotFoundException nnfe) {
                            Log.w(this, "Could not determine package name.");
                        }

                        return null;
                    }
                }).process();

        // When we are done, apply call identification to the call.
        future.thenApply(v -> {
            Log.i(CallsManager.this, "setting caller ID: %s", v);
            if (v != null) {
                synchronized (mLock) {
                    theCall.setCallIdentification(v);
                }
            }
            return null;
        });
    }

    /**
     * Finds the {@link PhoneAccountHandle}(s) which could potentially be used to place an outgoing
     * call.  Takes into account the following:
+25 −3
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package com.android.server.telecom;

import static android.telecom.Call.Details.DIRECTION_INCOMING;
import static android.telecom.Call.Details.DIRECTION_OUTGOING;
import static android.telecom.Call.Details.DIRECTION_UNKNOWN;

import android.net.Uri;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -167,6 +171,14 @@ public class ParcelableCallUtils {
        }

        ParcelableRttCall rttCall = includeRttCall ? getParcelableRttCall(call) : null;
        int callDirection;
        if (call.isIncoming()) {
            callDirection = DIRECTION_INCOMING;
        } else if (call.isUnknown()) {
            callDirection = DIRECTION_UNKNOWN;
        } else {
            callDirection = DIRECTION_OUTGOING;
        }

        return new ParcelableCall(
                call.getId(),
@@ -195,7 +207,8 @@ public class ParcelableCallUtils {
                call.getIntentExtras(),
                call.getExtras(),
                call.getCreationTimeMillis(),
                call.getCallIdentification());
                call.getCallIdentification(),
                callDirection);
    }

    /**
@@ -203,7 +216,7 @@ public class ParcelableCallUtils {
     * {@link android.telecom.CallScreeningService}.  We ONLY expose the following:
     * <ul>
     *     <li>Call Id (not exposed to public, but needed to associated calls)</li>
     *     <li>Call state</li>
     *     <li>Call directoin</li>
     *     <li>Creation time</li>
     *     <li>Connection time</li>
     *     <li>Handle (phone number)</li>
@@ -216,6 +229,14 @@ public class ParcelableCallUtils {
    public static ParcelableCall toParcelableCallForScreening(Call call) {
        Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
                call.getHandle() : null;
        int callDirection;
        if (call.isIncoming()) {
            callDirection = DIRECTION_INCOMING;
        } else if (call.isUnknown()) {
            callDirection = DIRECTION_UNKNOWN;
        } else {
            callDirection = DIRECTION_OUTGOING;
        }
        return new ParcelableCall(
                call.getId(),
                getParcelableState(call, false /* supportsExternalCalls */),
@@ -243,7 +264,8 @@ public class ParcelableCallUtils {
                null, /* intentExtras */
                null, /* callExtras */
                call.getCreationTimeMillis(),
                null /* callIdentification */);
                null /* callIdentification */,
                callDirection);
    }

    private static int getParcelableState(Call call, boolean supportsExternalCalls) {
+7 −13
Original line number Diff line number Diff line
@@ -18,14 +18,11 @@ package com.android.server.telecom.callfiltering;

import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.UserHandle;
import android.provider.CallLog;
import android.provider.Settings;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.TelecomManager;
@@ -34,6 +31,7 @@ import android.text.TextUtils;

import com.android.internal.telephony.CallerInfo;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallScreeningServiceHelper;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.LogUtils;
@@ -51,14 +49,6 @@ import com.android.server.telecom.TelecomSystem;
public class CallScreeningServiceController implements IncomingCallFilter.CallFilter,
        CallScreeningServiceFilter.CallScreeningFilterResultCallback {

    /**
     * Abstracts away dependency on the {@link PackageManager} required to fetch the label for an
     * app.
     */
    public interface AppLabelProxy {
        String getAppLabel(String packageName);
    }

    private final Context mContext;
    private final CallsManager mCallsManager;
    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
@@ -66,7 +56,7 @@ public class CallScreeningServiceController implements IncomingCallFilter.CallFi
    private final TelecomSystem.SyncRoot mTelecomLock;
    private final TelecomServiceImpl.SettingsSecureAdapter mSettingsSecureAdapter;
    private final CallerInfoLookupHelper mCallerInfoLookupHelper;
    private final AppLabelProxy mAppLabelProxy;
    private final CallScreeningServiceHelper.AppLabelProxy mAppLabelProxy;

    private final int CARRIER_CALL_FILTERING_TIMED_OUT = 2000; // 2 seconds
    private final int CALL_FILTERING_TIMED_OUT = 4500; // 4.5 seconds
@@ -96,7 +86,7 @@ public class CallScreeningServiceController implements IncomingCallFilter.CallFi
            TelecomSystem.SyncRoot lock,
            TelecomServiceImpl.SettingsSecureAdapter settingsSecureAdapter,
            CallerInfoLookupHelper callerInfoLookupHelper,
            AppLabelProxy appLabelProxy) {
            CallScreeningServiceHelper.AppLabelProxy appLabelProxy) {
        mContext = context;
        mCallsManager = callsManager;
        mPhoneAccountRegistrar = phoneAccountRegistrar;
@@ -144,6 +134,8 @@ public class CallScreeningServiceController implements IncomingCallFilter.CallFi
                }
            } else if (!TextUtils.isEmpty(packageName) &&
                    packageName.equals(getDefaultDialerPackageName())) {
                // Default dialer defined CallScreeningService cannot skip the call log.
                mResult.shouldAddToCallLog = true;
                mIsDefaultDialerFinished = true;
                if (result.mCallBlockReason == CallLog.Calls.BLOCK_REASON_CALL_SCREENING_SERVICE ||
                        mIsUserChosenFinished) {
@@ -151,6 +143,8 @@ public class CallScreeningServiceController implements IncomingCallFilter.CallFi
                }
            } else if (!TextUtils.isEmpty(packageName) &&
                    packageName.equals(getUserChosenPackageName())) {
                // User defined CallScreeningService cannot skip the call log.
                mResult.shouldAddToCallLog = true;
                mIsUserChosenFinished = true;
                if (mIsDefaultDialerFinished) {
                    finishCallScreening();
Loading