Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ac92a0c4 | ||
| cb44bc92e4 | |||
| bc07974b4b | |||
|
|
abe44f6ad1 | ||
| c17e1c74ee | |||
|
|
255bfe2262 | ||
| 8eea325e21 | |||
| 3afb2ee80d | |||
|
|
245188c1a1 | ||
| 1d2057d75a | |||
| d8343d089b | |||
|
|
5cc087330c | ||
| 4163f22f71 | |||
|
|
ed0761bd20 | ||
| 6fdaa8dfd3 | |||
|
|
88d0a3f887 | ||
| 240d39593e | |||
| f06d11711b | |||
|
|
75e1fe6812 | ||
| f8c1e0641d | |||
| 6795e692d6 | |||
|
|
1c42974fe8 | ||
| 2cde79e87d |
@@ -1,7 +1,7 @@
|
|||||||
# Paarrot
|
# Paarrot
|
||||||
|
|
||||||
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Installers for Windows and Linux can be downloaded from [releases](http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases).
|
Installers for Windows and Linux can be downloaded from [releases](http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases).
|
||||||
|
|||||||
8
android/.gitignore
vendored
8
android/.gitignore
vendored
@@ -52,10 +52,10 @@ captures/
|
|||||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
.idea/navEditor.xml
|
.idea/navEditor.xml
|
||||||
|
|
||||||
# Keystore files
|
# Keystore files - NEVER commit these!
|
||||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
*.jks
|
||||||
#*.jks
|
*.keystore
|
||||||
#*.keystore
|
keystore.properties
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|||||||
61
android/SIGNING_SETUP.md
Normal file
61
android/SIGNING_SETUP.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Android App Signing Setup
|
||||||
|
|
||||||
|
## Why This Matters
|
||||||
|
Android requires apps to be signed with the same key for updates to work. Without proper signing, users cannot update your app - they must uninstall and reinstall, losing all data.
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Create a Keystore (if you don't have one)
|
||||||
|
|
||||||
|
Run this command in the `android/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore paarrot-release.keystore -alias paarrot -keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be asked for:
|
||||||
|
- Keystore password (remember this!)
|
||||||
|
- Key password (remember this!)
|
||||||
|
- Your name, organization, etc.
|
||||||
|
|
||||||
|
**IMPORTANT**: Back up this keystore file and remember the passwords! If you lose either, you cannot update your app.
|
||||||
|
|
||||||
|
### 2. Create keystore.properties
|
||||||
|
|
||||||
|
Copy the example file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp keystore.properties.example keystore.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
Then edit `keystore.properties` and fill in:
|
||||||
|
- `storeFile`: path to your keystore (e.g., `paarrot-release.keystore`)
|
||||||
|
- `storePassword`: the keystore password you set
|
||||||
|
- `keyAlias`: the alias (e.g., `paarrot`)
|
||||||
|
- `keyPassword`: the key password you set
|
||||||
|
|
||||||
|
### 3. Verify
|
||||||
|
|
||||||
|
The file `keystore.properties` should be gitignored (check `.gitignore`).
|
||||||
|
|
||||||
|
### 4. Build
|
||||||
|
|
||||||
|
Now your release builds will be properly signed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
The signed APK will be in: `app/build/outputs/apk/release/`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Updates not installing"
|
||||||
|
- You're using a different keystore than the original APK
|
||||||
|
- Solution: Use the same keystore that was used for the first release
|
||||||
|
|
||||||
|
### "keystore.properties not found"
|
||||||
|
- Normal for debug builds
|
||||||
|
- Only needed for release builds
|
||||||
|
- Create the file following step 2 above
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
// Load keystore properties
|
||||||
|
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.paarrot.app"
|
namespace = "com.paarrot.app"
|
||||||
compileSdk = rootProject.ext.compileSdkVersion
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.paarrot.app"
|
applicationId "com.paarrot.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
@@ -21,6 +40,9 @@ android {
|
|||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
|
|||||||
@@ -47,6 +47,15 @@
|
|||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</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>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|||||||
@@ -3,34 +3,15 @@ package com.paarrot.app
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts [MatrixSyncService] after the device boots, using credentials
|
* Triggers a one-shot sync fetch after boot if credentials exist.
|
||||||
* previously persisted by [SyncServicePlugin].
|
|
||||||
*/
|
*/
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
|
MatrixSyncService.requestSyncFetch(context, "boot_completed")
|
||||||
val homeserver = prefs.getString(MatrixSyncService.EXTRA_HOMESERVER, null) ?: return
|
|
||||||
val token = prefs.getString(MatrixSyncService.EXTRA_TOKEN, null) ?: return
|
|
||||||
val userId = prefs.getString(MatrixSyncService.EXTRA_USER_ID, null) ?: ""
|
|
||||||
val deviceId = prefs.getString(MatrixSyncService.EXTRA_DEVICE_ID, null) ?: ""
|
|
||||||
|
|
||||||
val serviceIntent = Intent(context, MatrixSyncService::class.java).apply {
|
|
||||||
putExtra(MatrixSyncService.EXTRA_HOMESERVER, homeserver)
|
|
||||||
putExtra(MatrixSyncService.EXTRA_TOKEN, token)
|
|
||||||
putExtra(MatrixSyncService.EXTRA_USER_ID, userId)
|
|
||||||
putExtra(MatrixSyncService.EXTRA_DEVICE_ID, deviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(serviceIntent)
|
|
||||||
} else {
|
|
||||||
context.startService(serviceIntent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@@ -25,12 +23,10 @@ import java.net.URL
|
|||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent ForegroundService that maintains a Matrix /sync long-poll loop,
|
* ForegroundService that performs a short, one-shot Matrix /sync fetch.
|
||||||
* showing native notifications for new messages when the main app process is alive
|
|
||||||
* but the UI is not in the foreground, and when the app has been swiped away.
|
|
||||||
*
|
*
|
||||||
* Start via [SyncServicePlugin]. Credentials are persisted in SharedPreferences
|
* This service is wake-triggered by push pings (UnifiedPush/FCM bridge) and
|
||||||
* so [BootReceiver] can restart it after a device reboot.
|
* then fetches message content from Matrix before posting local notifications.
|
||||||
*/
|
*/
|
||||||
class MatrixSyncService : Service() {
|
class MatrixSyncService : Service() {
|
||||||
|
|
||||||
@@ -57,13 +53,20 @@ class MatrixSyncService : Service() {
|
|||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val triggerReason = intent?.getStringExtra(EXTRA_TRIGGER_REASON) ?: "unknown"
|
||||||
startForeground(NOTIF_ID_STATUS, buildStatusNotification())
|
startForeground(NOTIF_ID_STATUS, buildStatusNotification())
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
runSyncLoop(homeserver, token, userId)
|
try {
|
||||||
|
runSingleSyncFetch(homeserver, token, userId)
|
||||||
|
Log.d(TAG, "One-shot sync completed (reason=$triggerReason)")
|
||||||
|
} finally {
|
||||||
|
stopForegroundCompat()
|
||||||
|
stopSelf(startId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return START_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -71,44 +74,44 @@ class MatrixSyncService : Service() {
|
|||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runSyncLoop(homeserver: String, token: String, userId: String) {
|
private suspend fun runSingleSyncFetch(homeserver: String, token: String, userId: String) {
|
||||||
val prefs = applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val prefs = applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
var since = prefs.getString(KEY_SINCE, null)
|
val since = prefs.getString(KEY_SINCE, null)
|
||||||
var isFirstSync = since == null
|
val isFirstSync = since == null
|
||||||
|
|
||||||
while (serviceScope.isActive) {
|
try {
|
||||||
try {
|
val url = buildSyncUrl(homeserver.trimEnd('/'), since)
|
||||||
val url = buildSyncUrl(homeserver.trimEnd('/'), since)
|
val (responseCode, body) = doHttpGet(url, token)
|
||||||
val (responseCode, body) = doHttpGet(url, token)
|
|
||||||
|
|
||||||
when (responseCode) {
|
when (responseCode) {
|
||||||
200 -> {
|
200 -> {
|
||||||
val json = JSONObject(body ?: "{}")
|
val json = JSONObject(body ?: "{}")
|
||||||
val nextBatch = json.optString("next_batch").takeIf { it.isNotBlank() }
|
val nextBatch = json.optString("next_batch").takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
if (nextBatch != null) {
|
if (nextBatch != null) {
|
||||||
prefs.edit().putString(KEY_SINCE, nextBatch).apply()
|
prefs.edit().putString(KEY_SINCE, nextBatch).apply()
|
||||||
|
|
||||||
if (!isFirstSync && !appInForeground) {
|
if (!isFirstSync && !appInForeground) {
|
||||||
processRoomEvents(json, userId)
|
processRoomEvents(json, userId)
|
||||||
}
|
|
||||||
isFirstSync = false
|
|
||||||
since = nextBatch
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
401, 403 -> {
|
|
||||||
Log.w(TAG, "Auth error $responseCode — stopping sync")
|
|
||||||
stopSelf()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
else -> delay(BACKOFF_ERRS_MS)
|
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
401, 403 -> {
|
||||||
throw e
|
Log.w(TAG, "Auth error $responseCode — clearing credentials")
|
||||||
} catch (e: Exception) {
|
applicationContext
|
||||||
Log.w(TAG, "Sync error: ${e.message}")
|
.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
|
||||||
delay(BACKOFF_ERRS_MS)
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Sync fetch failed with HTTP $responseCode")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Sync error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +119,7 @@ class MatrixSyncService : Service() {
|
|||||||
val filter = """{"room":{"timeline":{"limit":10,"types":["m.room.message"]},"state":{"types":[]},"account_data":{"types":[]},"ephemeral":{"types":[]}},"account_data":{"types":[]},"presence":{"types":[]}}"""
|
val filter = """{"room":{"timeline":{"limit":10,"types":["m.room.message"]},"state":{"types":[]},"account_data":{"types":[]},"ephemeral":{"types":[]}},"account_data":{"types":[]},"presence":{"types":[]}}"""
|
||||||
val encodedFilter = URLEncoder.encode(filter, "UTF-8")
|
val encodedFilter = URLEncoder.encode(filter, "UTF-8")
|
||||||
val sinceParam = if (since != null) "&since=${URLEncoder.encode(since, "UTF-8")}" else ""
|
val sinceParam = if (since != null) "&since=${URLEncoder.encode(since, "UTF-8")}" else ""
|
||||||
return "$base/_matrix/client/v3/sync?timeout=30000&filter=$encodedFilter$sinceParam"
|
return "$base/_matrix/client/v3/sync?timeout=12000&filter=$encodedFilter$sinceParam"
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doHttpGet(urlString: String, token: String): Pair<Int, String?> =
|
private suspend fun doHttpGet(urlString: String, token: String): Pair<Int, String?> =
|
||||||
@@ -203,12 +206,21 @@ class MatrixSyncService : Service() {
|
|||||||
return NotificationCompat.Builder(this, CHANNEL_STATUS)
|
return NotificationCompat.Builder(this, CHANNEL_STATUS)
|
||||||
.setSmallIcon(R.drawable.ic_stat_paarrot)
|
.setSmallIcon(R.drawable.ic_stat_paarrot)
|
||||||
.setContentTitle("Paarrot")
|
.setContentTitle("Paarrot")
|
||||||
.setContentText("Connected")
|
.setContentText("Checking for new messages")
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopForegroundCompat() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureMessageChannel(nm: NotificationManager) {
|
private fun ensureMessageChannel(nm: NotificationManager) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
@@ -225,15 +237,52 @@ class MatrixSyncService : Service() {
|
|||||||
const val EXTRA_TOKEN = "access_token"
|
const val EXTRA_TOKEN = "access_token"
|
||||||
const val EXTRA_USER_ID = "user_id"
|
const val EXTRA_USER_ID = "user_id"
|
||||||
const val EXTRA_DEVICE_ID = "device_id"
|
const val EXTRA_DEVICE_ID = "device_id"
|
||||||
|
const val EXTRA_TRIGGER_REASON = "trigger_reason"
|
||||||
const val PREFS = "matrix_sync_prefs"
|
const val PREFS = "matrix_sync_prefs"
|
||||||
const val KEY_SINCE = "since_token"
|
const val KEY_SINCE = "since_token"
|
||||||
|
private const val KEY_LAST_WAKE_MS = "last_wake_ms"
|
||||||
private const val NOTIF_ID_STATUS = 1001
|
private const val NOTIF_ID_STATUS = 1001
|
||||||
private const val CHANNEL_STATUS = "sync_status"
|
private const val CHANNEL_STATUS = "sync_status"
|
||||||
private const val CHANNEL_MESSAGES = "messages"
|
private const val CHANNEL_MESSAGES = "messages"
|
||||||
private const val BACKOFF_ERRS_MS = 10_000L
|
private const val MIN_WAKE_INTERVAL_MS = 7_500L
|
||||||
|
const val ACTION_PUSH_PING = "com.paarrot.app.ACTION_PUSH_PING"
|
||||||
|
|
||||||
/** Set by [SyncServicePlugin] — true when the Capacitor WebView UI is visible. */
|
/** Set by [SyncServicePlugin] — true when the Capacitor WebView UI is visible. */
|
||||||
@Volatile
|
@Volatile
|
||||||
var appInForeground = false
|
var appInForeground = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a one-shot sync fetch if credentials are available and the call is not rate-limited.
|
||||||
|
*/
|
||||||
|
fun requestSyncFetch(context: Context, reason: String) {
|
||||||
|
val credsPrefs = context.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
|
||||||
|
val homeserver = credsPrefs.getString(EXTRA_HOMESERVER, null) ?: return
|
||||||
|
val token = credsPrefs.getString(EXTRA_TOKEN, null) ?: return
|
||||||
|
val userId = credsPrefs.getString(EXTRA_USER_ID, null) ?: ""
|
||||||
|
val deviceId = credsPrefs.getString(EXTRA_DEVICE_ID, null) ?: ""
|
||||||
|
|
||||||
|
val syncPrefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val lastWake = syncPrefs.getLong(KEY_LAST_WAKE_MS, 0L)
|
||||||
|
if (now - lastWake < MIN_WAKE_INTERVAL_MS) {
|
||||||
|
Log.d(TAG, "Skipping wake: rate-limited ($reason)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncPrefs.edit().putLong(KEY_LAST_WAKE_MS, now).apply()
|
||||||
|
|
||||||
|
val intent = Intent(context, MatrixSyncService::class.java).apply {
|
||||||
|
putExtra(EXTRA_HOMESERVER, homeserver)
|
||||||
|
putExtra(EXTRA_TOKEN, token)
|
||||||
|
putExtra(EXTRA_USER_ID, userId)
|
||||||
|
putExtra(EXTRA_DEVICE_ID, deviceId)
|
||||||
|
putExtra(EXTRA_TRIGGER_REASON, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.paarrot.app
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
@@ -13,19 +12,16 @@ import com.getcapacitor.annotation.CapacitorPlugin
|
|||||||
* Capacitor plugin that controls [MatrixSyncService] from JS.
|
* Capacitor plugin that controls [MatrixSyncService] from JS.
|
||||||
*
|
*
|
||||||
* JS API:
|
* JS API:
|
||||||
* - `start({ homeserverUrl, accessToken, userId, deviceId })` — start/update sync service
|
* - `start({ homeserverUrl, accessToken, userId, deviceId })` — persist/update sync credentials
|
||||||
* - `stop()` — stop sync service and clear persisted credentials
|
* - `triggerPing({ reason })` — force a one-shot fetch (for testing/manual wake)
|
||||||
|
* - `stop()` — clear persisted credentials and stop any in-flight fetch
|
||||||
* - `setAppForeground({ foreground })` — tell the service whether the app UI is visible
|
* - `setAppForeground({ foreground })` — tell the service whether the app UI is visible
|
||||||
* - `getStatus()` — returns `{ running: boolean }`
|
* - `getStatus()` — returns `{ running: boolean }`
|
||||||
*/
|
*/
|
||||||
@CapacitorPlugin(name = "MatrixBackgroundSync")
|
@CapacitorPlugin(name = "MatrixBackgroundSync")
|
||||||
class SyncServicePlugin : Plugin() {
|
class SyncServicePlugin : Plugin() {
|
||||||
|
|
||||||
/**
|
/** Persists Matrix credentials used later by push-triggered sync fetches. */
|
||||||
* Starts the [MatrixSyncService] with the provided Matrix credentials.
|
|
||||||
* Persists credentials in SharedPreferences so [BootReceiver] can restart
|
|
||||||
* the service after a device reboot.
|
|
||||||
*/
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun start(call: PluginCall) {
|
fun start(call: PluginCall) {
|
||||||
val homeserver = call.getString("homeserverUrl")
|
val homeserver = call.getString("homeserverUrl")
|
||||||
@@ -42,19 +38,14 @@ class SyncServicePlugin : Plugin() {
|
|||||||
.putString(MatrixSyncService.EXTRA_DEVICE_ID, deviceId)
|
.putString(MatrixSyncService.EXTRA_DEVICE_ID, deviceId)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
val intent = Intent(context, MatrixSyncService::class.java).apply {
|
call.resolve()
|
||||||
putExtra(MatrixSyncService.EXTRA_HOMESERVER, homeserver)
|
}
|
||||||
putExtra(MatrixSyncService.EXTRA_TOKEN, token)
|
|
||||||
putExtra(MatrixSyncService.EXTRA_USER_ID, userId)
|
|
||||||
putExtra(MatrixSyncService.EXTRA_DEVICE_ID, deviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Manually triggers a one-shot sync fetch (mainly for diagnostics/testing). */
|
||||||
|
@PluginMethod
|
||||||
|
fun triggerPing(call: PluginCall) {
|
||||||
|
val reason = call.getString("reason") ?: "manual_plugin_ping"
|
||||||
|
MatrixSyncService.requestSyncFetch(context, reason)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
android/keystore.properties.example
Normal file
14
android/keystore.properties.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Copy this file to keystore.properties and fill in your actual values
|
||||||
|
# NEVER commit keystore.properties to git!
|
||||||
|
|
||||||
|
# Path to your keystore file (relative to android/ directory)
|
||||||
|
storeFile=paarrot-release.keystore
|
||||||
|
|
||||||
|
# Keystore password
|
||||||
|
storePassword=your_keystore_password_here
|
||||||
|
|
||||||
|
# Key alias
|
||||||
|
keyAlias=paarrot
|
||||||
|
|
||||||
|
# Key password
|
||||||
|
keyPassword=your_key_password_here
|
||||||
2
cinny
2
cinny
Submodule cinny updated: 9eb5e4fa32...7b69ed6df8
@@ -419,9 +419,9 @@ function MessageNotifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the native Android background sync service on login, keeps it
|
* Configures native Android push-ping background sync on login, keeps it
|
||||||
* informed of foreground state so it doesn't double-fire notifications,
|
* informed of foreground state so it doesn't double-fire notifications,
|
||||||
* and stops it cleanly on unmount (logout).
|
* and clears it cleanly on unmount (logout).
|
||||||
* Only active on Android Capacitor builds.
|
* Only active on Android Capacitor builds.
|
||||||
*/
|
*/
|
||||||
function BackgroundSyncSetup() {
|
function BackgroundSyncSetup() {
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { Capacitor, registerPlugin } from '@capacitor/core';
|
|||||||
import type { MatrixClient } from 'matrix-js-sdk';
|
import type { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
interface MatrixBackgroundSyncPlugin {
|
interface MatrixBackgroundSyncPlugin {
|
||||||
/** Start the background sync service with the given Matrix credentials. */
|
/** Persist credentials used by push-triggered one-shot sync fetches. */
|
||||||
start(options: {
|
start(options: {
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
/** Stop the background sync service and clear persisted credentials. */
|
/** 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(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Notify the service whether the app UI is visible.
|
* Notify the service whether the app UI is visible.
|
||||||
@@ -28,9 +30,8 @@ export const isBackgroundSyncSupported = (): boolean =>
|
|||||||
Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android';
|
Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the native Matrix background sync service.
|
* Persists Matrix credentials for Android push-ping wake handling.
|
||||||
* Reads credentials directly from the active MatrixClient.
|
* Safe to call on every login.
|
||||||
* Safe to call on every login — the service is idempotent.
|
|
||||||
* @param mx Authenticated Matrix client
|
* @param mx Authenticated Matrix client
|
||||||
*/
|
*/
|
||||||
export const startBackgroundSync = async (mx: MatrixClient): Promise<void> => {
|
export const startBackgroundSync = async (mx: MatrixClient): Promise<void> => {
|
||||||
@@ -53,15 +54,29 @@ export const startBackgroundSync = async (mx: MatrixClient): Promise<void> => {
|
|||||||
userId,
|
userId,
|
||||||
deviceId: deviceId ?? '',
|
deviceId: deviceId ?? '',
|
||||||
});
|
});
|
||||||
console.log('[BackgroundSync] Service started');
|
console.log('[BackgroundSync] Push wake credentials configured');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[BackgroundSync] Failed to start:', err);
|
console.warn('[BackgroundSync] Failed to start:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the native Matrix background sync service.
|
* Manually triggers a one-shot native fetch.
|
||||||
* Call on logout so the service stops and credentials are wiped.
|
* Useful for diagnostics and bridge testing.
|
||||||
|
*/
|
||||||
|
export const triggerBackgroundSyncPing = async (reason?: string): Promise<void> => {
|
||||||
|
if (!isBackgroundSyncSupported()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MatrixBackgroundSync.triggerPing({ reason });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[BackgroundSync] triggerPing failed:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops native background sync handling and clears persisted credentials.
|
||||||
|
* Call on logout.
|
||||||
*/
|
*/
|
||||||
export const stopBackgroundSync = async (): Promise<void> => {
|
export const stopBackgroundSync = async (): Promise<void> => {
|
||||||
if (!isBackgroundSyncSupported()) return;
|
if (!isBackgroundSyncSupported()) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paarrot",
|
"name": "paarrot",
|
||||||
"version": "4.11.79",
|
"version": "4.11.88",
|
||||||
"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