Skip to content

Commit 6192626

Browse files
committed
Optimize watch sync/cache CPU, memory, and battery usage
1 parent 3b1b68b commit 6192626

3 files changed

Lines changed: 95 additions & 17 deletions

File tree

app/src/main/java/com/wapp/wearmessage/presentation/MessagingViewModel.kt

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.wapp.wearmessage.sync.SyncInboundEvent
1111
import com.wapp.wearmessage.sync.WearSyncBus
1212
import com.wapp.wearmessage.sync.contract.SyncMessageStatus
1313
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.Job
1415
import kotlinx.coroutines.delay
1516
import kotlinx.coroutines.flow.MutableStateFlow
1617
import kotlinx.coroutines.flow.StateFlow
@@ -29,6 +30,7 @@ class MessagingViewModel(
2930
val uiState: StateFlow<WearMessagingUiState> = _uiState.asStateFlow()
3031
private val secureCache = SecureMessageCache(application.applicationContext)
3132
private var hasReceivedSyncPayload = false
33+
private var persistCacheJob: Job? = null
3234

3335
init {
3436
bootstrapData()
@@ -309,9 +311,14 @@ class MessagingViewModel(
309311
.distinctUntilChanged()
310312
.collect { snapshot ->
311313
if (snapshot != null) {
312-
withContext(Dispatchers.IO) {
313-
secureCache.save(snapshot)
314-
}
314+
persistCacheJob?.cancel()
315+
persistCacheJob =
316+
viewModelScope.launch {
317+
delay(CACHE_PERSIST_DEBOUNCE_MS)
318+
withContext(Dispatchers.IO) {
319+
secureCache.save(snapshot)
320+
}
321+
}
315322
}
316323
}
317324
}
@@ -363,6 +370,8 @@ class MessagingViewModel(
363370
val conversations =
364371
mergedById.values
365372
.sortedByDescending { it.lastUpdatedAtEpochMillis }
373+
.take(MAX_CONVERSATIONS_IN_MEMORY)
374+
val retainedConversationIds = conversations.map { it.id }.toSet()
366375

367376
state.copy(
368377
conversationsState =
@@ -371,6 +380,10 @@ class MessagingViewModel(
371380
} else {
372381
ConversationsUiState.Success(conversations)
373382
},
383+
messagesByConversation =
384+
state.messagesByConversation.filterKeys { conversationId ->
385+
retainedConversationIds.contains(conversationId)
386+
},
374387
syncStatus = SyncStatus.Idle,
375388
)
376389
}
@@ -399,9 +412,18 @@ class MessagingViewModel(
399412
)
400413
}
401414
}
415+
val mergedMessagesByConversation =
416+
(state.messagesByConversation + mappedByConversation)
417+
.mapValues { (_, messages) ->
418+
if (messages.size <= MAX_MESSAGES_PER_CONVERSATION_IN_MEMORY) {
419+
messages
420+
} else {
421+
messages.takeLast(MAX_MESSAGES_PER_CONVERSATION_IN_MEMORY)
422+
}
423+
}
402424

403425
state.copy(
404-
messagesByConversation = state.messagesByConversation + mappedByConversation,
426+
messagesByConversation = mergedMessagesByConversation,
405427
syncStatus = SyncStatus.Idle,
406428
)
407429
}
@@ -423,6 +445,11 @@ class MessagingViewModel(
423445
)
424446
}
425447
}
448+
449+
override fun onCleared() {
450+
persistCacheJob?.cancel()
451+
super.onCleared()
452+
}
426453
}
427454

