Add "advanced call blocking mode"

CallScreeningService-based blocking available on Android 7+
This commit is contained in:
xynngh 2020-06-21 18:40:58 +04:00
parent 55b8c57264
commit 1dd983ac27
13 changed files with 397 additions and 22 deletions

View File

@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "dummydomain.yetanothercallblocker"
minSdkVersion 14

View File

@ -59,6 +59,27 @@
android:noHistory="true"
android:theme="@style/DialogBackgroundTheme" />
<activity
android:name=".DummyDialerActivity"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/DialogBackgroundTheme">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tel" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<receiver
android:name=".CallReceiver"
android:enabled="true"
@ -69,6 +90,14 @@
</receiver>
<service android:name=".work.TaskService" />
<service
android:name=".CallScreeningServiceImpl"
android:permission="android.permission.BIND_SCREENING_SERVICE">
<intent-filter>
<action android:name="android.telecom.CallScreeningService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -24,6 +24,7 @@ import dummydomain.yetanothercallblocker.event.CallOngoingEvent;
import dummydomain.yetanothercallblocker.event.CallStartedEvent;
import static dummydomain.yetanothercallblocker.EventUtils.postEvent;
import static java.util.Objects.requireNonNull;
public class CallReceiver extends BroadcastReceiver {
@ -81,9 +82,11 @@ public class CallReceiver extends BroadcastReceiver {
@SuppressLint("MissingPermission")
private boolean rejectCall(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
TelecomManager telecomManager = (TelecomManager)
context.getSystemService(Context.TELECOM_SERVICE);
try {
TelecomManager telecomManager = requireNonNull(
(TelecomManager) context.getSystemService(Context.TELECOM_SERVICE));
//noinspection deprecation
telecomManager.endCall();
LOG.info("Rejected call using TelecomManager");
@ -93,12 +96,14 @@ public class CallReceiver extends BroadcastReceiver {
}
}
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
try {
TelephonyManager tm = requireNonNull(
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
@SuppressLint("DiscouragedPrivateApi") // no choice
Method m = tm.getClass().getDeclaredMethod("getITelephony");
m.setAccessible(true);
ITelephony telephony = (ITelephony)m.invoke(tm);
ITelephony telephony = requireNonNull((ITelephony) m.invoke(tm));
telephony.endCall();
LOG.info("Rejected call using ITelephony");

View File

@ -0,0 +1,89 @@
package dummydomain.yetanothercallblocker;
import android.net.Uri;
import android.os.Build;
import android.telecom.Call;
import android.telecom.CallScreeningService;
import android.telecom.PhoneAccount;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dummydomain.yetanothercallblocker.data.DatabaseSingleton;
import dummydomain.yetanothercallblocker.data.NumberInfo;
import dummydomain.yetanothercallblocker.event.CallEndedEvent;
import static dummydomain.yetanothercallblocker.EventUtils.postEvent;
@RequiresApi(Build.VERSION_CODES.N)
public class CallScreeningServiceImpl extends CallScreeningService {
private static final Logger LOG = LoggerFactory.getLogger(CallScreeningServiceImpl.class);
@Override
public void onScreenCall(@NonNull Call.Details callDetails) {
LOG.info("onScreenCall({})", callDetails);
boolean shouldBlock = false;
NumberInfo numberInfo = null;
try {
boolean ignore = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (callDetails.getCallDirection() != Call.Details.DIRECTION_INCOMING) {
ignore = true;
}
}
if (!ignore && !App.getSettings().getBlockCalls()) {
ignore = true;
}
if (!ignore) {
Uri handle = callDetails.getHandle();
if (PhoneAccount.SCHEME_TEL.equals(handle.getScheme())) {
String number = handle.getSchemeSpecificPart();
LOG.debug("onScreenCall() number={}", number);
numberInfo = DatabaseSingleton.getNumberInfo(number);
if (numberInfo.rating == NumberInfo.Rating.NEGATIVE
&& numberInfo.contactItem == null) {
shouldBlock = true;
}
}
}
} finally {
LOG.debug("onScreenCall() blocking call: {}", shouldBlock);
CallScreeningService.CallResponse.Builder responseBuilder = new CallResponse.Builder();
if (shouldBlock) {
responseBuilder
.setDisallowCall(true)
.setRejectCall(true)
.setSkipNotification(true);
}
boolean blocked = shouldBlock;
try {
respondToCall(callDetails, responseBuilder.build());
} catch (Exception e) {
LOG.error("onScreenCall() error invoking respondToCall()", e);
blocked = false;
}
if (blocked) {
LOG.info("onScreenCall() blocked call");
NotificationHelper.showBlockedCallNotification(this, numberInfo);
postEvent(new CallEndedEvent());
}
}
}
}

View File

@ -0,0 +1,57 @@
package dummydomain.yetanothercallblocker;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DummyDialerActivity extends AppCompatActivity {
private static final Logger LOG = LoggerFactory.getLogger(DummyDialerActivity.class);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
LOG.info("onCreate() intent: {}", intent);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return; // not applicable to earlier versions
}
intent.setComponent(null);
ActivityInfo found = null;
for (ResolveInfo info : getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_ALL)) {
ActivityInfo activityInfo = info.activityInfo;
if (activityInfo != null && activityInfo.applicationInfo.enabled
&& !activityInfo.packageName.equals(BuildConfig.APPLICATION_ID)) {
LOG.debug("onCreate() found match: {}", activityInfo);
found = activityInfo;
break; // should explicitly prefer default dialer (by packageName)?
}
}
if (found != null) {
intent.setComponent(new ComponentName(found.applicationInfo.packageName, found.name));
startActivity(intent);
} else {
LOG.error("onCreate() didn't find any dialer to launch");
}
finish();
}
}

View File

@ -82,7 +82,7 @@ public class MainActivity extends AppCompatActivity {
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionHelper.onRequestPermissionsResult(this, requestCode, permissions, grantResults,
PermissionHelper.handlePermissionsResult(this, requestCode, permissions, grantResults,
settings.getIncomingCallNotifications(), settings.getBlockCalls(),
settings.getUseContacts());

View File

@ -1,9 +1,12 @@
package dummydomain.yetanothercallblocker;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.widget.Toast;
@ -20,9 +23,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static java.util.Objects.requireNonNull;
public class PermissionHelper {
private static final int PERMISSION_REQUEST_CODE = 1;
private static final int REQUEST_CODE_PERMISSIONS = 128;
private static final int REQUEST_CODE_DEFAULT_DIALER = 129;
private static final Logger LOG = LoggerFactory.getLogger(PermissionHelper.class);
@ -65,16 +71,16 @@ public class PermissionHelper {
if (!missingPermissions.isEmpty()) {
ActivityCompat.requestPermissions(activity,
missingPermissions.toArray(new String[0]), PERMISSION_REQUEST_CODE);
missingPermissions.toArray(new String[0]), REQUEST_CODE_PERMISSIONS);
}
}
public static void onRequestPermissionsResult(@NonNull Context context, int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults,
boolean infoExpected, boolean blockingExpected,
boolean contactsExpected) {
if (requestCode != PERMISSION_REQUEST_CODE) return;
public static void handlePermissionsResult(@NonNull Context context, int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults,
boolean infoExpected, boolean blockingExpected,
boolean contactsExpected) {
if (requestCode != REQUEST_CODE_PERMISSIONS) return;
boolean infoDenied = false;
boolean blockingDenied = false;
@ -139,4 +145,63 @@ public class PermissionHelper {
== PackageManager.PERMISSION_GRANTED;
}
public static void requestCallScreening(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
RoleManagerHelper.requestCallScreeningRole(activity);
} else {
setAsDefaultDialer(activity);
}
}
public static boolean isCallScreeningHeld(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (RoleManagerHelper.hasCallScreeningRole(context)) return true;
}
return isDefaultDialer(context);
}
public static boolean handleCallScreeningResult(Context context,
int requestCode, int resultCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (RoleManagerHelper.handleCallScreeningResult(context, requestCode, resultCode)) {
return true;
}
}
return handleDefaultDialerResult(context, requestCode, resultCode);
}
public static boolean isDefaultDialer(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false;
TelecomManager telecomManager = requireNonNull(
(TelecomManager) context.getSystemService(Context.TELECOM_SERVICE));
return BuildConfig.APPLICATION_ID.equals(telecomManager.getDefaultDialerPackage());
}
public static void setAsDefaultDialer(Activity activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
if (isDefaultDialer(activity)) return;
Intent intent = new Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER);
intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
BuildConfig.APPLICATION_ID);
activity.startActivityForResult(intent, REQUEST_CODE_DEFAULT_DIALER);
}
public static boolean handleDefaultDialerResult(Context context,
int requestCode, int resultCode) {
if (requestCode != REQUEST_CODE_DEFAULT_DIALER) return false;
if (resultCode != Activity.RESULT_OK) {
Toast.makeText(context, R.string.denied_default_dialer_message, Toast.LENGTH_LONG)
.show();
}
return true;
}
}

