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
ClaudeQAPlan.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.StorageMetadata
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.tasks.await
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Singleton
class FirebaseStorageDataSource @Inject constructor(
@ -31,19 +29,13 @@ class FirebaseStorageDataSource @Inject constructor(
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
suspend fun uploadProfilePhoto(uid: String, uri: Uri): String =
suspendCancellableCoroutine { cont ->
suspend fun uploadProfilePhoto(uid: String, uri: Uri): String {
val ref = storage.reference.child("users/$uid/profile.jpg")
val mimeType = context.contentResolver.getType(uri)
?.takeIf { it.startsWith("image/") }
?: "image/jpeg"
val metadata = StorageMetadata.Builder()
.setContentType(mimeType)
.build()
ref.putFile(uri, metadata)
.continueWithTask { ref.downloadUrl }
.addOnSuccessListener { cont.resume(it.toString()) }
.addOnFailureListener { cont.resumeWithException(it) }
val metadata = StorageMetadata.Builder().setContentType(mimeType).build()
return ref.putFile(uri, metadata).continueWithTask { ref.downloadUrl }.await().toString()
}
/**
@ -51,21 +43,12 @@ class FirebaseStorageDataSource @Inject constructor(
* profile-photo ownership model) and returns the tokenized download URL. The bytes are
* ciphertext, so Storage never holds anything readable.
*/
suspend fun uploadEncryptedMedia(uid: String, encryptedBytes: ByteArray): String =
suspendCancellableCoroutine { cont ->
suspend fun uploadEncryptedMedia(uid: String, encryptedBytes: ByteArray): String {
// Fail fast when clearly offline so the chat shows a retry instead of a stuck spinner.
if (!isOnline()) {
cont.resumeWithException(IOException("No internet connection"))
return@suspendCancellableCoroutine
}
if (!isOnline()) throw IOException("No internet connection")
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) }
val metadata = StorageMetadata.Builder().setContentType("application/octet-stream").build()
return ref.putBytes(encryptedBytes, metadata).continueWithTask { ref.downloadUrl }.await().toString()
}
/**
@ -74,29 +57,18 @@ class FirebaseStorageDataSource @Inject constructor(
* 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.
*/
suspend fun uploadBackupSnapshot(uid: String, snapshotId: String, encryptedBytes: ByteArray): String =
suspendCancellableCoroutine { cont ->
if (!isOnline()) {
cont.resumeWithException(IOException("No internet connection"))
return@suspendCancellableCoroutine
}
suspend fun uploadBackupSnapshot(uid: String, snapshotId: String, encryptedBytes: ByteArray): String {
if (!isOnline()) throw IOException("No internet connection")
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) }
val metadata = StorageMetadata.Builder().setContentType("application/octet-stream").build()
return ref.putBytes(encryptedBytes, metadata).continueWithTask { ref.downloadUrl }.await().toString()
}
/** 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). */
suspend fun deleteBackupSnapshot(uid: String, snapshotId: String): Unit =
suspendCancellableCoroutine { cont ->
storage.reference.child("users/$uid/backups/$snapshotId").delete()
.addOnSuccessListener { cont.resume(Unit) }
.addOnFailureListener { cont.resume(Unit) } // best-effort; a leftover blob is harmless ciphertext
suspend fun deleteBackupSnapshot(uid: String, snapshotId: String) {
// Best-effort: a leftover blob is harmless ciphertext, so swallow failures.
runCatching { storage.reference.child("users/$uid/backups/$snapshotId").delete().await() }
}
/**

View File

@ -11,11 +11,9 @@ import com.google.firebase.firestore.SetOptions
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Firestore data source for per-couple, per-date swipe state.
@ -57,7 +55,7 @@ class FirestoreDateSwipeDataSource @Inject constructor(
path.set(
mapOf("actions" to mapOf(swipe.userId to entry)),
SetOptions.merge()
).voidAwait()
).await()
}
/** 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? {
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)
}
@ -103,20 +101,13 @@ class FirestoreDateSwipeDataSource @Inject constructor(
@Suppress("UNCHECKED_CAST")
private suspend fun getAllSwipesForUser(coupleId: String, userId: String): List<DateSwipe> {
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) }
}
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> {
val aead = encryptionManager.aeadFor(coupleId)
val snap = swipesRef(coupleId).document(dateIdeaId).getDoc()
val snap = swipesRef(coupleId).document(dateIdeaId).get().await()
@Suppress("UNCHECKED_CAST")
val actions = snap.get("actions") as? Map<String, Map<String, Any>> ?: emptyMap()
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 ─────────────────────────────────────────────────────────────
@Suppress("UNCHECKED_CAST")