I am trying to develop an app that detects incoming call and announce the name of the caller. The broadcast receiver that I have used works fine when the app is in foreground. But when I close the app, it doesn't detect the phone state although I have declared it in my manifest.
Manifest.xml:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-feature android:name="android.hardware.telephony"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="oppo.permission.OPPO_COMPONENT_SAFE"/>
<uses-permission android:name="com.huawei.permission.external_app_settings.USE_COMPONENT"/>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CallAnnouncer"
tools:targetApi="31">
<activity
android:name=".ExcludeActivity"
android:exported="false" />
<activity
android:name=".SMSActivity"
android:exported="false" />
<activity
android:name=".CallActivity"
android:exported="false" />
<activity
android:name=".PermissionActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".CallReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
<receiver
android:name=".StartMyServiceAtBootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".Speaker"
android:enabled="true"/>
<activity
android:name=".MainActivity"
android:exported="true" />
</application>
CallReceiver.java
Context context1;
String savedNumber, name, incomeNumber;
private static int lastState = TelephonyManager.CALL_STATE_IDLE;
static boolean isIncoming;
Boolean isAdded = false;
SharedPreferences prefs;
@SuppressLint("UnsafeProtectedBroadcastReceiver")
@Override
public void onReceive(Context context, Intent intent) {
context1 = context;
TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telephony.listen(new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
prefs = context.getSharedPreferences("MyPref", 0);
SharedPreferences.Editor editor = prefs.edit();
isAdded = prefs.getBoolean("added", false);
incomeNumber = prefs.getString("number", null);
if (isAdded) {
onStateChanged(context, state, incomeNumber, intent);
editor.putString("number", incomeNumber);
} else {
onStateChanged(context, state, incomingNumber, intent);
editor.putString("number", incomingNumber);
}
editor.putBoolean("added", !isAdded);
editor.apply();
} else
incomeNumber = incomingNumber;
onStateChanged(context, state, incomingNumber, intent);
}
}, PhoneStateListener.LISTEN_CALL_STATE);
}
public void onStateChanged(Context context, int state, String number, Intent intent) {
if (lastState == state) {
//No change, debounce extras
return;
}
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
isIncoming = true;
Date callStartTime = new Date();
savedNumber = number;
onIncomingCallStarted(context, savedNumber, callStartTime, intent);
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
//Transition of ringing->offhook are pickups of incoming calls. Nothing done on them
if (lastState != TelephonyManager.CALL_STATE_RINGING) {
isIncoming = false;
callStartTime = new Date();
onOutgoingCallStarted(context, savedNumber, callStartTime, intent);
}
break;
case TelephonyManager.CALL_STATE_IDLE:
//Went to idle- this is the end of a call. What type depends on previous state(s)
if (lastState == TelephonyManager.CALL_STATE_RINGING) {
//Ring but no pickup- a miss
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
onMissedCall(context, incomeNumber, new Date(), intent);
} else
onMissedCall(context, savedNumber, new Date(), intent);
} else if (isIncoming) {
onIncomingCallEnded(context, savedNumber, new Date(), intent);
} else {
onOutgoingCallEnded(context, savedNumber, new Date(), intent);
}
break;
}
lastState = state;
}
protected void onIncomingCallStarted(Context ctx, String number, Date start, Intent intent) {
name = getContactName(number);
Intent ttsService = new Intent(ctx, Speaker.class);
ttsService.putExtra("text", "You have call from " + name);
ctx.startService(ttsService);
}
protected void onMissedCall(Context ctx, String number, Date start, Intent intent) {
Intent ttsService = new Intent(ctx, Speaker.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ttsService.putExtra("text", "You have a missed call");
} else {
name = getContactName(number);
ttsService.putExtra("text", "You have missed call from " + name);
}
ctx.startService(ttsService);
// saveData(ctx, number, intent, "Missed Call");
}
protected void onOutgoingCallStarted(Context ctx, String number, Date start, Intent intent) {
name = getContactName(number);
Log.d("TAG", "Outgoing Call to " + name);
}
protected void onIncomingCallEnded(Context ctx, String number, Date start, Intent intent) {
name = getContactName(number);
Log.d("TAG", "Incoming call ended of " + name);
}
protected void onOutgoingCallEnded(Context ctx, String number, Date start, Intent intent) {
name = getContactName(number);
Log.d("TAG", "Outgoing Call ended of " + name);
}
private String getContactName(String phone) {
ContentResolver cr = context1.getContentResolver();
Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phone));
Cursor cursor = cr.query(uri, new String[]{ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null);
if (cursor.moveToFirst()) {
name = cursor.getString(0);
} else {
name = "unknown number";
}
if (!cursor.isClosed()) {
cursor.close();
}
return name;
}
SpeakerService.java:
TextToSpeech tts = null;
String spokenText;
Boolean isInit = false;
HashMap<String, String> hash = new HashMap<>();
private static final String TAG = "TTS_Service";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getExtras() != null)
spokenText = intent.getStringExtra("text");
tts = new TextToSpeech(this, this);
return START_STICKY;
}
@Override
public void onInit(int i) {
if (i == TextToSpeech.SUCCESS) {
Log.d("TAG", "onInit: TextToSpeech Success");
int result = tts.setLanguage(Locale.US);
if (result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED) {
Log.d("TAG", "onInit: speaking........");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
hash.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
String.valueOf(AudioManager.STREAM_NOTIFICATION));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
tts.speak(spokenText, TextToSpeech.QUEUE_FLUSH, null, null);
} else {
tts.speak(spokenText, TextToSpeech.QUEUE_FLUSH, null);
}
}
isInit = true;
}
} else {
Log.d("TAG", "onInit: TTS initialization failed");
}
}
@Override
public void onDestroy() {
if (tts != null) {
tts.stop();
tts.shutdown();
}
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}