View File

@ -0,0 +1,52 @@
package dummydomain.yetanothercallblocker;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import static android.content.Context.ROLE_SERVICE;
import static java.util.Objects.requireNonNull;
@RequiresApi(Build.VERSION_CODES.Q)
public class RoleManagerHelper {
private static final int REQUEST_CODE_CALL_SCREENING = 130;
public static boolean hasCallScreeningRole(Context context) {
return getRoleManager(context).isRoleHeld(RoleManager.ROLE_CALL_SCREENING);
}
public static void requestCallScreeningRole(Activity activity) {
if (hasCallScreeningRole(activity)) return;
Intent intent = getRoleManager(activity)
.createRequestRoleIntent(RoleManager.ROLE_CALL_SCREENING);
activity.startActivityForResult(intent, REQUEST_CODE_CALL_SCREENING);
}
public static boolean handleCallScreeningResult(Context context,
int requestCode, int resultCode) {
if (requestCode != REQUEST_CODE_CALL_SCREENING) return false;
if (resultCode != Activity.RESULT_OK) {
Toast.makeText(context, R.string.denied_call_screening_role_message, Toast.LENGTH_LONG)
.show();
}
return true;
}
private static RoleManager getRoleManager(Context context) {
@SuppressLint("WrongConstant")
RoleManager roleManager = (RoleManager) context.getSystemService(ROLE_SERVICE);
return requireNonNull(roleManager);
}
}

