Skip to content

Commit

Permalink
Merge pull request #50 from infinum/fix/crash-notifications-not-showi…
Browse files Browse the repository at this point in the history
…ng-on-android-13

Fix: Add Android 13 notification permission support
  • Loading branch information
KCeh authored Mar 4, 2024
2 parents 0cba734 + 1094244 commit 6f20c95
Show file tree
Hide file tree
Showing 15 changed files with 130 additions and 13 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ _Plain_ formatter is selected by default, but selecting any other is persisted b
## Crash monitor
_Sentinel_ has a built in default uncaught exception handler and ANR observer. If switched on in
settings, it will notify both in a form of a notification.
settings, it will notify both in a form of a notification. Note that from Android 13 you need to give permission
to the app to show notifications.
Once tapped on this notification, a screen with details is shown. A complete list of crashes is
persisted between sessions and available on demand.
Methods to react on these crashes in a graceful way are provided in _Sentinel_.
Expand Down
2 changes: 2 additions & 0 deletions sentinel/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application>

<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ internal sealed class SettingsEvent {
data class CertificateMonitorChanged(
val value: CertificateMonitorEntity
) : SettingsEvent()

object PermissionsCheck : SettingsEvent()
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.infinum.sentinel.ui.settings

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RestrictTo
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.switchmaterial.SwitchMaterial
import com.infinum.sentinel.R
import com.infinum.sentinel.data.models.local.FormatEntity
Expand Down Expand Up @@ -33,6 +43,18 @@ internal class SettingsFragment : BaseChildFragment<Nothing, SettingsEvent>(R.la

override val viewModel: SettingsViewModel by viewModels()

private val permissionRequest = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isPermissionGranted: Boolean ->
if (!isPermissionGranted) {
Toast.makeText(
requireContext(),
getString(R.string.sentinel_notification_permission_denied),
Toast.LENGTH_LONG,
).show()
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand Down Expand Up @@ -107,10 +129,12 @@ internal class SettingsFragment : BaseChildFragment<Nothing, SettingsEvent>(R.la
binding.airplaneModeTriggerView,
trigger
)

else -> throw NotImplementedError()
}
}
}

is SettingsEvent.FormatChanged -> {
when (event.value.type) {
FormatType.PLAIN -> R.id.plainChip
Expand All @@ -123,6 +147,7 @@ internal class SettingsFragment : BaseChildFragment<Nothing, SettingsEvent>(R.la
binding.formatGroup.check(it)
}
}

is SettingsEvent.BundleMonitorChanged -> {
binding.bundleMonitorSwitch.setOnCheckedChangeListener(null)
binding.bundleMonitorSwitch.isChecked = event.value.notify
Expand Down Expand Up @@ -167,6 +192,7 @@ internal class SettingsFragment : BaseChildFragment<Nothing, SettingsEvent>(R.la
}
binding.limitValueView.text = String.format(FORMAT_BUNDLE_SIZE, event.value.limit)
}

is SettingsEvent.CrashMonitorChanged -> {
binding.uncaughtExceptionSwitch.setOnCheckedChangeListener(null)
binding.uncaughtExceptionSwitch.isChecked = event.value.notifyExceptions
Expand All @@ -184,6 +210,7 @@ internal class SettingsFragment : BaseChildFragment<Nothing, SettingsEvent>(R.la
viewModel.updateCrashMonitor(event.value.copy(includeAllData = isChecked))
}
}

is SettingsEvent.CertificateMonitorChanged -> {
binding.runOnStartSwitch.setOnCheckedChangeListener(null)
binding.runOnStartSwitch.isChecked = event.value.runOnStart
Expand Down Expand Up @@ -240,8 +267,48 @@ internal class SettingsFragment : BaseChildFragment<Nothing, SettingsEvent>(R.la
viewModel.updateCertificatesMonitor(event.value.copy(expireInUnit = ChronoUnit.YEARS))
}
}

SettingsEvent.PermissionsCheck -> {
handleNotificationsPermission()
}
}

private fun handleNotificationsPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
return
}
when {
ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED -> {
// We have permission, all good
}

shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
Snackbar.make(
binding.root,
getString(R.string.sentinel_notification_permission_denied),
Snackbar.LENGTH_LONG
).setAction(getString(R.string.sentinel_change)) {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.fromParts(
getString(R.string.sentinel_package_schema),
requireContext().packageName,
null
)
}.also { intent ->
startActivity(intent)
}
}.show()
}

else -> {
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}

private fun setupSwitch(switchView: SwitchMaterial, trigger: TriggerEntity) =
with(switchView) {
setOnCheckedChangeListener(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,14 @@ internal class SettingsViewModel(
anrObserver.stop()
}
}
if (isAtLeastSomeCrashOptionChecked(entity)) {
emitEvent(SettingsEvent.PermissionsCheck)
}
}

private fun isAtLeastSomeCrashOptionChecked(entity: CrashMonitorEntity): Boolean =
entity.notifyAnrs || entity.notifyExceptions

