diff --git a/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/NotificationExtensions.kt b/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/NotificationExtensions.kt new file mode 100644 index 000000000..028599ed9 --- /dev/null +++ b/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/NotificationExtensions.kt @@ -0,0 +1,42 @@ +package co.anitrend.core.android.helpers.notification + +import android.Manifest +import android.content.Context +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker.PERMISSION_GRANTED +import androidx.fragment.app.FragmentActivity +import co.anitrend.core.android.helpers.notification.NotificationHelper.Companion.POST_NOTIFICATION_PERMISSION_REQUEST_CODE +import co.anitrend.core.android.helpers.notification.config.NotificationConfig + + +fun Context.hasNotificationPermissionFor(config: NotificationConfig): Boolean { + val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PERMISSION_GRANTED + } else { + NotificationManagerCompat.from(this).areNotificationsEnabled() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasPermission) { + val channel = NotificationManagerCompat.from(this).getNotificationChannel(config.name) + if (channel != null && channel.importance == config.importance) { + return false + } + } + return hasPermission +} + +fun FragmentActivity.requestPostNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission( + /* context = */ this, + /* permission = */ Manifest.permission.POST_NOTIFICATIONS, + ) != PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + /* activity = */ this, + /* permissions = */ arrayOf(Manifest.permission.POST_NOTIFICATIONS), + /* requestCode = */ POST_NOTIFICATION_PERMISSION_REQUEST_CODE, + ) + } +} diff --git a/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/NotificationHelper.kt b/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/NotificationHelper.kt new file mode 100644 index 000000000..97d2679a8 --- /dev/null +++ b/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/NotificationHelper.kt @@ -0,0 +1,33 @@ +package co.anitrend.core.android.helpers.notification + +import android.app.NotificationChannel +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import co.anitrend.core.android.helpers.notification.config.NotificationConfig + +class NotificationHelper( + private val notificationManager: NotificationManagerCompat +) { + + @RequiresApi(Build.VERSION_CODES.O) + fun createNotificationChannels() { + val channels = NotificationConfig.entries.map { config -> + return with (NotificationChannel(config.name, config.title, config.importance)) { + description = config.description + group = config.group + setShowBadge(true) + enableLights(false) + } + } + notificationManager.createNotificationChannels(channels) + } + + companion object { + const val POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 0x12 + + fun notificationVisibilityFor(isAdult: Boolean) = + if (isAdult) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC + } +} diff --git a/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/config/NotificationConfig.kt b/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/config/NotificationConfig.kt new file mode 100644 index 000000000..b44864e73 --- /dev/null +++ b/android-core/src/main/kotlin/co/anitrend/core/android/helpers/notification/config/NotificationConfig.kt @@ -0,0 +1,29 @@ +package co.anitrend.core.android.helpers.notification.config + +import android.app.NotificationManager + +enum class NotificationConfig( + val title: String, + val description: String, + val importance: Int, + val group: String, +) { + GENERAL( + title = "General", + description = "AniTrend specific notifications", + importance = NotificationManager.IMPORTANCE_DEFAULT, + group = "co.anitrend.notification.group.GENERAL", + ), + ANILIST( + title = "AniList", + description = "AniList related notifications", + importance = NotificationManager.IMPORTANCE_DEFAULT, + group = "co.anitrend.notification.group.ANILIST", + ), + ANNOUNCEMENT( + title = "Announcements", + description = "Announcements and other important information", + importance = NotificationManager.IMPORTANCE_HIGH, + group = "co.anitrend.notification.group.ANNOUNCEMENT", + ) +} diff --git a/android-core/src/main/kotlin/co/anitrend/core/android/koin/Modules.kt b/android-core/src/main/kotlin/co/anitrend/core/android/koin/Modules.kt index 810da4cd3..fafa814b9 100644 --- a/android-core/src/main/kotlin/co/anitrend/core/android/koin/Modules.kt +++ b/android-core/src/main/kotlin/co/anitrend/core/android/koin/Modules.kt @@ -20,6 +20,7 @@ package co.anitrend.core.android.koin import android.content.pm.ShortcutManager import android.net.ConnectivityManager import android.os.PowerManager +import androidx.core.app.NotificationManagerCompat import co.anitrend.arch.extension.dispatchers.SupportDispatcher import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher import co.anitrend.arch.extension.ext.systemServiceOf @@ -27,6 +28,7 @@ import co.anitrend.arch.extension.util.date.contract.AbstractSupportDateHelper import co.anitrend.core.android.controller.power.AndroidPowerController import co.anitrend.core.android.controller.power.contract.IPowerController import co.anitrend.core.android.helpers.date.AniTrendDateHelper +import co.anitrend.core.android.helpers.notification.NotificationHelper import co.anitrend.core.android.settings.Settings import co.anitrend.core.android.settings.helper.config.ConfigurationHelper import co.anitrend.core.android.settings.helper.config.contract.IConfigurationHelper @@ -68,6 +70,13 @@ private val coreModule = module { val localeHelper = get() PrettyTime(localeHelper.locale) } + + factory { + NotificationHelper( + notificationManager = NotificationManagerCompat + .from(androidContext()) + ) + } } private val configurationModule = module { diff --git a/app-core/src/main/kotlin/co/anitrend/core/component/screen/AniTrendScreen.kt b/app-core/src/main/kotlin/co/anitrend/core/component/screen/AniTrendScreen.kt index 82cdc7797..965c53d17 100644 --- a/app-core/src/main/kotlin/co/anitrend/core/component/screen/AniTrendScreen.kt +++ b/app-core/src/main/kotlin/co/anitrend/core/component/screen/AniTrendScreen.kt @@ -51,7 +51,6 @@ abstract class AniTrendScreen : SupportActivity(), AndroidScopeComponent, KoinSc override val scope by activityRetainedScope() - private val connectivity by inject() /** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a1719303c..1ed697631 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + () { observeNavigationDrawer() } } + lifecycleScope.launch { + val needsNotificationPermission = NotificationConfig.entries.any { + !hasNotificationPermissionFor(it) + } + if (needsNotificationPermission) { + requestPostNotificationPermission() + } + } onUpdateUserInterface() } diff --git a/buildSrc/src/main/java/co/anitrend/buildSrc/plugins/components/ProjectConfiguration.kt b/buildSrc/src/main/java/co/anitrend/buildSrc/plugins/components/ProjectConfiguration.kt index f43b990d8..66d7ef63e 100644 --- a/buildSrc/src/main/java/co/anitrend/buildSrc/plugins/components/ProjectConfiguration.kt +++ b/buildSrc/src/main/java/co/anitrend/buildSrc/plugins/components/ProjectConfiguration.kt @@ -93,7 +93,7 @@ private fun Project.configureLint() = baseAppExtension().run { internal fun Project.configureAndroid(): Unit = baseExtension().run { compileSdkVersion(35) defaultConfig { - minSdk = 23 + minSdk = 24 targetSdk = 35 versionCode = props[PropertyTypes.CODE].toInt() versionName = props[PropertyTypes.VERSION]