Compare commits

..

3 Commits

Author SHA1 Message Date
GitHub Actions
6f583d1227 chore: bump version to 4.11.89 [skip ci] 2026-04-26 00:56:21 +00:00
6df60c8348 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 5s
Build / build-android (push) Successful in 4m48s
Build / create-release (push) Successful in 14s
2026-04-26 10:56:12 +10:00
79d46857f9 feat: integrate UnifiedPush for background synchronization and event handling
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 10:56:10 +10:00
11 changed files with 520 additions and 86 deletions

View File

@@ -65,6 +65,7 @@ dependencies {
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'org.unifiedpush.android:connector:3.3.2'
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

View File

@@ -40,6 +40,14 @@
android:exported="false"
android:stopWithTask="false" />
<service
android:name=".UnifiedPushService"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT" />
</intent-filter>
</service>
<receiver
android:name=".BootReceiver"
android:exported="true">
@@ -47,15 +55,6 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".PushPingReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="com.paarrot.app.ACTION_PUSH_PING" />
</intent-filter>
</receiver>
</application>
<!-- Permissions -->

View File

@@ -5,13 +5,17 @@ import android.content.Context
import android.content.Intent
/**
* Triggers a one-shot sync fetch after boot if credentials exist.
* Re-registers UnifiedPush after boot when credentials still exist.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
MatrixSyncService.requestSyncFetch(context, "boot_completed")
val prefs = context.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
prefs.getString(MatrixSyncService.EXTRA_HOMESERVER, null) ?: return
prefs.getString(MatrixSyncService.EXTRA_TOKEN, null) ?: return
UnifiedPushManager.register(context, activity = null)
}
}

View File

@@ -23,10 +23,8 @@ import java.net.URL
import java.net.URLEncoder
/**
* ForegroundService that performs a short, one-shot Matrix /sync fetch.
*
* This service is wake-triggered by push pings (UnifiedPush/FCM bridge) and
* then fetches message content from Matrix before posting local notifications.
* ForegroundService that performs a short, one-shot Matrix /sync fetch after
* a wake ping from UnifiedPush or a manual diagnostic trigger.
*/
class MatrixSyncService : Service() {
@@ -53,7 +51,7 @@ class MatrixSyncService : Service() {
return START_NOT_STICKY
}
val triggerReason = intent?.getStringExtra(EXTRA_TRIGGER_REASON) ?: "unknown"
val triggerReason = intent?.getStringExtra(EXTRA_TRIGGER_REASON) ?: MODE_ONE_SHOT
startForeground(NOTIF_ID_STATUS, buildStatusNotification())
serviceScope.launch {
@@ -245,7 +243,7 @@ class MatrixSyncService : Service() {
private const val CHANNEL_STATUS = "sync_status"
private const val CHANNEL_MESSAGES = "messages"
private const val MIN_WAKE_INTERVAL_MS = 7_500L
const val ACTION_PUSH_PING = "com.paarrot.app.ACTION_PUSH_PING"
const val MODE_ONE_SHOT = "one_shot"
/** Set by [SyncServicePlugin] — true when the Capacitor WebView UI is visible. */
@Volatile

View File

@@ -1,25 +0,0 @@
package com.paarrot.app
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* Receives tiny push wake pings and triggers one Matrix fetch.
*
* - `org.unifiedpush.android.connector.MESSAGE` supports UnifiedPush distributors.
* - [MatrixSyncService.ACTION_PUSH_PING] is an app-local fallback action
* that other bridges (e.g., FCM service) can emit.
*/
class PushPingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val reason = when (intent.action) {
"org.unifiedpush.android.connector.MESSAGE" -> "unifiedpush_ping"
MatrixSyncService.ACTION_PUSH_PING -> "app_push_ping"
else -> return
}
MatrixSyncService.requestSyncFetch(context, reason)
}
}

View File

@@ -12,16 +12,30 @@ import com.getcapacitor.annotation.CapacitorPlugin
* Capacitor plugin that controls [MatrixSyncService] from JS.
*
* JS API:
* - `start({ homeserverUrl, accessToken, userId, deviceId })` — persist/update sync credentials
* - `start({ homeserverUrl, accessToken, userId, deviceId })` — persist credentials and register UnifiedPush
* - `triggerPing({ reason })` — force a one-shot fetch (for testing/manual wake)
* - `stop()` — clear persisted credentials and stop any in-flight fetch
* - `stop()` — clear persisted credentials and unregister UnifiedPush
* - `setAppForeground({ foreground })` — tell the service whether the app UI is visible
* - `getStatus()` — returns `{ running: boolean }`
* - `getStatus()` — returns current fetch and UnifiedPush state
*/
@CapacitorPlugin(name = "MatrixBackgroundSync")
class SyncServicePlugin : Plugin() {
/** Persists Matrix credentials used later by push-triggered sync fetches. */
fun emitUnifiedPushEvent(eventName: String, payload: JSObject) {
notifyListeners(eventName, payload, true)
}
override fun load() {
super.load()
UnifiedPushManager.setPlugin(this)
}
override fun handleOnDestroy() {
UnifiedPushManager.clearPlugin(this)
super.handleOnDestroy()
}
/** Persists Matrix credentials and starts UnifiedPush registration. */
@PluginMethod
fun start(call: PluginCall) {
val homeserver = call.getString("homeserverUrl")
@@ -38,6 +52,8 @@ class SyncServicePlugin : Plugin() {
.putString(MatrixSyncService.EXTRA_DEVICE_ID, deviceId)
.apply()
UnifiedPushManager.register(context, activity)
call.resolve()
}
@@ -54,6 +70,7 @@ class SyncServicePlugin : Plugin() {
fun stop(call: PluginCall) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
context.stopService(Intent(context, MatrixSyncService::class.java))
UnifiedPushManager.unregister(context)
call.resolve()
}
@@ -76,7 +93,8 @@ class SyncServicePlugin : Plugin() {
@Suppress("DEPRECATION")
val running = am.getRunningServices(Int.MAX_VALUE)
.any { it.service.className == MatrixSyncService::class.java.name }
call.resolve(JSObject().apply { put("running", running) })
val result = UnifiedPushManager.getStatus(context).apply { put("running", running) }
call.resolve(result)
}
companion object {

View File

@@ -0,0 +1,149 @@
package com.paarrot.app
import android.app.Activity
import android.content.Context
import android.util.Log
import com.getcapacitor.JSObject
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.INSTANCE_DEFAULT
import org.unifiedpush.android.connector.UnifiedPush
/** Coordinates UnifiedPush registration state and bridges events back to JS. */
object UnifiedPushManager {
private const val TAG = "UnifiedPushManager"
private const val PREFS = "unifiedpush_prefs"
private const val KEY_ENDPOINT = "endpoint"
private const val KEY_INSTANCE = "instance"
private const val EVENT_NEW_ENDPOINT = "unifiedPushNewEndpoint"
private const val EVENT_UNREGISTERED = "unifiedPushUnregistered"
private const val EVENT_REGISTRATION_FAILED = "unifiedPushRegistrationFailed"
private const val DEFAULT_MESSAGE = "Paarrot notifications"
@Volatile
private var plugin: SyncServicePlugin? = null
fun setPlugin(plugin: SyncServicePlugin) {
this.plugin = plugin
}
fun clearPlugin(plugin: SyncServicePlugin) {
if (this.plugin === plugin) {
this.plugin = null
}
}
fun register(context: Context, activity: Activity?) {
val savedDistributor = runCatching { UnifiedPush.getSavedDistributor(context) }.getOrNull().orEmpty()
if (savedDistributor.isNotBlank()) {
requestRegistration(context)
return
}
if (activity == null) {
dispatchRegistrationFailed(FailedReason.ACTION_REQUIRED.name, INSTANCE_DEFAULT)
return
}
UnifiedPush.tryUseCurrentOrDefaultDistributor(activity) { success ->
if (success) {
requestRegistration(context)
} else {
dispatchRegistrationFailed(FailedReason.ACTION_REQUIRED.name, INSTANCE_DEFAULT)
}
}
}
fun unregister(context: Context) {
runCatching {
UnifiedPush.unregister(context, INSTANCE_DEFAULT)
}.onFailure {
Log.w(TAG, "Failed to unregister UnifiedPush: ${it.message}")
}
val previousEndpoint = getEndpoint(context)
clearEndpoint(context)
dispatchUnregistered(previousEndpoint, INSTANCE_DEFAULT)
}
fun getStatus(context: Context): JSObject {
val status = JSObject()
status.put("endpoint", getEndpoint(context) ?: "")
status.put("instance", getInstance(context) ?: INSTANCE_DEFAULT)
status.put("registered", !getEndpoint(context).isNullOrBlank())
status.put("distributor", runCatching { UnifiedPush.getSavedDistributor(context) }.getOrDefault(""))
status.put(
"distributors",
runCatching { UnifiedPush.getDistributors(context) }.getOrDefault(emptyList<String>())
)
return status
}
fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
val previousEndpoint = getEndpoint(context)
persistEndpoint(context, endpoint, instance)
val payload = JSObject().apply {
put("endpoint", endpoint)
put("instance", instance)
put("previousEndpoint", previousEndpoint ?: "")
}
dispatch(EVENT_NEW_ENDPOINT, payload)
}
fun onRegistrationFailed(reason: String, instance: String) {
dispatchRegistrationFailed(reason, instance)
}
fun onUnregistered(context: Context, instance: String) {
val previousEndpoint = getEndpoint(context)
clearEndpoint(context)
dispatchUnregistered(previousEndpoint, instance)
}
private fun requestRegistration(context: Context) {
runCatching {
UnifiedPush.register(context, INSTANCE_DEFAULT, DEFAULT_MESSAGE, null)
}.onFailure {
Log.w(TAG, "UnifiedPush register failed: ${it.message}")
dispatchRegistrationFailed(FailedReason.INTERNAL_ERROR.name, INSTANCE_DEFAULT)
}
}
private fun dispatchUnregistered(previousEndpoint: String?, instance: String) {
val payload = JSObject().apply {
put("previousEndpoint", previousEndpoint ?: "")
put("instance", instance)
}
dispatch(EVENT_UNREGISTERED, payload)
}
private fun dispatchRegistrationFailed(reason: String, instance: String) {
val payload = JSObject().apply {
put("reason", reason)
put("instance", instance)
}
dispatch(EVENT_REGISTRATION_FAILED, payload)
}
private fun dispatch(eventName: String, payload: JSObject) {
plugin?.emitUnifiedPushEvent(eventName, payload)
}
private fun persistEndpoint(context: Context, endpoint: String, instance: String) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
.putString(KEY_ENDPOINT, endpoint)
.putString(KEY_INSTANCE, instance)
.apply()
}
private fun clearEndpoint(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
.remove(KEY_ENDPOINT)
.remove(KEY_INSTANCE)
.apply()
}
private fun getEndpoint(context: Context): String? =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_ENDPOINT, null)
private fun getInstance(context: Context): String? =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_INSTANCE, null)
}

View File

@@ -0,0 +1,35 @@
package com.paarrot.app
import android.util.Log
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
/** Receives UnifiedPush events and turns push messages into Matrix fetch wakes. */
class UnifiedPushService : PushService() {
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
Log.i(TAG, "UnifiedPush endpoint updated for instance=$instance")
UnifiedPushManager.onNewEndpoint(applicationContext, endpoint.url, instance)
}
override fun onMessage(message: PushMessage, instance: String) {
Log.d(TAG, "UnifiedPush wake ping received for instance=$instance, bytes=${message.content.size}")
MatrixSyncService.requestSyncFetch(applicationContext, "unifiedpush_message")
}
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
Log.w(TAG, "UnifiedPush registration failed: $reason")
UnifiedPushManager.onRegistrationFailed(reason.name, instance)
}
override fun onUnregistered(instance: String) {
Log.i(TAG, "UnifiedPush unregistered for instance=$instance")
UnifiedPushManager.onUnregistered(applicationContext, instance)
}
private companion object {
const val TAG = "UnifiedPushService"
}
}