View File

@ -1,10 +1,15 @@
package dummydomain.yetanothercallblocker;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroup;
@ -33,8 +38,34 @@ public class SettingsActivity extends AppCompatActivity {
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Settings settings = App.getSettings();
PermissionHelper.handlePermissionsResult(this, requestCode, permissions, grantResults,
settings.getIncomingCallNotifications(), settings.getBlockCalls(),
settings.getUseContacts());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (PermissionHelper.handleCallScreeningResult(this, requestCode, resultCode)) {
for (Fragment fragment : getSupportFragmentManager().getFragments()) {
if (fragment instanceof SettingsFragment) {
((SettingsFragment) fragment).updateCallScreeningPreference();
}
}
}
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private static final String PREF_USE_CALL_SCREENING_SERVICE = "useCallScreeningService";
private static final String PREF_AUTO_UPDATE_ENABLED = "autoUpdateEnabled";
private static final String PREF_CATEGORY_NOTIFICATIONS = "categoryNotifications";
@ -62,6 +93,24 @@ public class SettingsActivity extends AppCompatActivity {
return true;
});
SwitchPreferenceCompat callScreeningPref =
requireNonNull(findPreference(PREF_USE_CALL_SCREENING_SERVICE));
callScreeningPref.setChecked(PermissionHelper.isCallScreeningHeld(getActivity()));
callScreeningPref.setOnPreferenceChangeListener((preference, newValue) -> {
if (Boolean.TRUE.equals(newValue)) {
PermissionHelper.requestCallScreening(getActivity());
} else {
Toast.makeText(getActivity(),
R.string.use_call_screening_service_disable_message,
Toast.LENGTH_LONG).show();
return false;
}
return true;
});
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
callScreeningPref.setVisible(false);
}
SwitchPreferenceCompat nonPersistentAutoUpdatePref =
requireNonNull(findPreference(PREF_AUTO_UPDATE_ENABLED));
nonPersistentAutoUpdatePref.setChecked(updateScheduler.isAutoUpdateScheduled());
@ -103,5 +152,11 @@ public class SettingsActivity extends AppCompatActivity {
}
}
}
public void updateCallScreeningPreference() {
SwitchPreferenceCompat callScreeningPref =
requireNonNull(findPreference(PREF_USE_CALL_SCREENING_SERVICE));
callScreeningPref.setChecked(PermissionHelper.isCallScreeningHeld(getActivity()));
}
}
}

View File

@ -13,8 +13,12 @@ public class CallLogItem {
switch (type) {
case CallLog.Calls.INCOMING_TYPE: return INCOMING;
case CallLog.Calls.OUTGOING_TYPE: return OUTGOING;
case CallLog.Calls.MISSED_TYPE: return MISSED;
case CallLog.Calls.REJECTED_TYPE: return REJECTED;
case CallLog.Calls.MISSED_TYPE:
case CallLog.Calls.VOICEMAIL_TYPE:
return MISSED;
case CallLog.Calls.REJECTED_TYPE:
case CallLog.Calls.BLOCKED_TYPE:
return REJECTED;
default: return OTHER;
}
}

View File

