diff --git a/atox/src/main/kotlin/BootReceiver.kt b/atox/src/main/kotlin/BootReceiver.kt index 28023415..6730e09b 100644 --- a/atox/src/main/kotlin/BootReceiver.kt +++ b/atox/src/main/kotlin/BootReceiver.kt @@ -1,12 +1,16 @@ -// SPDX-FileCopyrightText: 2020-2021 aTox contributors +// SPDX-FileCopyrightText: 2020-2023 Robin Lindén // // SPDX-License-Identifier: GPL-3.0-only package ltd.evilcorp.atox +import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -16,6 +20,7 @@ import ltd.evilcorp.atox.tox.ToxStarter import ltd.evilcorp.domain.tox.ToxSaveStatus private const val ENCRYPTED = "aTox profile encrypted" +private const val TAG = "BootReceiver" class BootReceiver : BroadcastReceiver() { @Inject @@ -25,6 +30,16 @@ class BootReceiver : BroadcastReceiver() { if (intent.action == Intent.ACTION_BOOT_COMPLETED) { (context.applicationContext as App).component.inject(this) if (toxStarter.tryLoadTox(null) == ToxSaveStatus.Encrypted) { + Log.i(TAG, "Telling the user to unlock their profile") + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w(TAG, "Missing notify-permission") + return + } + val channel = NotificationChannelCompat.Builder(ENCRYPTED, NotificationManagerCompat.IMPORTANCE_HIGH) .setName(context.getString(R.string.atox_profile_locked)) .setDescription(context.getString(R.string.channel_profile_locked_explanation)) diff --git a/atox/src/main/kotlin/ToxService.kt b/atox/src/main/kotlin/ToxService.kt index 450a2fe4..10a87cba 100644 --- a/atox/src/main/kotlin/ToxService.kt +++ b/atox/src/main/kotlin/ToxService.kt @@ -1,14 +1,18 @@ -// SPDX-FileCopyrightText: 2019-2022 aTox contributors +// SPDX-FileCopyrightText: 2019-2023 Robin Lindén +// SPDX-FileCopyrightText: 2021-2022 aTox contributors // // SPDX-License-Identifier: GPL-3.0-only package ltd.evilcorp.atox +import android.Manifest import android.app.Notification import android.app.PendingIntent import android.content.Intent +import android.content.pm.PackageManager import android.os.Build import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -73,8 +77,8 @@ class ToxService : LifecycleService() { notifier.createNotificationChannel(channel) } - private fun subTextFor(status: ConnectionStatus?) = when (status) { - null, ConnectionStatus.None -> getText(R.string.atox_offline) + private fun subTextFor(status: ConnectionStatus) = when (status) { + ConnectionStatus.None -> getText(R.string.atox_offline) ConnectionStatus.TCP -> getText(R.string.atox_connected_with_tcp) ConnectionStatus.UDP -> getText(R.string.atox_connected_with_udp) } @@ -85,13 +89,20 @@ class ToxService : LifecycleService() { PendingIntentCompat.getActivity(this, 0, notificationIntent, 0) } - return NotificationCompat.Builder(this, channelId) + val builder = NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.ic_notification) .setColor(ResourcesCompat.getColor(resources, R.color.colorPrimary, null)) .setContentIntent(pendingIntent) .setContentTitle(getString(R.string.tox_service_running)) - .setContentText(subTextFor(status)) - .build() + + if (status != null) { + // Either we haven't received a status from Tox yet, or we don't + // have notification permissions meaning we wouldn't be able to + // update a status if we showed one. + builder.setContentText(subTextFor(status)) + } + + return builder.build() } override fun onCreate() { @@ -106,6 +117,14 @@ class ToxService : LifecycleService() { } } + val canPostNotifications = ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + if (!canPostNotifications) { + Log.w(TAG, "Notifications disallowed") + } + createNotificationChannel() startForeground(NOTIFICATION_ID, notificationFor(connectionStatus)) @@ -116,7 +135,10 @@ class ToxService : LifecycleService() { .flowWithLifecycle(lifecycle) .collect { user -> connectionStatus = user.connectionStatus - notifier.notify(NOTIFICATION_ID, notificationFor(connectionStatus)) + if (canPostNotifications) { + notifier.notify(NOTIFICATION_ID, notificationFor(connectionStatus)) + } + if (connectionStatus == ConnectionStatus.None) { Log.i(TAG, "Gone offline, scheduling bootstrap") bootstrapTimer.schedule(BOOTSTRAP_INTERVAL_MS, BOOTSTRAP_INTERVAL_MS) { diff --git a/atox/src/main/kotlin/ui/NotificationHelper.kt b/atox/src/main/kotlin/ui/NotificationHelper.kt index 20bde713..ce585b6d 100644 --- a/atox/src/main/kotlin/ui/NotificationHelper.kt +++ b/atox/src/main/kotlin/ui/NotificationHelper.kt @@ -1,13 +1,16 @@ -// SPDX-FileCopyrightText: 2019-2022 aTox contributors +// SPDX-FileCopyrightText: 2019-2023 Robin Lindén +// SPDX-FileCopyrightText: 2022 aTox contributors // // SPDX-License-Identifier: GPL-3.0-only package ltd.evilcorp.atox.ui +import android.Manifest import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint @@ -17,6 +20,8 @@ import android.graphics.Rect import android.media.AudioAttributes import android.media.RingtoneManager import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -44,6 +49,8 @@ import ltd.evilcorp.core.vo.FriendRequest import ltd.evilcorp.core.vo.UserStatus import ltd.evilcorp.domain.tox.PublicKey +private const val TAG = "NotificationHelper" + private const val MESSAGE = "aTox messages" private const val FRIEND_REQUEST = "aTox friend requests" private const val CALL = "aTox calls" @@ -119,6 +126,15 @@ class NotificationHelper @Inject constructor( outgoing: Boolean = false, silent: Boolean = outgoing, ) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w(TAG, "Received message, notifications disallowed") + return + } + val notificationBuilder = NotificationCompat.Builder(context, MESSAGE) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(android.R.drawable.sym_action_chat) @@ -214,6 +230,15 @@ class NotificationHelper @Inject constructor( } fun showFriendRequestNotification(friendRequest: FriendRequest, silent: Boolean) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w(TAG, "Received friend request, notifications disallowed") + return + } + val notificationBuilder = NotificationCompat.Builder(context, FRIEND_REQUEST) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(android.R.drawable.btn_star_big_on) @@ -235,6 +260,15 @@ class NotificationHelper @Inject constructor( notifier.cancel(pk.string().hashCode() + CALL.hashCode()) fun showOngoingCallNotification(contact: Contact) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w(TAG, "Call ongoing, notifications disallowed") + return + } + dismissCallNotification(PublicKey(contact.publicKey)) val notificationBuilder = NotificationCompat.Builder(context, CALL) .setCategory(NotificationCompat.CATEGORY_CALL) @@ -278,6 +312,15 @@ class NotificationHelper @Inject constructor( } fun showPendingCallNotification(status: UserStatus, c: Contact) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w(TAG, "Call pending, notifications disallowed") + return + } + val notification = NotificationCompat.Builder(context, CALL) .setCategory(NotificationCompat.CATEGORY_CALL) .setSmallIcon(android.R.drawable.ic_menu_call) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af1729fa..9de48e01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,9 @@ coroutines = "1.7.3" dagger = "2.48" espresso = "3.5.1" jackson = "2.15.2" -lifecycle = "2.5.1" # 2.6.1 -navigation = "2.5.3" # 2.7.0 -room = "2.4.3" # 2.5.2 +lifecycle = "2.6.1" +navigation = "2.6.0" # 2.7.1 requires API 34. +room = "2.5.2" tox4j-android = "0.2.18" tox4j-core = "0.2.3" @@ -24,11 +24,11 @@ androidApplication = { id = "com.android.application", version.ref = "android-pl androidLibrary = { id = "com.android.library", version.ref = "android-plugin" } [libraries] -androidx-activity = "androidx.activity:activity:1.5.1" # 1.7.2 -androidx-appcompat = "androidx.appcompat:appcompat:1.4.2" # 1.6.1 +androidx-activity = "androidx.activity:activity:1.7.2" +androidx-appcompat = "androidx.appcompat:appcompat:1.6.1" androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" -androidx-core-ktx = "androidx.core:core-ktx:1.8.0" # 1.10.1 -androidx-fragment = "androidx.fragment:fragment:1.5.7" # 1.6.1 +androidx-core-ktx = "androidx.core:core-ktx:1.10.1" +androidx-fragment = "androidx.fragment:fragment:1.6.1" androidx-multidex = "androidx.multidex:multidex:2.0.1" androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigation" } @@ -45,7 +45,7 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "room" } -google-android-material = "com.google.android.material:material:1.5.0" # 1.9.0 +google-android-material = "com.google.android.material:material:1.9.0" google-dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } google-dagger-core = { module = "com.google.dagger:dagger", version.ref = "dagger" }