View File

@@ -2,7 +2,7 @@
buildscript {
ext {
kotlin_version = '1.9.25'
kotlin_version = '2.3.0'
}
repositories {

View File

@@ -1,8 +1,33 @@
import { Capacitor, registerPlugin } from '@capacitor/core';
import type { MatrixClient } from 'matrix-js-sdk';
import { Capacitor, registerPlugin, type PluginListenerHandle } from '@capacitor/core';
import type { IPusherRequest, MatrixClient } from 'matrix-js-sdk';
type UnifiedPushStatus = {
running: boolean;
endpoint: string;
instance: string;
registered: boolean;
distributor: string;
distributors: string[];
};
type UnifiedPushEndpointEvent = {
endpoint: string;
previousEndpoint: string;
instance: string;
};
type UnifiedPushUnregisteredEvent = {
previousEndpoint: string;
instance: string;
};
type UnifiedPushRegistrationFailedEvent = {
reason: string;
instance: string;
};
interface MatrixBackgroundSyncPlugin {
/** Persist credentials used by push-triggered one-shot sync fetches. */
/** Persist credentials and request UnifiedPush registration. */
start(options: {
homeserverUrl: string;
accessToken: string;
@@ -11,7 +36,7 @@ interface MatrixBackgroundSyncPlugin {
}): Promise<void>;
/** Trigger a one-shot fetch as if a push ping arrived. */
triggerPing(options: { reason?: string }): Promise<void>;
/** Stop any in-flight fetch and clear persisted credentials. */
/** Stop any in-flight fetch, clear credentials, and unregister UnifiedPush. */
stop(): Promise<void>;
/**
* Notify the service whether the app UI is visible.
@@ -19,45 +44,285 @@ interface MatrixBackgroundSyncPlugin {
* because the JS layer handles them via LocalNotifications.
*/
setAppForeground(options: { foreground: boolean }): Promise<void>;
/** Returns whether the sync service is currently running. */
getStatus(): Promise<{ running: boolean }>;
/** Returns fetch state and current UnifiedPush registration details. */
getStatus(): Promise<UnifiedPushStatus>;
addListener(
eventName: 'unifiedPushNewEndpoint',
listenerFunc: (event: UnifiedPushEndpointEvent) => void
): Promise<PluginListenerHandle>;
addListener(
eventName: 'unifiedPushUnregistered',
listenerFunc: (event: UnifiedPushUnregisteredEvent) => void
): Promise<PluginListenerHandle>;
addListener(
eventName: 'unifiedPushRegistrationFailed',
listenerFunc: (event: UnifiedPushRegistrationFailedEvent) => void
): Promise<PluginListenerHandle>;
}
type StoredPusherState = {
endpoint: string;
appId: string;
};
const MatrixBackgroundSync = registerPlugin<MatrixBackgroundSyncPlugin>('MatrixBackgroundSync');
const DEFAULT_UNIFIED_PUSH_GATEWAY = 'https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify';
const PUSHER_APP_ID_BASE = 'com.paarrot.app.android';
const PUSHER_STORAGE_PREFIX = 'paarrot.unifiedpush';
/** Returns true when the current platform is Android Capacitor. */
export const isBackgroundSyncSupported = (): boolean =>
Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android';
/**
* Persists Matrix credentials for Android push-ping wake handling.
* Safe to call on every login.
* @param mx Authenticated Matrix client
*/
export const startBackgroundSync = async (mx: MatrixClient): Promise<void> => {
if (!isBackgroundSyncSupported()) return;
const getStoredPusherKey = (userId: string | null, deviceId: string | null): string =>
`${PUSHER_STORAGE_PREFIX}:${userId ?? 'unknown'}:${deviceId ?? 'unknown'}`;
const homeserverUrl = mx.getHomeserverUrl();
const accessToken = mx.getAccessToken();
const userId = mx.getUserId();
const deviceId = mx.getDeviceId();
const buildPusherAppId = (deviceId: string | null): string => {
const raw = deviceId ? `${PUSHER_APP_ID_BASE}.${deviceId}` : PUSHER_APP_ID_BASE;
return raw.length > 64 ? raw.slice(0, 64) : raw;
};
if (!homeserverUrl || !accessToken || !userId) {
console.warn('[BackgroundSync] Missing credentials, not starting');
return;
}
const loadStoredPusherState = (
userId: string | null,
deviceId: string | null
): StoredPusherState | undefined => {
if (typeof window === 'undefined' || !window.localStorage) return undefined;
const raw = window.localStorage.getItem(getStoredPusherKey(userId, deviceId));
if (!raw) return undefined;
try {
return JSON.parse(raw) as StoredPusherState;
} catch {
return undefined;
}
};
const saveStoredPusherState = (
userId: string | null,
deviceId: string | null,
state: StoredPusherState
): void => {
if (typeof window === 'undefined' || !window.localStorage) return;
window.localStorage.setItem(getStoredPusherKey(userId, deviceId), JSON.stringify(state));
};
const clearStoredPusherState = (userId: string | null, deviceId: string | null): void => {
if (typeof window === 'undefined' || !window.localStorage) return;
window.localStorage.removeItem(getStoredPusherKey(userId, deviceId));
};
const resolveUnifiedPushGateway = async (endpoint: string): Promise<string> => {
try {
const discoveryUrl = new URL(endpoint);
discoveryUrl.pathname = '/_matrix/push/v1/notify';
discoveryUrl.search = '';
const response = await fetch(discoveryUrl.toString(), { method: 'GET' });
if (!response.ok) return DEFAULT_UNIFIED_PUSH_GATEWAY;
const body = (await response.json()) as {
gateway?: string;
unifiedpush?: { gateway?: string };
};
if (body.gateway === 'matrix' || body.unifiedpush?.gateway === 'matrix') {
return discoveryUrl.toString();
}
} catch (err) {
console.warn('[BackgroundSync] UnifiedPush gateway discovery failed:', err);
}
return DEFAULT_UNIFIED_PUSH_GATEWAY;
};
class AndroidUnifiedPushManager {
private client: MatrixClient | undefined;
private listenerHandles: PluginListenerHandle[] = [];
private listenersReady = false;
/** Start native UnifiedPush registration and synchronize the Matrix pusher. */
async start(mx: MatrixClient): Promise<void> {
if (!isBackgroundSyncSupported()) return;
const homeserverUrl = mx.getHomeserverUrl();
const accessToken = mx.getAccessToken();
const userId = mx.getUserId();
const deviceId = mx.getDeviceId();
if (!homeserverUrl || !accessToken || !userId) {
console.warn('[BackgroundSync] Missing credentials, not starting');
return;
}
this.client = mx;
await this.ensureListeners();
await MatrixBackgroundSync.start({
homeserverUrl,
accessToken,
userId,
deviceId: deviceId ?? '',
});
console.log('[BackgroundSync] Push wake credentials configured');
} catch (err) {
console.warn('[BackgroundSync] Failed to start:', err);
await this.syncExistingEndpoint();
console.log('[BackgroundSync] UnifiedPush registration requested');
}
/** Stop native UnifiedPush integration and remove the Matrix pusher for this device. */
async stop(): Promise<void> {
if (!isBackgroundSyncSupported()) return;
const client = this.client;
if (client) {
const deviceId = client.getDeviceId();
const userId = client.getUserId();
const status = await this.safeGetStatus();
const stored = loadStoredPusherState(userId, deviceId);
const endpoint = status?.endpoint || stored?.endpoint;
const appId = stored?.appId ?? buildPusherAppId(deviceId);
if (endpoint) {
await this.removePusher(client, endpoint, appId);
}
clearStoredPusherState(userId, deviceId);
}
await MatrixBackgroundSync.stop();
await this.disposeListeners();
this.client = undefined;
console.log('[BackgroundSync] UnifiedPush stopped');
}
/** Update the Matrix pusher when a new native endpoint is published. */
private async handleNewEndpoint(event: UnifiedPushEndpointEvent): Promise<void> {
const client = this.client;
if (!client) return;
if (event.previousEndpoint) {
await this.removePusher(client, event.previousEndpoint);
}
await this.upsertPusher(client, event.endpoint);
}
/** Remove the Matrix pusher when UnifiedPush unregisters this instance. */
private async handleUnregistered(event: UnifiedPushUnregisteredEvent): Promise<void> {
const client = this.client;
if (!client) return;
const stored = loadStoredPusherState(client.getUserId(), client.getDeviceId());
const endpoint = event.previousEndpoint || stored?.endpoint;
const appId = stored?.appId ?? buildPusherAppId(client.getDeviceId());
if (endpoint) {
await this.removePusher(client, endpoint, appId);
}
clearStoredPusherState(client.getUserId(), client.getDeviceId());
}
/** Log native registration failures so the missing distributor path is visible. */
private handleRegistrationFailed(event: UnifiedPushRegistrationFailedEvent): void {
console.warn('[BackgroundSync] UnifiedPush registration failed:', event.reason);
}
/** Install plugin listeners once for the active Matrix client. */
private async ensureListeners(): Promise<void> {
if (this.listenersReady) return;
this.listenerHandles = [
await MatrixBackgroundSync.addListener('unifiedPushNewEndpoint', (event) => {
void this.handleNewEndpoint(event);
}),
await MatrixBackgroundSync.addListener('unifiedPushUnregistered', (event) => {
void this.handleUnregistered(event);
}),
await MatrixBackgroundSync.addListener('unifiedPushRegistrationFailed', (event) => {
this.handleRegistrationFailed(event);
}),
];
this.listenersReady = true;
}
/** Remove all plugin listeners when the manager stops. */
private async disposeListeners(): Promise<void> {
await Promise.all(this.listenerHandles.map((handle) => handle.remove()));
this.listenerHandles = [];
this.listenersReady = false;
}
/** Reconcile an already-persisted native endpoint after app startup. */
private async syncExistingEndpoint(): Promise<void> {
const client = this.client;
if (!client) return;
const status = await this.safeGetStatus();
if (status?.registered && status.endpoint) {
await this.upsertPusher(client, status.endpoint);
}
}
/** Create or refresh the Matrix HTTP pusher for the current device. */
private async upsertPusher(mx: MatrixClient, endpoint: string): Promise<void> {
const deviceId = mx.getDeviceId();
const userId = mx.getUserId();
const appId = buildPusherAppId(deviceId);
const gatewayUrl = await resolveUnifiedPushGateway(endpoint);
await mx.setPusher({
kind: 'http',
app_id: appId,
pushkey: endpoint,
app_display_name: 'Paarrot',
device_display_name: deviceId ?? 'Android',
lang: 'en',
data: {
url: gatewayUrl,
format: 'event_id_only',
},
append: false,
device_id: deviceId ?? undefined,
} as unknown as IPusherRequest);
saveStoredPusherState(userId, deviceId, { endpoint, appId });
}
/** Remove a previously-registered Matrix HTTP pusher for this device. */
private async removePusher(
mx: MatrixClient,
endpoint: string,
appId = buildPusherAppId(mx.getDeviceId())
): Promise<void> {
try {
await mx.setPusher({
pushkey: endpoint,
app_id: appId,
kind: null,
} as unknown as IPusherRequest);
} catch (err) {
console.warn('[BackgroundSync] Failed to remove UnifiedPush pusher:', err);
}
}
/** Read native registration state without failing the caller. */
private async safeGetStatus(): Promise<UnifiedPushStatus | undefined> {
try {
return await MatrixBackgroundSync.getStatus();
} catch (err) {
console.warn('[BackgroundSync] Failed to read UnifiedPush status:', err);
return undefined;
}
}
}
const unifiedPushManager = new AndroidUnifiedPushManager();
/** Start native UnifiedPush registration and sync the Matrix pusher. */
export const startBackgroundSync = async (mx: MatrixClient): Promise<void> => {
await unifiedPushManager.start(mx);
};
/**
@@ -74,19 +339,9 @@ export const triggerBackgroundSyncPing = async (reason?: string): Promise<void>
}
};
/**
* Stops native background sync handling and clears persisted credentials.
* Call on logout.
*/
/** Stop UnifiedPush integration and remove the Matrix pusher for this session. */
export const stopBackgroundSync = async (): Promise<void> => {
if (!isBackgroundSyncSupported()) return;
try {
await MatrixBackgroundSync.stop();
console.log('[BackgroundSync] Service stopped');
} catch (err) {
console.warn('[BackgroundSync] Failed to stop:', err);
}
await unifiedPushManager.stop();
};
/**

View File

@@ -1,6 +1,6 @@
{
"name": "paarrot",
"version": "4.11.88",
"version": "4.11.89",
"description": "Paarrot - A Matrix client based on Cinny",
"engines": {
"node": ">=18.0.0"