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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import co.anitrend.data.settings.feature.IFeatureFlagSetting
import co.anitrend.data.settings.notification.INotificationSettings
import co.anitrend.data.settings.power.IPowerSettings
import co.anitrend.data.settings.privacy.IPrivacySettings
import co.anitrend.data.settings.push.IUnifiedPushSettings
import co.anitrend.data.settings.refresh.IRefreshBehaviourSettings
import co.anitrend.data.settings.sort.ISortOrderSettings
import co.anitrend.data.settings.sync.ISyncSettings
Expand Down Expand Up @@ -82,6 +83,7 @@ private val coreModule =
IPowerSettings::class,
IConnectivitySettings::class,
INotificationSettings::class,
IUnifiedPushSettings::class,
IUserSettings::class,
ICacheSettings::class,
ISyncSettings::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import co.anitrend.arch.extension.settings.EnumSetting
import co.anitrend.arch.extension.settings.FloatSetting
import co.anitrend.arch.extension.settings.IntSetting
import co.anitrend.arch.extension.settings.LongSetting
import co.anitrend.arch.extension.settings.NullableStringSetting
import co.anitrend.arch.extension.settings.SetSetting
import co.anitrend.data.auth.settings.IAuthenticationSettings
import co.anitrend.data.auth.settings.IAuthenticationSettings.Companion.INVALID_USER_ID
Expand All @@ -40,6 +41,7 @@ import co.anitrend.data.settings.feature.IFeatureFlagSetting
import co.anitrend.data.settings.notification.INotificationSettings
import co.anitrend.data.settings.power.IPowerSettings
import co.anitrend.data.settings.privacy.IPrivacySettings
import co.anitrend.data.settings.push.IUnifiedPushSettings
import co.anitrend.data.settings.refresh.IRefreshBehaviourSettings
import co.anitrend.data.settings.sort.ISortOrderSettings
import co.anitrend.data.settings.sync.ISyncSettings
Expand All @@ -64,7 +66,8 @@ class Settings(
ICacheSettings,
ISyncSettings,
IDeveloperSettings,
IFeatureFlagSetting {
IFeatureFlagSetting,
IUnifiedPushSettings {
override val locale =
EnumSetting(
key = R.string.settings_configuration_locale,
Expand Down Expand Up @@ -193,6 +196,22 @@ class Settings(
preference = this,
)

override val unifiedPushEndpoint =
NullableStringSetting(
key = R.string.settings_notifications_unifiedpush_endpoint,
default = null,
resources = context.resources,
preference = this,
)

override val unifiedPushInstance =
NullableStringSetting(
key = R.string.settings_notifications_unifiedpush_instance,
default = null,
resources = context.resources,
preference = this,
)

override val titleLanguage =
EnumSetting(
key = R.string.settings_user_title_language,
Expand Down
2 changes: 2 additions & 0 deletions android/core/src/main/res/values/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<string name="settings_notifications_enabled" translatable="false">_notificationsEnabled</string>
<string name="settings_notifications_anilist_enabled" translatable="false">_notificationsAniListEnabled</string>
<string name="settings_notifications_announcements_enabled" translatable="false">_notificationsAnnouncementsEnabled</string>
<string name="settings_notifications_unifiedpush_endpoint" translatable="false">_notificationsUnifiedPushEndpoint</string>
<string name="settings_notifications_unifiedpush_instance" translatable="false">_notificationsUnifiedPushInstance</string>

<string name="settings_auto_heap_dump" translatable="false">_autoHeapDump</string>
<string name="settings_show_leak_launcher" translatable="false">_showLeakLauncher</string>
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
/** Material Design */
implementation(libs.google.android.material)

githubImplementation(libs.unifiedpush.connector)
googleImplementation(libs.google.android.gsm.playServicesOssLicenses)
googleImplementation(libs.google.firebase.messaging.ktx)
}
Expand Down
5 changes: 4 additions & 1 deletion app/core/src/main/res/xml/backup_descriptor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
<full-backup-content>
<!-- https://developer.android.com/guide/topics/data/autobackup#XMLSyntax -->
<!-- Exclude specific shared preferences that contain GCM registration Id -->
<include domain="sharedpref" path="."/>
<include domain="sharedpref" path="." />
<exclude domain="database" path="unifiedpush-connector" />
<exclude domain="database" path="unifiedpush-connector-journal" />
<exclude domain="sharedpref" path="unifiedpush.connector.xml" />
</full-backup-content>
29 changes: 28 additions & 1 deletion app/src/github/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,33 @@

<application
android:allowBackup="false"
tools:replace="android:allowBackup" />
tools:replace="android:allowBackup">

<service
android:name=".service.ForegroundService"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.RAISE_TO_FOREGROUND" />
</intent-filter>
</service>

<service
android:name=".service.MessagingService"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT" />
</intent-filter>
</service>

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="co.anitrend.initializer.GithubApplicationInitializer"
android:value="androidx.startup" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2026 AniTrend
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package co.anitrend.initializer

import android.content.Context
import androidx.startup.Initializer
import co.anitrend.android.core.koinOf
import co.anitrend.core.initializer.contract.AbstractCoreInitializer
import co.anitrend.core.koin.helper.DynamicFeatureModuleHelper.Companion.loadModules
import co.anitrend.koin.githubAppModules
import co.anitrend.push.PushRegistrationCoordinator

class GithubApplicationInitializer : AbstractCoreInitializer<Unit>() {
/**
* @return A list of dependencies that this [Initializer] depends on. This is
* used to determine initialization order of [Initializer]s.
*
* For e.g. if a [Initializer] `B` defines another [Initializer] `A` as its dependency,
* then `A` gets initialized before `B`.
*/
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(ApplicationInitializer::class.java)

override fun create(context: Context) {
githubAppModules.loadModules()
koinOf<PushRegistrationCoordinator>()
}
}
47 changes: 47 additions & 0 deletions app/src/github/kotlin/co/anitrend/koin/GithubModules.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2026 AniTrend
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package co.anitrend.koin

import co.anitrend.core.R
import co.anitrend.core.koin.helper.DynamicFeatureModuleHelper
import co.anitrend.push.PushConnector
import co.anitrend.push.PushRegistrationCoordinator
import co.anitrend.push.UnifiedPushConnector
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module

private val variantModule =
module(createdAtStart = true) {
factory<PushConnector> {
UnifiedPushConnector(
context = androidApplication(),
)
}
factory {
PushRegistrationCoordinator(
connector = get(),
messageForDistributor =
androidContext().getString(R.string.application_name),
)
}
}

internal val githubAppModules =
DynamicFeatureModuleHelper(
listOf(variantModule),
)
44 changes: 44 additions & 0 deletions app/src/github/kotlin/co/anitrend/push/UnifiedPushConnector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2026 AniTrend
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package co.anitrend.push

import android.content.Context
import org.unifiedpush.android.connector.UnifiedPush

internal class UnifiedPushConnector(
private val context: Context,
) : PushConnector {
override val acknowledgedDistributor: String?
get() = UnifiedPush.getAckDistributor(context)

override val savedDistributor: String?
get() = UnifiedPush.getSavedDistributor(context)

override val availableDistributors: List<String>
get() = UnifiedPush.getDistributors(context)

override fun saveDistributor(distributor: String) {
UnifiedPush.saveDistributor(context, distributor)
}

override fun register(messageForDistributor: String) {
UnifiedPush.register(
context = context,
messageForDistributor = messageForDistributor,
)
}
}
28 changes: 28 additions & 0 deletions app/src/github/kotlin/co/anitrend/service/ForegroundService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2026 AniTrend
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package co.anitrend.service

import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder

class ForegroundService : Service() {
private val binder = Binder()

override fun onBind(intent: Intent): IBinder = binder
}
68 changes: 63 additions & 5 deletions app/src/github/kotlin/co/anitrend/service/MessagingService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,68 @@
*/
package co.anitrend.service

import android.content.Intent
import android.os.IBinder
import co.anitrend.core.service.AniTrendLifecycleService
import co.anitrend.data.settings.push.IUnifiedPushSettings
import co.anitrend.push.PushRegistrationCoordinator
import org.koin.android.ext.android.inject
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.PushService
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
import timber.log.Timber

class MessagingService : AniTrendLifecycleService() {
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
class MessagingService : PushService() {
private val settings by inject<IUnifiedPushSettings>()
private val coordinator by inject<PushRegistrationCoordinator>()

override fun onMessage(
message: PushMessage,
instance: String,
) {
Timber.i(
"UnifiedPush payload received for instance=%s size=%d decrypted=%s",
instance,
message.content.size,
message.decrypted,
)
}

override fun onNewEndpoint(
endpoint: PushEndpoint,
instance: String,
) {
val previousEndpoint = settings.endpointFor(instance)
settings.updateEndpoint(instance, endpoint.url)

if (previousEndpoint == endpoint.url) {
Timber.i("UnifiedPush endpoint acknowledged for instance=%s", instance)
} else {
Timber.i(
"UnifiedPush endpoint updated for instance=%s temporary=%s",
instance,
endpoint.temporary,
)
}
}

override fun onRegistrationFailed(
reason: FailedReason,
instance: String,
) {
Timber.w("UnifiedPush registration failed for instance=%s reason=%s", instance, reason)

if (reason == FailedReason.INTERNAL_ERROR) {
coordinator.ensureRegistered()
}
}

override fun onTempUnavailable(instance: String) {
super.onTempUnavailable(instance)
Timber.w("UnifiedPush distributor temporarily unavailable for instance=%s", instance)
}

override fun onUnregistered(instance: String) {
settings.clearEndpoint(instance)
Timber.w("UnifiedPush registration removed for instance=%s", instance)
coordinator.ensureRegistered()
}
}
Loading