428455
private fun ConversationsUiState.withConversationList(
@@ -490,10 +517,24 @@ private fun WearMessagingUiState.toCachedSyncSnapshotOrNull(): CachedSyncSnapsho
490517
val messages =
491518
messagesByConversation
492519
.values
520+
.map { conversationMessages ->
521+
if (conversationMessages.size <= MAX_MESSAGES_PER_CONVERSATION_CACHE) {
522+
conversationMessages
523+
} else {
524+
conversationMessages.takeLast(MAX_MESSAGES_PER_CONVERSATION_CACHE)
525+
}
526+
}
493527
.flatten()
494528
.sortedWith(
495529
compareBy<Message>({ it.conversationId }, { it.id })
496530
)
531+
.let { sortedMessages ->
532+
if (sortedMessages.size <= MAX_MESSAGES_TOTAL_CACHE) {
533+
sortedMessages
534+
} else {
535+
sortedMessages.takeLast(MAX_MESSAGES_TOTAL_CACHE)
536+
}
537+
}
497538
.map { message ->
498539
CachedMessage(
499540
id = message.id,
@@ -511,7 +552,6 @@ private fun WearMessagingUiState.toCachedSyncSnapshotOrNull(): CachedSyncSnapsho
511552
return CachedSyncSnapshot(
512553
conversations = conversations,
513554
messages = messages,
514-
savedAtEpochMillis = System.currentTimeMillis(),
515555
)
516556
}
517557

@@ -537,3 +577,9 @@ private fun Long.toClockLabel(): String {
537577
val minute = (minutes % 60).toInt().absoluteValue
538578
return "%02d:%02d".format(hour, minute)
539579
}
580+
581+
private const val CACHE_PERSIST_DEBOUNCE_MS = 500L
582+
private const val MAX_CONVERSATIONS_IN_MEMORY = 300
583+
private const val MAX_MESSAGES_PER_CONVERSATION_IN_MEMORY = 250
584+
private const val MAX_MESSAGES_PER_CONVERSATION_CACHE = 120
585+
private const val MAX_MESSAGES_TOTAL_CACHE = 1500

app/src/main/java/com/wapp/wearmessage/storage/SecureMessageCache.kt

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.util.Base64
77
import org.json.JSONArray
88
import org.json.JSONObject
99
import java.nio.ByteBuffer
10+
import java.security.MessageDigest
1011
import java.security.KeyStore
1112
import javax.crypto.Cipher
1213
import javax.crypto.KeyGenerator
@@ -19,16 +20,25 @@ class SecureMessageCache(
1920
private val prefs by lazy {
2021
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
2122
}
23+
private val secretKey by lazy { getOrCreateSecretKey() }
24+
@Volatile
25+
private var lastSavedDigest: String? = prefs.getString(PREF_KEY_CACHE_DIGEST, null)
2226

2327
fun save(snapshot: CachedSyncSnapshot) {
2428
val plainPayload = snapshot.toJson().toString().toByteArray(Charsets.UTF_8)
29+
val digest = plainPayload.sha256B64()
30+
if (digest == lastSavedDigest) {
31+
return
32+
}
2533
val encryptedPayload = encrypt(plainPayload) ?: return
2634
prefs.edit()
2735
.putString(
2836
PREF_KEY_CACHE_BLOB,
2937
Base64.encodeToString(encryptedPayload, Base64.NO_WRAP),
3038
)
39+
.putString(PREF_KEY_CACHE_DIGEST, digest)
3140
.apply()
41+
lastSavedDigest = digest
3242
}
3343

3444
fun load(): CachedSyncSnapshot? {
@@ -42,18 +52,23 @@ class SecureMessageCache(
4252
runCatching { JSONObject(String(plainPayload, Charsets.UTF_8)) }
4353
.getOrNull()
4454
?: return clearAndNull()
55+
lastSavedDigest = prefs.getString(PREF_KEY_CACHE_DIGEST, null)
4556
return CachedSyncSnapshot.fromJson(json) ?: clearAndNull()
4657
}
4758

4859
private fun clearAndNull(): CachedSyncSnapshot? {
49-
prefs.edit().remove(PREF_KEY_CACHE_BLOB).apply()
60+
prefs.edit()
61+
.remove(PREF_KEY_CACHE_BLOB)
62+
.remove(PREF_KEY_CACHE_DIGEST)
63+
.apply()
64+
lastSavedDigest = null
5065
return null
5166
}
5267

5368
private fun encrypt(plain: ByteArray): ByteArray? =
5469
runCatching {
5570
val cipher = Cipher.getInstance(TRANSFORMATION)
56-
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey())
71+
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
5772
val iv = cipher.iv ?: return null
5873
val encrypted = cipher.doFinal(plain)
5974
ByteBuffer.allocate(1 + iv.size + encrypted.size)
@@ -74,7 +89,7 @@ class SecureMessageCache(
7489
val encrypted = ByteArray(payloadBuffer.remaining())
7590
payloadBuffer.get(encrypted)
7691
val cipher = Cipher.getInstance(TRANSFORMATION)
77-
cipher.init(Cipher.DECRYPT_MODE, getOrCreateSecretKey(), javax.crypto.spec.GCMParameterSpec(TAG_SIZE_BITS, iv))
92+
cipher.init(Cipher.DECRYPT_MODE, secretKey, javax.crypto.spec.GCMParameterSpec(TAG_SIZE_BITS, iv))
7893
cipher.doFinal(encrypted)
7994
}.getOrNull()
8095

@@ -105,6 +120,7 @@ class SecureMessageCache(
105120
companion object {
106121
private const val PREFS_NAME = "wessage_secure_message_cache"
107122
private const val PREF_KEY_CACHE_BLOB = "cache_blob_v1"
123+
private const val PREF_KEY_CACHE_DIGEST = "cache_blob_digest_v1"
108124
private const val KEY_ALIAS = "wessage.message.cache.key.v1"
109125
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
110126
private const val TRANSFORMATION = "AES/GCM/NoPadding"
@@ -115,12 +131,11 @@ class SecureMessageCache(
115131
data class CachedSyncSnapshot(
116132
val conversations: List<CachedConversation>,
117133
val messages: List<CachedMessage>,
118-
val savedAtEpochMillis: Long,
119134
) {
120135
fun toJson(): JSONObject =
121136
JSONObject()
122137
.put("schemaVersion", SCHEMA_VERSION)
123-
.put("savedAtEpochMillis", savedAtEpochMillis)
138+
.put("savedAtEpochMillis", System.currentTimeMillis())
124139
.put(
125140
"conversations",
126141
JSONArray().apply {
@@ -207,7 +222,6 @@ data class CachedSyncSnapshot(
207222
return CachedSyncSnapshot(
208223
conversations = conversations,
209224
messages = messages,
210-
savedAtEpochMillis = json.optLong("savedAtEpochMillis", 0L),
211225
)
212226
}
213227
}
@@ -241,3 +255,8 @@ private fun JSONArray.toStringList(): List<String> =
241255
add(optString(index))
242256
}
243257
}
258+
259+
private fun ByteArray.sha256B64(): String {
260+
val digest = MessageDigest.getInstance("SHA-256").digest(this)
261+
return Base64.encodeToString(digest, Base64.NO_WRAP)
262+
}

app/src/main/java/com/wapp/wearmessage/sync/WearSyncTransport.kt

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ class WearSyncTransport(
3636
)
3737
var sent = false
3838
nodes.forEach { node ->
39-
Tasks.await(
40-
messageClient.sendMessage(
41-
node.id,
42-
SyncPaths.KEY_EXCHANGE_REQUEST,
43-
secureCodec.createKeyExchangePayload(),
39+
if (!secureCodec.hasPeerKey(node.id)) {
40+
Tasks.await(
41+
messageClient.sendMessage(
42+
node.id,
43+
SyncPaths.KEY_EXCHANGE_REQUEST,
44+
secureCodec.createKeyExchangePayload(),
45+
)
4446
)
45-
)
47+
}
4648
Tasks.await(
4749
messageClient.sendMessage(
4850
node.id,
@@ -65,6 +67,16 @@ class WearSyncTransport(
6567
val plainPayload = SyncJsonCodec.encodeWatchMutation(mutation)
6668
var sent = false
6769
nodes.forEach { node ->
70+
if (!secureCodec.hasPeerKey(node.id)) {
71+
Tasks.await(
72+
messageClient.sendMessage(
73+
node.id,
74+
SyncPaths.KEY_EXCHANGE_REQUEST,
75+
secureCodec.createKeyExchangePayload(),
76+
)
77+
)
78+
return@forEach
79+
}
6880
val encrypted =
6981
secureCodec.encrypt(
7082
nodeId = node.id,
@@ -79,6 +91,7 @@ class WearSyncTransport(
7991
secureCodec.createKeyExchangePayload(),
8092
)
8193
)
94+
return@forEach
8295
} else {
8396
Tasks.await(messageClient.sendMessage(node.id, SyncPaths.MUTATION, encrypted))
8497
sent = true

0 commit comments

Comments
 (0)