chore(gitignore): ignore .code-workspace; cleanup Firebase data source dead code

This commit is contained in:
null 2026-07-06 21:41:54 -05:00
parent 39fbd943ff
commit f160f141d6
3 changed files with 32 additions and 83 deletions

1
.gitignore vendored
View File

@ -90,3 +90,4 @@ Future.md
docs/strategy/positioning-vs-paired.md docs/strategy/positioning-vs-paired.md
ClaudeQAPlan.md ClaudeQAPlan.md
ClaudeReport.md ClaudeReport.md
relationship-app.code-workspace

View File

@ -7,12 +7,10 @@ import android.net.Uri
import com.google.firebase.storage.FirebaseStorage import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageMetadata import com.google.firebase.storage.StorageMetadata
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.tasks.await
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Singleton @Singleton
class FirebaseStorageDataSource @Inject constructor( class FirebaseStorageDataSource @Inject constructor(
@ -31,42 +29,27 @@ class FirebaseStorageDataSource @Inject constructor(
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
} }
suspend fun uploadProfilePhoto(uid: String, uri: Uri): String = suspend fun uploadProfilePhoto(uid: String, uri: Uri): String {
suspendCancellableCoroutine { cont -> val ref = storage.reference.child("users/$uid/profile.jpg")
val ref = storage.reference.child("users/$uid/profile.jpg") val mimeType = context.contentResolver.getType(uri)
val mimeType = context.contentResolver.getType(uri) ?.takeIf { it.startsWith("image/") }
?.takeIf { it.startsWith("image/") } ?: "image/jpeg"
?: "image/jpeg" val metadata = StorageMetadata.Builder().setContentType(mimeType).build()
val metadata = StorageMetadata.Builder() return ref.putFile(uri, metadata).continueWithTask { ref.downloadUrl }.await().toString()
.setContentType(mimeType) }
.build()
ref.putFile(uri, metadata)
.continueWithTask { ref.downloadUrl }
.addOnSuccessListener { cont.resume(it.toString()) }
.addOnFailureListener { cont.resumeWithException(it) }
}
/** /**
* Uploads already-encrypted chat-media bytes under the author's own storage path (mirrors the * Uploads already-encrypted chat-media bytes under the author's own storage path (mirrors the
* profile-photo ownership model) and returns the tokenized download URL. The bytes are * profile-photo ownership model) and returns the tokenized download URL. The bytes are
* ciphertext, so Storage never holds anything readable. * ciphertext, so Storage never holds anything readable.
*/ */
suspend fun uploadEncryptedMedia(uid: String, encryptedBytes: ByteArray): String = suspend fun uploadEncryptedMedia(uid: String, encryptedBytes: ByteArray): String {
suspendCancellableCoroutine { cont -> // Fail fast when clearly offline so the chat shows a retry instead of a stuck spinner.
// Fail fast when clearly offline so the chat shows a retry instead of a stuck spinner. if (!isOnline()) throw IOException("No internet connection")
if (!isOnline()) { val ref = storage.reference.child("users/$uid/chat_media/${java.util.UUID.randomUUID()}")
cont.resumeWithException(IOException("No internet connection")) val metadata = StorageMetadata.Builder().setContentType("application/octet-stream").build()
return@suspendCancellableCoroutine return ref.putBytes(encryptedBytes, metadata).continueWithTask { ref.downloadUrl }.await().toString()
} }
val ref = storage.reference.child("users/$uid/chat_media/${java.util.UUID.randomUUID()}")
val metadata = StorageMetadata.Builder()
.setContentType("application/octet-stream")
.build()
ref.putBytes(encryptedBytes, metadata)
.continueWithTask { ref.downloadUrl }
.addOnSuccessListener { cont.resume(it.toString()) }
.addOnFailureListener { cont.resumeWithException(it) }
}
/** /**
* Uploads an already-encrypted conversation-backup snapshot under the uploader's OWN path and * Uploads an already-encrypted conversation-backup snapshot under the uploader's OWN path and
@ -74,30 +57,19 @@ class FirebaseStorageDataSource @Inject constructor(
* can fetch it). The bytes are couple-key ciphertext, so Storage holds nothing readable. Living * can fetch it). The bytes are couple-key ciphertext, so Storage holds nothing readable. Living
* under users/{uid}/ means the existing account-delete cleanup covers backups too. * under users/{uid}/ means the existing account-delete cleanup covers backups too.
*/ */
suspend fun uploadBackupSnapshot(uid: String, snapshotId: String, encryptedBytes: ByteArray): String = suspend fun uploadBackupSnapshot(uid: String, snapshotId: String, encryptedBytes: ByteArray): String {
suspendCancellableCoroutine { cont -> if (!isOnline()) throw IOException("No internet connection")
if (!isOnline()) { val ref = storage.reference.child("users/$uid/backups/$snapshotId")
cont.resumeWithException(IOException("No internet connection")) val metadata = StorageMetadata.Builder().setContentType("application/octet-stream").build()
return@suspendCancellableCoroutine return ref.putBytes(encryptedBytes, metadata).continueWithTask { ref.downloadUrl }.await().toString()
} }
val ref = storage.reference.child("users/$uid/backups/$snapshotId")
val metadata = StorageMetadata.Builder()
.setContentType("application/octet-stream")
.build()
ref.putBytes(encryptedBytes, metadata)
.continueWithTask { ref.downloadUrl }
.addOnSuccessListener { cont.resume(it.toString()) }
.addOnFailureListener { cont.resumeWithException(it) }
}
/** Best-effort delete of a previously-uploaded backup snapshot (post-compaction cleanup; succeeds /** Best-effort delete of a previously-uploaded backup snapshot (post-compaction cleanup; succeeds
* only for the uploader's own path, so a cross-partner stale blob is left for that owner/cleanup). */ * only for the uploader's own path, so a cross-partner stale blob is left for that owner/cleanup). */
suspend fun deleteBackupSnapshot(uid: String, snapshotId: String): Unit = suspend fun deleteBackupSnapshot(uid: String, snapshotId: String) {
suspendCancellableCoroutine { cont -> // Best-effort: a leftover blob is harmless ciphertext, so swallow failures.
storage.reference.child("users/$uid/backups/$snapshotId").delete() runCatching { storage.reference.child("users/$uid/backups/$snapshotId").delete().await() }
.addOnSuccessListener { cont.resume(Unit) } }
.addOnFailureListener { cont.resume(Unit) } // best-effort; a leftover blob is harmless ciphertext
}
/** /**
* Downloads the raw (still-encrypted) bytes for a media message over HTTP using the tokenized * Downloads the raw (still-encrypted) bytes for a media message over HTTP using the tokenized

View File

@ -11,11 +11,9 @@ import com.google.firebase.firestore.SetOptions
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.tasks.await
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/** /**
* Firestore data source for per-couple, per-date swipe state. * Firestore data source for per-couple, per-date swipe state.
@ -57,7 +55,7 @@ class FirestoreDateSwipeDataSource @Inject constructor(
path.set( path.set(
mapOf("actions" to mapOf(swipe.userId to entry)), mapOf("actions" to mapOf(swipe.userId to entry)),
SetOptions.merge() SetOptions.merge()
).voidAwait() ).await()
} }
/** Decrypt a stored action, tolerating legacy plaintext written before E2EE. */ /** Decrypt a stored action, tolerating legacy plaintext written before E2EE. */
@ -72,7 +70,7 @@ class FirestoreDateSwipeDataSource @Inject constructor(
suspend fun getSwipe(coupleId: String, dateIdeaId: String, userId: String): DateSwipe? { suspend fun getSwipe(coupleId: String, dateIdeaId: String, userId: String): DateSwipe? {
val aead = encryptionManager.aeadFor(coupleId) val aead = encryptionManager.aeadFor(coupleId)
val snap = swipesRef(coupleId).document(dateIdeaId).getDoc() val snap = swipesRef(coupleId).document(dateIdeaId).get().await()
return snap.toDateSwipe(dateIdeaId, userId, aead, coupleId) return snap.toDateSwipe(dateIdeaId, userId, aead, coupleId)
} }
@ -103,20 +101,13 @@ class FirestoreDateSwipeDataSource @Inject constructor(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private suspend fun getAllSwipesForUser(coupleId: String, userId: String): List<DateSwipe> { private suspend fun getAllSwipesForUser(coupleId: String, userId: String): List<DateSwipe> {
val aead = encryptionManager.aeadFor(coupleId) val aead = encryptionManager.aeadFor(coupleId)
val snap = swipesRef(coupleId).getQuery() val snap = swipesRef(coupleId).get().await()
return snap.documents.mapNotNull { it.toDateSwipe(it.id, userId, aead, coupleId) } return snap.documents.mapNotNull { it.toDateSwipe(it.id, userId, aead, coupleId) }
} }
private suspend fun com.google.firebase.firestore.CollectionReference.getQuery() =
suspendCancellableCoroutine { cont ->
get()
.addOnSuccessListener { cont.resume(it) }
.addOnFailureListener { cont.resumeWithException(it) }
}
suspend fun getAllSwipesForDate(coupleId: String, dateIdeaId: String): List<DateSwipe> { suspend fun getAllSwipesForDate(coupleId: String, dateIdeaId: String): List<DateSwipe> {
val aead = encryptionManager.aeadFor(coupleId) val aead = encryptionManager.aeadFor(coupleId)
val snap = swipesRef(coupleId).document(dateIdeaId).getDoc() val snap = swipesRef(coupleId).document(dateIdeaId).get().await()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val actions = snap.get("actions") as? Map<String, Map<String, Any>> ?: emptyMap() val actions = snap.get("actions") as? Map<String, Map<String, Any>> ?: emptyMap()
return actions.map { (uid, data) -> return actions.map { (uid, data) ->
@ -129,21 +120,6 @@ class FirestoreDateSwipeDataSource @Inject constructor(
} }
} }
// ─── Coroutine helpers ───────────────────────────────────────────────────
private suspend fun com.google.firebase.firestore.DocumentReference.getDoc(): DocumentSnapshot =
suspendCancellableCoroutine { cont ->
get()
.addOnSuccessListener { cont.resume(it) }
.addOnFailureListener { cont.resumeWithException(it) }
}
private suspend fun com.google.android.gms.tasks.Task<Void>.voidAwait() =
suspendCancellableCoroutine<Unit> { cont ->
addOnSuccessListener { cont.resume(Unit) }
addOnFailureListener { cont.resumeWithException(it) }
}
// ─── Mappers ───────────────────────────────────────────────────────────── // ─── Mappers ─────────────────────────────────────────────────────────────
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")