blob: 064527b4e219af01e1f316c7c470b621fba9b4c8 [file] [log] [blame]
/*
* 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;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.CallScreeningService;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import java.util.Arrays;
/**
* Responsible for handling reports via
* {@link android.telecom.TelecomManager#reportNuisanceCallStatus(Uri, boolean)} as to whether the
* user has indicated a call is a nuisance call.
*
* Since nuisance reports can be initiated from the call log, potentially long after a call has
* completed, calls are identified by the {@link Call#getHandle()}. A nuisance report for a call is
* only accepted if:
* <ul>
* <li>A missed, incoming, or rejected call to that number exists in the call log. We want to
* avoid a scenario where a user reports a single outgoing call as a nuisance call.</li>
* <li>The call occurred via a sim-based phone account; we do not want to report nuisance calls
* which only ever took place via a self-managed ConnectionService. It is, however, valid for
* a number to be contacted both via a sim-based phone account and a self-managed one.</li>
* <li>The {@link CallScreeningService} has provided call identification for calls in the past.
* This provides an incentive for {@link CallScreeningService} implementations to use the caller
* ID APIs appropriately if they are going to benefit from use reports of nuisance and
* non-nuisance calls.</li>
* </ul>
*/
public class NuisanceCallReporter {
/**
* Columns we want to retrieve from the call log.
*/
private static final String[] CALL_LOG_PROJECTION = new String[] {
CallLog.Calls._ID,
CallLog.Calls.DURATION,
CallLog.Calls.TYPE,
CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
CallLog.Calls.PHONE_ACCOUNT_ID
};
public static final int CALL_LOG_COLUMN_ID =
Arrays.asList(CALL_LOG_PROJECTION).indexOf(CallLog.Calls._ID);
public static final int CALL_LOG_COLUMN_DURATION =
Arrays.asList(CALL_LOG_PROJECTION).indexOf(CallLog.Calls.DURATION);
public static final int CALL_LOG_COLUMN_TYPE =
Arrays.asList(CALL_LOG_PROJECTION).indexOf(CallLog.Calls.TYPE);
public static final int CALL_LOG_COLUMN_PHONE_ACCOUNT_COMPONENT_NAME =
Arrays.asList(CALL_LOG_PROJECTION).indexOf(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME);
public static final int CALL_LOG_COLUMN_PHONE_ACCOUNT_ID =
Arrays.asList(CALL_LOG_PROJECTION).indexOf(CallLog.Calls.PHONE_ACCOUNT_ID);
/**
* Represents information about a nuisance report via
* {@link android.telecom.TelecomManager#reportNuisanceCallStatus(Uri, boolean)}.
*/
private static class NuisanceReport {
public String callScreeningPackageName;
public Uri handle;
public boolean isNuisance;
public NuisanceReport(String packageName, Uri handle, boolean isNuisance) {
this.callScreeningPackageName = packageName;
this.handle = handle;
this.isNuisance = isNuisance;
}
}
/**
* Proxy interface to abstract calls to
* {@link android.telephony.PhoneNumberUtils#formatNumberToE164(String, String)}.
* Facilitates testing.
*/
public interface PhoneNumberUtilsProxy {
String formatNumberToE164(String number);
}
/**
* Proxy interface to abstract queries to the package manager to determine if a
* {@link PhoneAccountHandle} is for a self-managed connection service.
*/
public interface PhoneAccountRegistrarProxy {
boolean isSelfManagedConnectionService(PhoneAccountHandle handle);
}
/**
* Restrict to call log entries for the specified number where its an incoming, missed, blocked
* or rejected call.
*/
private static final String NUMBER_WHERE_CLAUSE =
CallLog.Calls.CACHED_NORMALIZED_NUMBER + " = ? AND " + CallLog.Calls.TYPE
+ " IN (" + CallLog.Calls.INCOMING_TYPE + "," + CallLog.Calls.MISSED_TYPE + ","
+ CallLog.Calls.BLOCKED_TYPE + "," + CallLog.Calls.REJECTED_TYPE + ")";
/**
* Call log where clause to find entries with call identification reported by a specified call
* screening service.
*/
private static final String CALL_ID_PACKAGE_WHERE_CLAUSE =
CallLog.Calls.CALL_ID_PACKAGE_NAME + " = ? ";
private final Context mContext;
private final PhoneNumberUtilsProxy mPhoneNumberUtilsProxy;
private final PhoneAccountRegistrarProxy mPhoneAccountRegistrarProxy;
private UserHandle mCurrentUserHandle;
public NuisanceCallReporter(Context context, PhoneNumberUtilsProxy phoneNumberUtilsProxy,
PhoneAccountRegistrarProxy phoneAccountRegistrarProxy) {
mContext = context;
mPhoneNumberUtilsProxy = phoneNumberUtilsProxy;
mPhoneAccountRegistrarProxy = phoneAccountRegistrarProxy;
}
public void setCurrentUserHandle(UserHandle userHandle) {
if (userHandle == null) {
Log.d(this, "setCurrentUserHandle, userHandle = null");
userHandle = Process.myUserHandle();
}
Log.d(this, "setCurrentUserHandle, %s", userHandle);
mCurrentUserHandle = userHandle;
}
/**
* Given a call handle reported by the default dialer, inform the
* {@link android.telecom.CallScreeningService} of whether the user has indicated a call is
* or is not a nuisance call.
*
* @param callScreeningPackageName the package name of the call screening service.
* @param handle the handle of the call to report nuisance status on.
* @param isNuisance {@code true} if the call is a nuisance call, {@code false} otherwise.
*/
public void reportNuisanceCallStatus(@NonNull String callScreeningPackageName,
@NonNull Uri handle, boolean isNuisance) {
// Don't report the nuisance status to a call screening app if it has not provided any
// caller id info in the past.
if (!hasCallScreeningServiceProvidedCallId(callScreeningPackageName)) {
Log.i(this, "reportNuisanceCallStatus: app %s has not provided caller ID; skipping.",
callScreeningPackageName);
return;
}
maybeSendNuisanceReport(new NuisanceReport(callScreeningPackageName, handle, isNuisance));
}
private void maybeSendNuisanceReport(@NonNull NuisanceReport nuisanceReport) {
Uri callsUri = CallLog.Calls.CONTENT_URI;
if (mCurrentUserHandle == null) {
return;
}
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI,
mCurrentUserHandle.getIdentifier());
String normalizedNumber = mPhoneNumberUtilsProxy.formatNumberToE164(
nuisanceReport.handle.getSchemeSpecificPart());
// Query the call log for the most recent information about this call.
Cursor cursor = mContext.getContentResolver().query(callsUri, CALL_LOG_PROJECTION,
NUMBER_WHERE_CLAUSE, new String[] { normalizedNumber },
CallLog.Calls.DEFAULT_SORT_ORDER);
Log.d(this, "maybeSendNuisanceReport: number=%s, isNuisance=%b",
Log.piiHandle(normalizedNumber), nuisanceReport.isNuisance);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
final long duration = cursor.getLong(CALL_LOG_COLUMN_DURATION);
final int callType = cursor.getInt(CALL_LOG_COLUMN_TYPE);
final String phoneAccountComponentName = cursor.getString(
CALL_LOG_COLUMN_PHONE_ACCOUNT_COMPONENT_NAME);
final String phoneAccountId = cursor.getString(
CALL_LOG_COLUMN_PHONE_ACCOUNT_ID);
PhoneAccountHandle handle = new PhoneAccountHandle(
ComponentName.unflattenFromString(phoneAccountComponentName),
phoneAccountId);
if (mPhoneAccountRegistrarProxy.isSelfManagedConnectionService(handle)) {
// Skip this call log entry; it was made via a self-managed CS.
Log.d(this, "maybeSendNuisanceReport: skip self-mgd CS %s",
phoneAccountComponentName);
continue;
}
sendNuisanceReportIntent(nuisanceReport, duration, callType);
// Stop when we send a nuisance report.
break;
}
} finally {
cursor.close();
}
}
}
/**
* Determines if a {@link CallScreeningService} has provided
* {@link android.telecom.CallIdentification} for calls in the past.
* @param packageName The package name of the {@link CallScreeningService}.
* @return {@code true} if the app has provided call identification, {@code false} otherwise.
*/
private boolean hasCallScreeningServiceProvidedCallId(@NonNull String packageName) {
// Query the call log for any entries which have call identification provided by the call
// screening package.
Cursor cursor = mContext.getContentResolver().query(CallLog.Calls.CONTENT_URI,
CALL_LOG_PROJECTION, CALL_ID_PACKAGE_WHERE_CLAUSE, new String[] { packageName },
CallLog.Calls.DEFAULT_SORT_ORDER + " LIMIT 1");
return cursor.getCount() > 0;
}
private void sendNuisanceReportIntent(@NonNull NuisanceReport nuisanceReport, long duration,
int callType) {
Log.i(this, "handleCallLogResult: number=%s, duration=%d, type=%d",
Log.piiHandle(nuisanceReport.handle), duration, callType);
Intent intent = new Intent(CallScreeningService.ACTION_NUISANCE_CALL_STATUS_CHANGED);
intent.setPackage(nuisanceReport.callScreeningPackageName);
intent.putExtra(CallScreeningService.EXTRA_CALL_HANDLE, nuisanceReport.handle);
intent.putExtra(CallScreeningService.EXTRA_IS_NUISANCE, nuisanceReport.isNuisance);
intent.putExtra(CallScreeningService.EXTRA_CALL_TYPE, callType);
intent.putExtra(CallScreeningService.EXTRA_CALL_DURATION, getCallDurationBucket(duration));
mContext.sendBroadcastAsUser(intent, mCurrentUserHandle);
}
/**
* Maps a call duration in milliseconds to a call duration bucket.
* @param callDuration Call duration, in milliseconds.
* @return The call duration bucket
*/
public @CallScreeningService.CallDuration int getCallDurationBucket(long callDuration) {
if (callDuration < 3000L) {
return CallScreeningService.CALL_DURATION_VERY_SHORT;
} else if (callDuration >= 3000L && callDuration < 60000L) {
return CallScreeningService.CALL_DURATION_SHORT;
} else if (callDuration >= 6000L && callDuration < 120000L) {
return CallScreeningService.CALL_DURATION_MEDIUM;
} else {
return CallScreeningService.CALL_DURATION_LONG;
}
}
}