fun updateCertificatesMonitor(entity: CertificateMonitorEntity) {
launch {
io {
Expand All @@ -141,6 +147,12 @@ internal class SettingsViewModel(
workManager.startCertificatesCheck(it)
} ?: workManager.stopCertificatesCheck()
}
if (isAtLeastSomeCertificateOptionChecked(entity)) {
emitEvent(SettingsEvent.PermissionsCheck)
}
}
}

private fun isAtLeastSomeCertificateOptionChecked(entity: CertificateMonitorEntity): Boolean =
entity.runOnStart || entity.runInBackground || entity.notifyInvalidNow || entity.notifyToExpire
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.infinum.sentinel.data.models.local.CrashEntity
import com.infinum.sentinel.ui.shared.Constants.NOTIFICATIONS_CHANNEL_ID
import me.tatarka.inject.annotations.Inject

@Suppress("TooManyFunctions")
@Inject
internal class SystemNotificationFactory(
private val context: Context,
Expand All @@ -29,6 +30,14 @@ internal class SystemNotificationFactory(
private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

private fun canShowNotifications(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
notificationManager.areNotificationsEnabled()
} else {
true
}
}

init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
Expand Down Expand Up @@ -117,6 +126,9 @@ internal class SystemNotificationFactory(
content: String,
intents: Array<Intent>,
) {
if (!canShowNotifications()) {
return
}
val builder = NotificationCompat.Builder(context, NOTIFICATIONS_CHANNEL_ID)
.setContentIntent(buildPendingIntent(intents))
.setLocalOnly(true)
Expand All @@ -139,6 +151,7 @@ internal class SystemNotificationFactory(
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
PendingIntent.FLAG_MUTABLE

else -> PendingIntent.FLAG_CANCEL_CURRENT
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ internal data class AppInfoTool(
it.context.startActivity(
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts(SCHEME_PACKAGE, it.context.packageName, null)
data = Uri.fromParts(
it.context.getString(R.string.sentinel_package_schema),
it.context.packageName,
null
)
}
)
}
) : Sentinel.Tool {

companion object {
private const val SCHEME_PACKAGE = "package"
}

/**
* A dedicated name for this tool
*
Expand Down
5 changes: 5 additions & 0 deletions sentinel/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<string name="sentinel_search">Search</string>
<string name="sentinel_show">Show</string>
<string name="sentinel_save">Save</string>
<string name="sentinel_change">Change</string>

<string name="sentinel_network">Network</string>
<string name="sentinel_memory">Memory</string>
Expand Down Expand Up @@ -151,5 +152,9 @@
<string name="sentinel_months">Months</string>
<string name="sentinel_years">Years</string>

<string name="sentinel_notification_permission_denied">Notification permission denied. Can\'t show info</string>

<string name="sentinel_unknown">unknown</string>
<string name="sentinel_package_schema">package</string>

</resources>
12 changes: 12 additions & 0 deletions sentinel/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
<item name="colorOnSurfaceInverse">@color/sentinel_inverseOnSurface</item>
<item name="colorSurfaceInverse">@color/sentinel_inverseSurface</item>
<item name="colorPrimaryInverse">@color/sentinel_primaryInverse</item>
<item name="snackbarStyle">@style/Sentinel.Snackbar</item>
<item name="snackbarButtonStyle">@style/Sentinel.Snackbar.TextButton</item>
</style>

<style name="Sentinel.Theme" parent="Sentinel.BaseTheme">
Expand Down Expand Up @@ -89,4 +91,14 @@
<style name="Sentinel.Animation" parent="@android:style/Animation">
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>

<style name="Sentinel.Snackbar" parent="@style/Widget.MaterialComponents.Snackbar">
<item name="android:background">@color/sentinel_onBackground</item>
</style>

<style name="Sentinel.Snackbar.TextButton" parent="@style/Widget.MaterialComponents.Button.TextButton.Snackbar">
<item name="android:textColor">@color/sentinel_primary</item>
</style>


</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class LoggerActivity : AppCompatActivity() {
}
)

@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ internal class LoggerAdapter(
currentList: MutableList<SentinelFileTree.Entry>
) =
onListChanged(currentList.isEmpty())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ internal class LoggerViewHolder(
stackTraceContainer.isVisible = false
root.setOnClickListener(null)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ internal class LogsAdapter(
currentList: MutableList<File>
) =
onListChanged(currentList.isEmpty())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ internal class LogsViewHolder(
deleteButton.setOnClickListener(null)
shareButton.setOnClickListener(null)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.infinum.sentinel.ui.shared
import android.content.Context
import java.io.File
import java.util.Calendar
import java.util.Locale

internal class LogFileResolver(
private val context: Context
Expand All @@ -27,11 +28,12 @@ internal class LogFileResolver(
val year = Calendar.getInstance().get(Calendar.YEAR)

val filename = buildString {
append(String.format("%02d", day))
val locale = Locale.getDefault()
append(String.format(locale = locale, format = "%02d", day))
append("-")
append(String.format("%02d", month))
append(String.format(locale = locale, format = "%02d", month))
append("-")
append(String.format("%04d", year))
append(String.format(locale = locale, format = "%04d", year))
append(LOG_EXTENSION)
}
val nowFile = File("${parent.absolutePath}/$filename")
Expand Down

0 comments on commit 6f20c95

Please sign in to comment.