@ -22,6 +22,9 @@
<string name="sia_category_nonprofit">Некоммерческая орг</string>
<string name="block_calls">Блокир-ть нежелат. вызовы</string>
<string name="block_calls_summary">Автоматически сбрасывать звонки c номеров с отрицательным рейтингом</string>
<string name="use_call_screening_service">Продвинутый режим блокирования вызовов</string>
<string name="use_call_screening_service_summary">Позволяет блокировать вызовы до того, как телефон зазвонит. Это требует назначить приложение \"приложением для звонков\" (на Android 79) или \"приложением для АОН и защиты от спама\" (на Android 10+)</string>
<string name="use_call_screening_service_disable_message">Выберите другое приложение для звонков или защиты от спама в меню Настройки - Приложения - Приложения по умолчанию</string>
<string name="auto_updates">Авто-обновл. базы номеров</string>
<string name="auto_updates_summary">Получать ежедневные обновления базы (загружаются только изменения, поэтому расход трафика небольшой)</string>
<string name="open_debug_activity">Работа с базой номеров</string>
@ -53,6 +56,8 @@
<string name="feature_info">уведомления</string>
<string name="feature_call_blocking">блокир-е вызовов</string>
<string name="feature_contacts">имена контактов</string>
<string name="denied_default_dialer_message">Продвинутый режим блокирования не сможет работать, т.к. приложение не назначено \"приложением для звонков\"</string>
<string name="denied_call_screening_role_message">Продвинутый режим блокирования не сможет работать, т.к. приложение не назначено \"приложением для АОН и защиты от спама\"</string>
<string name="no_main_db_title">Загрузить базу номеров</string>
<string name="no_main_db_text">База номеров отсутствует. Для полноценной работы приложения требуется загрузка базы номеров (около 25 МБ трафика).</string>
<string name="download_main_db">Загрузить базу номеров</string>
@ -73,6 +78,7 @@
<string name="open_settings_activity">Настройки</string>
<string name="title_settings_activity">Настройки</string>
<string name="settings_category_main">Основные</string>
<string name="settings_category_call_blocking">Блокирование вызовов</string>
<string name="settings_category_notifications_incoming_calls">Уведомления на входящие</string>
<string name="show_notifications_for_known_callers">Уведомления для известных звонящих</string>
<string name="show_notifications_for_known_callers_summary">Показывать уведомления для известных звонящих (номеров из телефонной книги)</string>

View File

@ -56,6 +56,8 @@
<string name="feature_info">notifications</string>
<string name="feature_call_blocking">call blocking</string>
<string name="feature_contacts">using contacts</string>
<string name="denied_default_dialer_message">Advanced call blocking won\'t work due to the app not being set as the \"Phone app\"</string>
<string name="denied_call_screening_role_message">Advanced call blocking won\'t work due to the app not being set as the \"Caller ID app\"</string>
<string name="call_log_permission_message">Grant \"Phone\" permission to see recent calls</string>
@ -77,6 +79,7 @@
<string name="open_settings_activity">Settings</string>
<string name="title_settings_activity">Settings</string>
<string name="settings_category_main">Main</string>
<string name="settings_category_call_blocking">Call blocking</string>
<string name="settings_category_notifications_incoming_calls">Incoming calls notifications</string>
<string name="show_notifications_for_known_callers">Notifications for known callers</string>
<string name="show_notifications_for_known_callers_summary">Display notifications for known callers (numbers in Contacts)</string>
@ -87,6 +90,9 @@
<string name="incoming_call_notifications_summary">Displays a notification with phone number summary (rating, reviews count, category) during incoming calls</string>
<string name="block_calls">Block unwanted calls</string>
<string name="block_calls_summary">Automatically blocks calls with negative rating</string>
<string name="use_call_screening_service">Advanced call blocking mode</string>
<string name="use_call_screening_service_summary">Allows to block calls before the phone starts to ring. Requires the app to be set as the \"Phone app\" (Android 79) or as the \"Caller ID app\" (Android 10+)</string>
<string name="use_call_screening_service_disable_message">Select different \"Phone app\" or \"Caller ID app\" in Settings - Apps - Default apps</string>
<string name="auto_updates">Auto-update database</string>
<string name="auto_updates_summary">Automatically receive daily DB updates (these are incremental/delta updates, so they consume very little traffic)</string>
<string name="use_contacts">Use contacts</string>

View File

@ -1,5 +1,4 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/settings_category_main">
<SwitchPreferenceCompat
@ -7,10 +6,6 @@
app:key="incomingCallNotifications"
app:summary="@string/incoming_call_notifications_summary"
app:title="@string/incoming_call_notifications" />
<SwitchPreferenceCompat
app:key="blockCalls"
app:summary="@string/block_calls_summary"
app:title="@string/block_calls" />
<SwitchPreferenceCompat
app:key="autoUpdateEnabled"
app:persistent="false"
@ -22,6 +17,18 @@
app:title="@string/use_contacts" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_category_call_blocking">
<SwitchPreferenceCompat
app:key="blockCalls"
app:summary="@string/block_calls_summary"
app:title="@string/block_calls" />
<SwitchPreferenceCompat
app:key="useCallScreeningService"
app:persistent="false"
app:summary="@string/use_call_screening_service_summary"
app:title="@string/use_call_screening_service" />
</PreferenceCategory>
<PreferenceCategory
app:key="categoryNotifications"
app:title="@string/settings_category_notifications_incoming_calls">