I'm developing a simple call blocker Android app and was using CallScreeningService quite successfully. I do some database calls to detect numbers to be blocked and start an async task to send some information about the blocked calls to an external server. The implementation is working fine most of the time. But sometimes this fails miserably making the phone ring even after the call is said to be blocked. Following is my code.
@RequiresApi(api = Build.VERSION_CODES.N)
public class CallCaptureService extends CallScreeningService {
@Override
public void onScreenCall(Call.Details details) {
boolean blockCall = false;
String handle = details.getHandle().toString();
handle = handle.replace("tel:", "");
Log.d(SettingsConstants.CALL_LOG_TITLE, String.format("Call event received. Handle='%s'", handle));
long startTime = System.currentTimeMillis();
if (handle != null && !handle.isEmpty()) {
try {
String callingNumber = URLDecoder.decode(handle, "UTF-8");
if (AppUtil.isBlockedNumber(callingNumber)) { //This is a sqlite database call
blockCall = true;
} else if (AppUtil.isKnownPhoneNumber(this, callingNumber)) { //This is again a database call
//Do not block call
}
Log.i(SettingsConstants.CALL_LOG_TITLE, String.format("Sending call information to Call handler"));
Intent callCapturedIntent = new Intent();
callCapturedIntent.putExtra(ExtraKeys.CALLING_NUMBER_EXTRA_KEY, callingNumber);
callCapturedIntent.putExtra(ExtraKeys.CALL_STATE_EXTRA_KEY, CallState.RINGING.getValue());
callCapturedIntent.putExtra(ExtraKeys.CALL_SCREENING_USED_EXTRA_KEY, true);
callCapturedIntent.putExtra(ExtraKeys.ALREADY_KNOWN_NUMBER_EXTRA_KEY, isKnownPhoneNumber);
handleCallForCallScreening task = new handleCallForCallScreening(callCapturedIntent, getApplicationContext());
task.execute();
CallResponse.Builder response = new CallResponse.Builder();
response.setRejectCall(blockCall);
response.setSkipCallLog(blockCall);
response.setSkipNotification(blockCall);
response.setDisallowCall(blockCall);
long endTime = System.currentTimeMillis();
long delay = (endTime - startTime);
Log.i(SettingsConstants.CALL_LOG_TITLE, String.format("(Waiting for call) time for processing %s = %d", handle, delay));
if (blockedCallHangupTime > 0) {
long remainingTime = blockedCallHangupTime - delay;
if (remainingTime > 0) {
try {
Log.i(SettingsConstants.CALL_LOG_TITLE, String.format("Sleeping for %d (ms)", remainingTime));
Thread.sleep(remainingTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
respondToCall(details, response.build());
if (blockCall) {
Log.i(SettingsConstants.CALL_LOG_TITLE, String.format("Call Blocked Successfully"));
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
}
Log.d(SettingsConstants.CALL_LOG_TITLE, String.format("Finished handling call event. Handle='%s'", handle));
}
class handleCallForCallScreening extends AsyncTask<Void, Void, Void> {
Context _ctx;
Intent _intent;
public handleCallForCallScreening(Intent intent, Context context) {
_ctx = context;
_intent = intent;
}
@Override
protected Void doInBackground(Void... voids) {
CallHandlerHelper.handleCall(_intent, _ctx); //This method has a network operation, which will call a rest API from a remote server
return null;
}
}
}
And this is the code extract of the method inside our async task. This contains a network operation.
public static void handleCall(@NonNull Intent intent, Context context) {
...
String callingNumber = intent.getStringExtra(ExtraKeys.CALLING_NUMBER_EXTRA_KEY);
boolean callScreeningUsed = intent.getBooleanExtra(ExtraKeys.CALL_SCREENING_USED_EXTRA_KEY, false);
boolean isKnownPhoneNumber = intent.getBooleanExtra(ExtraKeys.ALREADY_KNOWN_NUMBER_EXTRA_KEY, false);
CallState state = CallState.fromInteger(intent.getIntExtra(ExtraKeys.CALL_STATE_EXTRA_KEY, 3));
...
CallSenderResult result = sendCALL(context, callingNumber, requestId, null);
}
private static CallSenderResult sendCALL(Context ctx, String callingNumber, String requestId) {
String appInstanceUniqueKey = StorageHelper.getAppInstanceUniqueKey(ctx);
CallSenderResult result = new CallSenderResult();
try {
if (SettingsConstants.IGNORE_SSL_CERTIFICATE) {
CommunicationRest.disableSSLCertificateChecking();
}
String accessToken = StorageHelper.getAccessToken(ctx);
String phoneNumberKey = StorageHelper.getPhoneNumberKey(ctx);
String refreshToken = StorageHelper.getRefreshToken(ctx);
JSONObject jsonObject = new JSONObject();
jsonObject.put("callingNumber", callingNumber);
jsonObject.put("requestId", requestId);
jsonObject.put("phoneNumberKey", phoneNumberKey);
jsonObject.put("detectedCallingNumbers", detectedCallingNumbersString);
....
Log.i(SettingsConstants.CALL_LOG_TITLE, String.format("Sending CALL information. CallingNumber:'%s', RequestId:'%s', delectedCallingNumbers:'%s'", callingNumber, requestId, detectedCallingNumbers));
String jsonString = jsonObject.toString();
HttpsURLConnection conn = NetworkUtil.getHttpPostURLConnectionForJson(accessToken, jsonString, FirebaseConstants.CALL_RESPONSE_URL);
int responseCode = conn.getResponseCode();
switch (responseCode) {
case 200:
Log.i(SettingsConstants.CALL_LOG_TITLE, String.format("CALL response sent successfully. ReqId:'%s', CallingNumber:'%s', delectedCallingNumbers:'%s'", requestId, callingNumber, detectedCallingNumbers));
result.setState(CallSenderState.SUCCESS);
break;
...
}
} catch (Exception e) {
...
}
return result;
}
public static final int CONNECTION_TIMEOUT_IN_MS = 30 *1000;
public static final int READ_TIMEOUT_IN_MS = 30 *1000;
public static HttpsURLConnection getHttpPostURLConnectionForJson(String accessToken, String json, String urlString) throws IOException {
URL url = new URL(urlString);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("authorization", "Bearer " + accessToken);
conn.setDoOutput(true);
conn.setConnectTimeout(SettingsConstants.CONNECTION_TIMEOUT_IN_MS); //
conn.setReadTimeout(SettingsConstants.READ_TIMEOUT_IN_MS);
OutputStream os = conn.getOutputStream();
os.write(json.getBytes("UTF-8"));
os.close();
return conn;
}
This code works fine most of the time and blocks calls if the number is found in our blocked number database. But sometimes this fails even the blocked number is found in our database and the log is there saying that the number is blocked successfully.
This is a log extract from our device when the number is intended to be blocked but actually rang.
2020-03-06 13:15:32.079 DEBUG CALL: Call event received. Handle='%2B85590914400'
2020-03-06 13:15:32.091 INFO CALL: Sending call information to Call handler
2020-03-06 13:15:32.099 INFO CALL: (Waiting for call) time for processing %2B85590914400 = 14
2020-03-06 13:15:32.116 INFO CALL: Sending CALL information. CallingNumber:'+85590914400' // This is printed from async task
2020-03-06 13:15:37.137 INFO CALL: Call Blocked Successfully
2020-03-06 13:15:37.162 DEBUG CALL: Finished handling call event. Handle='%2B85590914400'
2020-03-06 13:15:37.240 INFO CALL: Phone State change detected.(RINGING) //This is where the ringing occurs.
2020-03-06 13:15:37.890 INFO CALL: CLI response sent successfully. ReqId:'1583480726222', CallingNumber:'+85590914400', delectedCallingNumbers:'null' // This is printed from async task
2020-03-06 13:15:38.941 INFO CALL: Phone State change detected.(IDLE)
One thing I noticed is that that there is a 5s delay between "(Waiting for call) time for processing ..." message and "Call Blocked Successfully message". For the situations where this code works perfectly the timegap between these two messages are just few milliseconds. So I doubt whether there is some kind of a hang occurring inside respondToCall message.
Also this issue mostly occurs when I'm on a weak data connection. But the network operation is written inside an async task which may not have any effect on the CallScreeningService behaviour. But not sure about whats happening here.