chore(gitignore): ignore .code-workspace; cleanup Firebase data source dead code
This commit is contained in:
parent
39fbd943ff
commit
f160f141d6
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue