Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3d4dcb67e | ||
| a744e36fb1 |
@@ -4,6 +4,9 @@ import android.app.Activity
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
@@ -20,15 +23,26 @@ object UnifiedPushManager {
|
|||||||
private const val PREFS = "unifiedpush_prefs"
|
private const val PREFS = "unifiedpush_prefs"
|
||||||
private const val KEY_ENDPOINT = "endpoint"
|
private const val KEY_ENDPOINT = "endpoint"
|
||||||
private const val KEY_INSTANCE = "instance"
|
private const val KEY_INSTANCE = "instance"
|
||||||
|
private const val KEY_LAST_FAILURE = "last_failure"
|
||||||
private const val EVENT_NEW_ENDPOINT = "unifiedPushNewEndpoint"
|
private const val EVENT_NEW_ENDPOINT = "unifiedPushNewEndpoint"
|
||||||
private const val EVENT_UNREGISTERED = "unifiedPushUnregistered"
|
private const val EVENT_UNREGISTERED = "unifiedPushUnregistered"
|
||||||
private const val EVENT_REGISTRATION_FAILED = "unifiedPushRegistrationFailed"
|
private const val EVENT_REGISTRATION_FAILED = "unifiedPushRegistrationFailed"
|
||||||
private const val DEFAULT_MESSAGE = "Paarrot notifications"
|
private const val DEFAULT_MESSAGE = "Paarrot notifications"
|
||||||
|
private const val REGISTER_DELAY_MS = 750L
|
||||||
|
private const val IGNORE_UNREGISTER_MS = 2_500L
|
||||||
const val DEFAULT_MATRIX_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
const val DEFAULT_MATRIX_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
||||||
|
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var plugin: SyncServicePlugin? = null
|
private var plugin: SyncServicePlugin? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var setupGeneration = 0
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var ignoreUnregisteredUntilElapsed = 0L
|
||||||
|
|
||||||
fun setPlugin(plugin: SyncServicePlugin) {
|
fun setPlugin(plugin: SyncServicePlugin) {
|
||||||
this.plugin = plugin
|
this.plugin = plugin
|
||||||
}
|
}
|
||||||
@@ -61,19 +75,24 @@ object UnifiedPushManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears any saved distributor and shows an in-app distributor picker.
|
* Shows an in-app distributor picker, then registers after a short delay.
|
||||||
* UnifiedPush's OS deeplink picker is silent when only one distributor
|
*
|
||||||
* (e.g. ntfy) is installed, which looked like a broken Reset button.
|
* Important: we intentionally avoid calling [UnifiedPush.removeDistributor] before the
|
||||||
|
* user picks. That sent an UNREGISTER that could arrive *after* the new REGISTER and
|
||||||
|
* wipe the fresh endpoint (looked like "found ntfy but never connects").
|
||||||
*/
|
*/
|
||||||
fun requestDistributorSetup(context: Context, activity: Activity, onDone: (Boolean) -> Unit) {
|
fun requestDistributorSetup(context: Context, activity: Activity, onDone: (Boolean) -> Unit) {
|
||||||
runCatching { UnifiedPush.removeDistributor(context) }
|
val gen = ++setupGeneration
|
||||||
.onFailure { Log.w(TAG, "removeDistributor failed: ${it.message}") }
|
ignoreUnregisteredUntilElapsed = SystemClock.elapsedRealtime() + IGNORE_UNREGISTER_MS
|
||||||
clearEndpoint(context)
|
clearEndpoint(context)
|
||||||
|
clearLastFailure(context)
|
||||||
|
|
||||||
val distributors = runCatching { UnifiedPush.getDistributors(context) }
|
val distributors = runCatching { UnifiedPush.getDistributors(context) }
|
||||||
.getOrDefault(emptyList())
|
.getOrDefault(emptyList())
|
||||||
|
Log.i(TAG, "Distributor setup: found=${distributors.joinToString()}")
|
||||||
if (distributors.isEmpty()) {
|
if (distributors.isEmpty()) {
|
||||||
Log.w(TAG, "No UnifiedPush distributors installed")
|
Log.w(TAG, "No UnifiedPush distributors installed")
|
||||||
|
persistLastFailure(context, "NO_DISTRIBUTOR")
|
||||||
dispatchRegistrationFailed(FailedReason.ACTION_REQUIRED.name, INSTANCE_DEFAULT)
|
dispatchRegistrationFailed(FailedReason.ACTION_REQUIRED.name, INSTANCE_DEFAULT)
|
||||||
onDone(false)
|
onDone(false)
|
||||||
return
|
return
|
||||||
@@ -98,10 +117,7 @@ object UnifiedPushManager {
|
|||||||
finish(false)
|
finish(false)
|
||||||
return@setItems
|
return@setItems
|
||||||
}
|
}
|
||||||
runCatching { UnifiedPush.saveDistributor(context, chosen) }
|
activateDistributor(context, chosen, gen, finish)
|
||||||
.onFailure { Log.w(TAG, "saveDistributor failed: ${it.message}") }
|
|
||||||
requestRegistration(context)
|
|
||||||
finish(true)
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> finish(false) }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> finish(false) }
|
||||||
.setOnCancelListener { finish(false) }
|
.setOnCancelListener { finish(false) }
|
||||||
@@ -109,6 +125,62 @@ object UnifiedPushManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves [chosen] as the distributor and registers after a delay so any prior
|
||||||
|
* UNREGISTER from a distributor switch can settle first.
|
||||||
|
*/
|
||||||
|
private fun activateDistributor(
|
||||||
|
context: Context,
|
||||||
|
chosen: String,
|
||||||
|
gen: Int,
|
||||||
|
finish: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
ignoreUnregisteredUntilElapsed = SystemClock.elapsedRealtime() + IGNORE_UNREGISTER_MS
|
||||||
|
val previous = runCatching { UnifiedPush.getSavedDistributor(context) }.getOrNull().orEmpty()
|
||||||
|
Log.i(TAG, "Activating distributor=$chosen previous=$previous")
|
||||||
|
|
||||||
|
if (previous.isNotBlank() && previous != chosen) {
|
||||||
|
runCatching { UnifiedPush.removeDistributor(context) }
|
||||||
|
.onFailure { Log.w(TAG, "removeDistributor failed: ${it.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
if (gen != setupGeneration) {
|
||||||
|
Log.w(TAG, "Skipping stale distributor activation gen=$gen current=$setupGeneration")
|
||||||
|
finish(false)
|
||||||
|
return@postDelayed
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching { UnifiedPush.saveDistributor(context, chosen) }
|
||||||
|
.onFailure {
|
||||||
|
Log.w(TAG, "saveDistributor failed: ${it.message}")
|
||||||
|
persistLastFailure(context, "SAVE_FAILED")
|
||||||
|
finish(false)
|
||||||
|
return@postDelayed
|
||||||
|
}
|
||||||
|
|
||||||
|
val saved = runCatching { UnifiedPush.getSavedDistributor(context) }.getOrNull()
|
||||||
|
if (saved != chosen) {
|
||||||
|
Log.e(TAG, "Distributor not saved after saveDistributor (got=$saved)")
|
||||||
|
persistLastFailure(context, "SAVE_NOT_PERSISTED")
|
||||||
|
finish(false)
|
||||||
|
return@postDelayed
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRegistration(context)
|
||||||
|
// Report true once register was requested; JS polls for the endpoint.
|
||||||
|
finish(true)
|
||||||
|
|
||||||
|
// One retry if the distributor is slow to ack.
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
if (gen != setupGeneration) return@postDelayed
|
||||||
|
if (!getEndpoint(context).isNullOrBlank()) return@postDelayed
|
||||||
|
Log.w(TAG, "No endpoint yet after ${REGISTER_DELAY_MS}ms — retrying register")
|
||||||
|
requestRegistration(context)
|
||||||
|
}, REGISTER_DELAY_MS)
|
||||||
|
}, if (previous.isNotBlank() && previous != chosen) REGISTER_DELAY_MS else 150L)
|
||||||
|
}
|
||||||
|
|
||||||
/** Human-readable label for a distributor package (falls back to package name). */
|
/** Human-readable label for a distributor package (falls back to package name). */
|
||||||
private fun distributorLabel(context: Context, packageName: String): String {
|
private fun distributorLabel(context: Context, packageName: String): String {
|
||||||
return try {
|
return try {
|
||||||
@@ -173,6 +245,7 @@ object UnifiedPushManager {
|
|||||||
status.put("instance", getInstance(context) ?: INSTANCE_DEFAULT)
|
status.put("instance", getInstance(context) ?: INSTANCE_DEFAULT)
|
||||||
status.put("registered", !getEndpoint(context).isNullOrBlank())
|
status.put("registered", !getEndpoint(context).isNullOrBlank())
|
||||||
status.put("distributor", runCatching { UnifiedPush.getSavedDistributor(context) }.getOrDefault(""))
|
status.put("distributor", runCatching { UnifiedPush.getSavedDistributor(context) }.getOrDefault(""))
|
||||||
|
status.put("lastFailure", getLastFailure(context) ?: "")
|
||||||
val distributors = JSArray()
|
val distributors = JSArray()
|
||||||
runCatching { UnifiedPush.getDistributors(context) }
|
runCatching { UnifiedPush.getDistributors(context) }
|
||||||
.getOrDefault(emptyList())
|
.getOrDefault(emptyList())
|
||||||
@@ -182,6 +255,8 @@ object UnifiedPushManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
||||||
|
Log.i(TAG, "onNewEndpoint instance=$instance endpoint=$endpoint")
|
||||||
|
clearLastFailure(context)
|
||||||
val previousEndpoint = getEndpoint(context)
|
val previousEndpoint = getEndpoint(context)
|
||||||
persistEndpoint(context, endpoint, instance)
|
persistEndpoint(context, endpoint, instance)
|
||||||
val payload = JSObject().apply {
|
val payload = JSObject().apply {
|
||||||
@@ -192,11 +267,18 @@ object UnifiedPushManager {
|
|||||||
dispatch(EVENT_NEW_ENDPOINT, payload)
|
dispatch(EVENT_NEW_ENDPOINT, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRegistrationFailed(reason: String, instance: String) {
|
fun onRegistrationFailed(context: Context, reason: String, instance: String) {
|
||||||
|
Log.w(TAG, "onRegistrationFailed reason=$reason instance=$instance")
|
||||||
|
persistLastFailure(context, reason)
|
||||||
dispatchRegistrationFailed(reason, instance)
|
dispatchRegistrationFailed(reason, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUnregistered(context: Context, instance: String) {
|
fun onUnregistered(context: Context, instance: String) {
|
||||||
|
if (SystemClock.elapsedRealtime() < ignoreUnregisteredUntilElapsed) {
|
||||||
|
Log.i(TAG, "Ignoring unregistered during setup window instance=$instance")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i(TAG, "onUnregistered instance=$instance")
|
||||||
val previousEndpoint = getEndpoint(context)
|
val previousEndpoint = getEndpoint(context)
|
||||||
clearEndpoint(context)
|
clearEndpoint(context)
|
||||||
dispatchUnregistered(previousEndpoint, instance)
|
dispatchUnregistered(previousEndpoint, instance)
|
||||||
@@ -204,9 +286,11 @@ object UnifiedPushManager {
|
|||||||
|
|
||||||
private fun requestRegistration(context: Context) {
|
private fun requestRegistration(context: Context) {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
Log.i(TAG, "Calling UnifiedPush.register instance=$INSTANCE_DEFAULT")
|
||||||
UnifiedPush.register(context, INSTANCE_DEFAULT, DEFAULT_MESSAGE, null)
|
UnifiedPush.register(context, INSTANCE_DEFAULT, DEFAULT_MESSAGE, null)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.w(TAG, "UnifiedPush register failed: ${it.message}")
|
Log.w(TAG, "UnifiedPush register failed: ${it.message}")
|
||||||
|
persistLastFailure(context, FailedReason.INTERNAL_ERROR.name)
|
||||||
dispatchRegistrationFailed(FailedReason.INTERNAL_ERROR.name, INSTANCE_DEFAULT)
|
dispatchRegistrationFailed(FailedReason.INTERNAL_ERROR.name, INSTANCE_DEFAULT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +329,21 @@ object UnifiedPushManager {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun persistLastFailure(context: Context, reason: String) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
|
.putString(KEY_LAST_FAILURE, reason)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearLastFailure(context: Context) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
|
.remove(KEY_LAST_FAILURE)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastFailure(context: Context): String? =
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_LAST_FAILURE, null)
|
||||||
|
|
||||||
private fun getEndpoint(context: Context): String? =
|
private fun getEndpoint(context: Context): String? =
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_ENDPOINT, null)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_ENDPOINT, null)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class UnifiedPushService : PushService() {
|
|||||||
|
|
||||||
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
|
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
|
||||||
Log.w(TAG, "UnifiedPush registration failed: $reason")
|
Log.w(TAG, "UnifiedPush registration failed: $reason")
|
||||||
UnifiedPushManager.onRegistrationFailed(reason.name, instance)
|
UnifiedPushManager.onRegistrationFailed(applicationContext, reason.name, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnregistered(instance: String) {
|
override fun onUnregistered(instance: String) {
|
||||||
|
|||||||
2
cinny
2
cinny
Submodule cinny updated: e460dbd34e...0fb3da20b9
@@ -171,6 +171,7 @@ type PushStatus = {
|
|||||||
distributor: string;
|
distributor: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
distributors: string[];
|
distributors: string[];
|
||||||
|
lastFailure: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Android-only section showing UnifiedPush registration status and controls. */
|
/** Android-only section showing UnifiedPush registration status and controls. */
|
||||||
@@ -194,6 +195,7 @@ function AndroidPushNotifications() {
|
|||||||
distributor: s.distributor || '',
|
distributor: s.distributor || '',
|
||||||
endpoint: s.endpoint || '',
|
endpoint: s.endpoint || '',
|
||||||
distributors,
|
distributors,
|
||||||
|
lastFailure: s.lastFailure || '',
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
@@ -211,7 +213,7 @@ function AndroidPushNotifications() {
|
|||||||
await refresh();
|
await refresh();
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setLastError(
|
setLastError(
|
||||||
'Could not register a push distributor. Install ntfy (with UnifiedPush enabled), then try Reset again.'
|
'Selected distributor but did not get a push endpoint. In ntfy: enable UnifiedPush, allow unrestricted battery, then try Reset again.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [refresh])
|
}, [refresh])
|
||||||
@@ -244,6 +246,13 @@ function AndroidPushNotifications() {
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (status.lastFailure) {
|
||||||
|
return (
|
||||||
|
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
|
||||||
|
{`Registration failed (${status.lastFailure}). Tap Reset and pick ntfy again.`}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (status.distributors.length === 0) {
|
if (status.distributors.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
|
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type UnifiedPushStatus = {
|
|||||||
registered: boolean;
|
registered: boolean;
|
||||||
distributor: string;
|
distributor: string;
|
||||||
distributors: string[] | string;
|
distributors: string[] | string;
|
||||||
|
lastFailure?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClearRoomNotificationsResult = {
|
type ClearRoomNotificationsResult = {
|
||||||
@@ -551,16 +552,20 @@ export const requestResetPushRegistration = async (): Promise<{ success: boolean
|
|||||||
if (!result?.success) return { success: false };
|
if (!result?.success) return { success: false };
|
||||||
|
|
||||||
// Endpoint arrives asynchronously from the distributor after register().
|
// Endpoint arrives asynchronously from the distributor after register().
|
||||||
for (let i = 0; i < 20; i += 1) {
|
for (let i = 0; i < 40; i += 1) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
const status = await getBackgroundSyncStatus();
|
const status = await getBackgroundSyncStatus();
|
||||||
if (status?.registered && status.endpoint) {
|
if (status?.registered && status.endpoint) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
if (status?.lastFailure) {
|
||||||
|
console.warn('[BackgroundSync] Registration failed while waiting:', status.lastFailure);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picker succeeded but endpoint not yet ack'd — still treat as success.
|
console.warn('[BackgroundSync] Distributor selected but no endpoint received in time');
|
||||||
return { success: true };
|
return { success: false };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[BackgroundSync] requestDistributorSetup failed:', err);
|
console.error('[BackgroundSync] requestDistributorSetup failed:', err);
|
||||||
return { success: false };
|
return { success: false };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paarrot",
|
"name": "paarrot",
|
||||||
"version": "4.11.142",
|
"version": "4.11.143",
|
||||||
"description": "Paarrot - A Matrix client based on Cinny",
|
"description": "Paarrot - A Matrix client based on Cinny",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user