Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Changed

- Compatibility updates for Android 15 & 16

### Fixed
- Fixed incoming call screen hidden by lock screen ([#165])

## [1.7.3] - 2025-10-16
### Changed
- Updated translations
Expand Down Expand Up @@ -203,6 +205,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#139]: https://github.com/FossifyOrg/Phone/issues/139
[#146]: https://github.com/FossifyOrg/Phone/issues/146
[#147]: https://github.com/FossifyOrg/Phone/issues/147
[#165]: https://github.com/FossifyOrg/Phone/issues/165
[#181]: https://github.com/FossifyOrg/Phone/issues/181
[#183]: https://github.com/FossifyOrg/Phone/issues/183
[#186]: https://github.com/FossifyOrg/Phone/issues/186
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />

<uses-permission
android:name="android.permission.USE_FINGERPRINT"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.fossify.phone.dialogs.ChangeSortingDialog
import org.fossify.phone.dialogs.FilterContactSourcesDialog
import org.fossify.phone.extensions.clearMissedCalls
import org.fossify.phone.extensions.config
import org.fossify.phone.extensions.handleFullScreenNotificationsPermission
import org.fossify.phone.extensions.launchCreateNewContactIntent
import org.fossify.phone.fragments.ContactsFragment
import org.fossify.phone.fragments.FavoritesFragment
Expand Down Expand Up @@ -73,7 +74,11 @@ class MainActivity : SimpleActivity() {
checkContactPermissions()

if (!config.wasOverlaySnackbarConfirmed && !Settings.canDrawOverlays(this)) {
val snackbar = Snackbar.make(binding.mainHolder, R.string.allow_displaying_over_other_apps, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok) {
val snackbar = Snackbar.make(
binding.mainHolder,
R.string.allow_displaying_over_other_apps,
Snackbar.LENGTH_INDEFINITE
).setAction(R.string.ok) {
config.wasOverlaySnackbarConfirmed = true
startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
}
Expand All @@ -84,9 +89,9 @@ class MainActivity : SimpleActivity() {
snackbar.show()
}

handleNotificationPermission { granted ->
handleFullScreenNotificationsPermission { granted ->
if (!granted) {
PermissionRequiredDialog(this, R.string.allow_notifications_incoming_calls, { openNotificationSettings() })
toast(org.fossify.commons.R.string.notifications_disabled)
}
}
} else {
Expand Down
39 changes: 39 additions & 0 deletions app/src/main/kotlin/org/fossify/phone/extensions/Activity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@ import android.provider.ContactsContract
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import org.fossify.commons.R
import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.commons.dialogs.CallConfirmationDialog
import org.fossify.commons.dialogs.PermissionRequiredDialog
import org.fossify.commons.extensions.canUseFullScreenIntent
import org.fossify.commons.extensions.initiateCall
import org.fossify.commons.extensions.isDefaultDialer
import org.fossify.commons.extensions.isPackageInstalled
import org.fossify.commons.extensions.launchActivityIntent
import org.fossify.commons.extensions.launchCallIntent
import org.fossify.commons.extensions.launchViewContactIntent
import org.fossify.commons.extensions.openFullScreenIntentSettings
import org.fossify.commons.extensions.openNotificationSettings
import org.fossify.commons.extensions.telecomManager
import org.fossify.commons.helpers.CONTACT_ID
import org.fossify.commons.helpers.IS_PRIVATE
import org.fossify.commons.helpers.PERMISSION_READ_PHONE_STATE
import org.fossify.commons.helpers.SimpleContactsHelper
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.models.contacts.Contact
import org.fossify.phone.BuildConfig
import org.fossify.phone.activities.DialerActivity
import org.fossify.phone.activities.SimpleActivity
import org.fossify.phone.dialogs.SelectSIMDialog
Expand Down Expand Up @@ -181,3 +187,36 @@ fun SimpleActivity.showSelectSimDialog(
) { handle ->
callback(handle)
}

fun SimpleActivity.handleFullScreenNotificationsPermission(callback: (granted: Boolean) -> Unit) {
handleNotificationPermission { granted ->
if (granted) {
if (canUseFullScreenIntent()) {
callback(true)
} else {
PermissionRequiredDialog(
activity = this,
textId = R.string.allow_full_screen_notifications_incoming_calls,
positiveActionCallback = {
@SuppressLint("NewApi")
openFullScreenIntentSettings(BuildConfig.APPLICATION_ID)
},
negativeActionCallback = {
callback(false)
}
)
}
} else {
PermissionRequiredDialog(
activity = this,
textId = R.string.allow_notifications_incoming_calls,
positiveActionCallback = {
openNotificationSettings()
},
negativeActionCallback = {
callback(false)
}
)
}
}
}
11 changes: 9 additions & 2 deletions app/src/main/kotlin/org/fossify/phone/extensions/Context.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.fossify.phone.extensions

import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.content.Context
import android.content.Context.KEYGUARD_SERVICE
import android.content.Intent
import android.media.AudioManager
import android.net.Uri
Expand All @@ -14,9 +16,14 @@ import org.fossify.phone.models.SIMAccount

val Context.config: Config get() = Config.newInstance(applicationContext)

val Context.audioManager: AudioManager get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val Context.audioManager: AudioManager
get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager

val Context.powerManager: PowerManager get() = getSystemService(Context.POWER_SERVICE) as PowerManager
val Context.powerManager: PowerManager
get() = getSystemService(Context.POWER_SERVICE) as PowerManager

val Context.keyguardManager: KeyguardManager
get() = getSystemService(KEYGUARD_SERVICE) as KeyguardManager

@SuppressLint("MissingPermission")
fun Context.getAvailableSIMCardLabels(): List<SIMAccount> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,65 @@ package org.fossify.phone.helpers
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.telecom.Call
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.extensions.setText
import org.fossify.commons.extensions.setVisibleIf
import org.fossify.commons.helpers.isOreoPlus
import org.fossify.phone.R
import org.fossify.phone.activities.CallActivity
import org.fossify.phone.extensions.powerManager
import org.fossify.phone.receivers.CallActionReceiver

class CallNotificationManager(private val context: Context) {
private val CALL_NOTIFICATION_ID = 42
private val ACCEPT_CALL_CODE = 0
private val DECLINE_CALL_CODE = 1
companion object {
private const val CALL_NOTIFICATION_ID = 42
private const val ACCEPT_CALL_CODE = 0
private const val DECLINE_CALL_CODE = 1
}

private val notificationManager = context.notificationManager
private val callContactAvatarHelper = CallContactAvatarHelper(context)

@SuppressLint("NewApi")
fun setupNotification(forceLowPriority: Boolean = false) {
fun setupNotification(lowPriority: Boolean = false) {
getCallContact(context.applicationContext, CallManager.getPrimaryCall()) { callContact ->
val callContactAvatar = callContactAvatarHelper.getCallContactAvatar(callContact)
val callState = CallManager.getState()
val isHighPriority = context.powerManager.isInteractive && callState == Call.STATE_RINGING && !forceLowPriority
val channelId = if (isHighPriority) "simple_dialer_call_high_priority" else "simple_dialer_call"
if (isOreoPlus()) {
val importance = if (isHighPriority) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_DEFAULT
val name = if (isHighPriority) {
context.getString(R.string.call_notification_channel_high_priority)
} else {
context.getString(R.string.call_notification_channel)
}

NotificationChannel(channelId, name, importance).apply {
setSound(null, null)
notificationManager.createNotificationChannel(this)
}
}
val isHighPriority = callState == Call.STATE_RINGING && !lowPriority
val channelId =
if (isHighPriority) "simple_dialer_call_high_priority" else "simple_dialer_call"
createNotificationChannel(isHighPriority, channelId)

val openAppIntent = CallActivity.getStartIntent(context)
val openAppPendingIntent = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_MUTABLE)
val openAppPendingIntent =
PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_MUTABLE)

val acceptCallIntent = Intent(context, CallActionReceiver::class.java)
acceptCallIntent.action = ACCEPT_CALL
val acceptPendingIntent =
PendingIntent.getBroadcast(context, ACCEPT_CALL_CODE, acceptCallIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE)
PendingIntent.getBroadcast(
context,
ACCEPT_CALL_CODE,
acceptCallIntent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
)

val declineCallIntent = Intent(context, CallActionReceiver::class.java)
declineCallIntent.action = DECLINE_CALL
val declinePendingIntent =
PendingIntent.getBroadcast(context, DECLINE_CALL_CODE, declineCallIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE)

var callerName = if (callContact.name.isNotEmpty()) callContact.name else context.getString(R.string.unknown_caller)
PendingIntent.getBroadcast(
context,
DECLINE_CALL_CODE,
declineCallIntent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
)

var callerName = callContact.name.ifEmpty { context.getString(R.string.unknown_caller) }
if (callContact.numberLabel.isNotEmpty()) {
callerName += " - ${callContact.numberLabel}"
}
Expand All @@ -82,21 +83,22 @@ class CallNotificationManager(private val context: Context) {
setOnClickPendingIntent(R.id.notification_accept_call, acceptPendingIntent)

if (callContactAvatar != null) {
setImageViewBitmap(R.id.notification_thumbnail, callContactAvatarHelper.getCircularBitmap(callContactAvatar))
setImageViewBitmap(
R.id.notification_thumbnail,
callContactAvatarHelper.getCircularBitmap(callContactAvatar)
)
}
}

val builder = NotificationCompat.Builder(context, channelId)
val builder = Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_phone_vector)
.setContentIntent(openAppPendingIntent)
.setPriority(if (isHighPriority) NotificationManager.IMPORTANCE_HIGH else NotificationCompat.PRIORITY_DEFAULT)
.setCategory(Notification.CATEGORY_CALL)
.setCustomContentView(collapsedView)
.setOngoing(true)
.setSound(null)
.setUsesChronometer(callState == Call.STATE_ACTIVE)
.setChannelId(channelId)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setStyle(Notification.DecoratedCustomViewStyle())

if (isHighPriority) {
builder.setFullScreenIntent(openAppPendingIntent, true)
Expand All @@ -110,6 +112,20 @@ class CallNotificationManager(private val context: Context) {
}
}

fun createNotificationChannel(isHighPriority: Boolean, channelId: String) {
val name = if (isHighPriority) {
context.getString(R.string.call_notification_channel_high_priority)
} else {
context.getString(R.string.call_notification_channel)
}

val importance = if (isHighPriority) IMPORTANCE_HIGH else IMPORTANCE_DEFAULT
NotificationChannel(channelId, name, importance).apply {
setSound(null, null)
notificationManager.createNotificationChannel(this)
}
}

fun cancelNotification() {
notificationManager.cancel(CALL_NOTIFICATION_ID)
}
Expand Down
34 changes: 25 additions & 9 deletions app/src/main/kotlin/org/fossify/phone/services/CallService.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.fossify.phone.services

import android.app.KeyguardManager
import android.content.Context
import android.telecom.Call
import android.telecom.CallAudioState
import android.telecom.InCallService
import org.fossify.commons.extensions.canUseFullScreenIntent
import org.fossify.commons.extensions.hasPermission
import org.fossify.commons.helpers.PERMISSION_POST_NOTIFICATIONS
import org.fossify.phone.activities.CallActivity
import org.fossify.phone.extensions.config
import org.fossify.phone.extensions.isOutgoing
import org.fossify.phone.extensions.keyguardManager
import org.fossify.phone.extensions.powerManager
import org.fossify.phone.helpers.CallManager
import org.fossify.phone.helpers.CallNotificationManager
Expand Down Expand Up @@ -35,17 +37,31 @@ class CallService : InCallService() {
CallManager.inCallService = this
call.registerCallback(callListener)

val isScreenLocked = (getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager).isDeviceLocked
if (!powerManager.isInteractive || call.isOutgoing() || isScreenLocked || config.alwaysShowFullscreen) {
// Incoming/Outgoing (locked): high priority (FSI)
// Incoming (unlocked): if user opted in, low priority ➜ manual activity start, otherwise high priority (FSI)
// Outgoing (unlocked): low priority ➜ manual activity start
val isIncoming = !call.isOutgoing()
val isDeviceLocked = !powerManager.isInteractive || keyguardManager.isDeviceLocked
val lowPriority = when {
isIncoming && isDeviceLocked -> false
!isIncoming && isDeviceLocked -> false
isIncoming && !isDeviceLocked -> config.alwaysShowFullscreen
else -> true
}

callNotificationManager.setupNotification(lowPriority)
if (
lowPriority
|| !hasPermission(PERMISSION_POST_NOTIFICATIONS)
|| !canUseFullScreenIntent()
) {
try {
callNotificationManager.setupNotification(true)
startActivity(CallActivity.getStartIntent(this))
} catch (e: Exception) {
// seems like startActivity can throw AndroidRuntimeException and ActivityNotFoundException, not yet sure when and why, lets show a notification
} catch (_: Exception) {
// seems like startActivity can throw AndroidRuntimeException and
// ActivityNotFoundException, not yet sure when and why, lets show a notification
callNotificationManager.setupNotification()
}
} else {
callNotificationManager.setupNotification()
}
}

Expand Down
Loading