Skip to content

Commit 6a028a1

Browse files
author
mcbaumwolle
committed
refactor: use OpenDocument picker and support PNG mime types
1 parent 47b62f3 commit 6a028a1

File tree

5 files changed

+47
-18
lines changed

5 files changed

+47
-18
lines changed

feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/data/datasource/LocalAvatarImageDataSource.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package net.thunderbird.feature.account.avatar.data.datasource
33
import com.eygraber.uri.Uri
44
import net.thunderbird.core.file.DirectoryProvider
55
import net.thunderbird.core.file.FileManager
6+
import net.thunderbird.core.file.MimeType
7+
import net.thunderbird.core.file.MimeTypeResolver
68
import net.thunderbird.feature.account.AccountId
79
import net.thunderbird.feature.account.avatar.data.AvatarDataContract.DataSource.LocalAvatarImage
810

@@ -14,25 +16,27 @@ import net.thunderbird.feature.account.avatar.data.AvatarDataContract.DataSource
1416
*
1517
* @param fileManager The [FileManager] for file operations.
1618
* @param directoryProvider The [DirectoryProvider] to get app directories.
19+
* @param mimeTypeResolver The [MimeTypeResolver] to detect image formats.
1720
*/
1821
internal class LocalAvatarImageDataSource(
1922
private val fileManager: FileManager,
2023
private val directoryProvider: DirectoryProvider,
24+
private val mimeTypeResolver: MimeTypeResolver,
2125
) : LocalAvatarImage {
2226

2327
override suspend fun update(id: AccountId, imageUri: Uri): Uri {
24-
// Detect desired extension from input (simple heuristic)
25-
val isPng = imageUri.toString().endsWith(".png", ignoreCase = true)
28+
// Using the resolver to detect the actual (MIME) type
29+
val mimeType = mimeTypeResolver.getMimeType(imageUri)
30+
val isPng = mimeType == MimeType.PNG
31+
32+
// Set the correct extension (.png or .jpg)
2633
val targetExtension = if (isPng) EXTENSION_PNG else EXTENSION_JPG
2734

28-
// 1. Clean up any existing avatars (jpg or png) for this account
29-
// to avoid having "123.jpg" and "123.png" existing simultaneously.
35+
// Cleaning up old files
3036
delete(id)
3137

32-
// 2. Generate the new target URI
38+
// 4. Generating the new path and copy
3339
val avatarImageUri = getAvatarImageUri(id, targetExtension)
34-
35-
// 3. Copy the file
3640
fileManager.copy(imageUri, avatarImageUri)
3741

3842
return avatarImageUri

feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/di/FeatureAccountAvatarModule.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ val featureAccountAvatarModule = module {
1111
LocalAvatarImageDataSource(
1212
fileManager = get(),
1313
directoryProvider = get(),
14+
mimeTypeResolver = get(),
1415
)
1516
}
1617

feature/account/avatar/impl/src/test/kotlin/net/thunderbird/feature/account/avatar/data/datasource/LocalAvatarImageDataSourceTest.kt

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import kotlinx.coroutines.test.runTest
1111
import net.thunderbird.core.file.DirectoryProvider
1212
import net.thunderbird.core.file.FileManager
1313
import net.thunderbird.core.file.FileOperationError
14+
import net.thunderbird.core.file.MimeType
15+
import net.thunderbird.core.file.MimeTypeResolver
1416
import net.thunderbird.core.outcome.Outcome
17+
import net.thunderbird.feature.account.AccountId
1518
import net.thunderbird.feature.account.AccountIdFactory
1619
import net.thunderbird.feature.account.avatar.data.AvatarDataContract
1720
import org.junit.Rule
@@ -25,24 +28,29 @@ class LocalAvatarImageDataSourceTest {
2528

2629
private lateinit var directoryProvider: DirectoryProvider
2730
private lateinit var fileManager: SpyFileManager
31+
private lateinit var mimeTypeResolver: FakeMimeTypeResolver
2832
private lateinit var testSubject: LocalAvatarImageDataSource
2933

3034
@BeforeTest
3135
fun setUp() {
3236
val appDir = folder.newFolder("app")
3337
directoryProvider = FakeDirectoryProvider(appDir.absolutePath.toKmpUri())
3438
fileManager = SpyFileManager()
35-
testSubject = LocalAvatarImageDataSource(fileManager, directoryProvider)
39+
mimeTypeResolver = FakeMimeTypeResolver()
40+
testSubject = LocalAvatarImageDataSource(fileManager, directoryProvider, mimeTypeResolver)
3641
}
3742

3843
@Test
3944
fun `update with JPG should copy image to JPG path and clean up old files`() = runTest {
4045
// Arrange
4146
val accountId = AccountIdFactory.create()
4247
val source = "file:///external/picked/image.jpg".toKmpUri()
43-
val expectedDir = getAvatarDir()
48+
val expectedDir = getAvatarDir(accountId)
4449
val expectedDest = expectedDir.buildUpon().appendPath("$accountId.jpg").build()
4550

51+
// Tell the fake that this source is a JPEG
52+
mimeTypeResolver.mimeTypeToReturn = MimeType.JPEG
53+
4654
// Act
4755
val returned = testSubject.update(accountId, source)
4856

@@ -51,7 +59,7 @@ class LocalAvatarImageDataSourceTest {
5159
assertThat(fileManager.lastCopySource).isEqualTo(source)
5260
assertThat(fileManager.lastCopyDestination).isEqualTo(expectedDest)
5361

54-
// Verify
62+
// Verify cleanup
5563
val expectedPngDel = expectedDir.buildUpon().appendPath("$accountId.png").build()
5664
val expectedJpgDel = expectedDir.buildUpon().appendPath("$accountId.jpg").build()
5765
assertThat(fileManager.deletedPaths).contains(expectedPngDel)
@@ -63,9 +71,12 @@ class LocalAvatarImageDataSourceTest {
6371
// Arrange
6472
val accountId = AccountIdFactory.create()
6573
val source = "file:///external/picked/photo.png".toKmpUri()
66-
val expectedDir = getAvatarDir()
74+
val expectedDir = getAvatarDir(accountId)
6775
val expectedDest = expectedDir.buildUpon().appendPath("$accountId.png").build()
6876

77+
// Tell the fake that this source is a PNG
78+
mimeTypeResolver.mimeTypeToReturn = MimeType.PNG
79+
6980
// Act
7081
val returned = testSubject.update(accountId, source)
7182

@@ -79,7 +90,7 @@ class LocalAvatarImageDataSourceTest {
7990
fun `delete should remove both JPG and PNG paths`() = runTest {
8091
// Arrange
8192
val accountId = AccountIdFactory.create()
82-
val expectedDir = getAvatarDir()
93+
val expectedDir = getAvatarDir(accountId)
8394
val jpgPath = expectedDir.buildUpon().appendPath("$accountId.jpg").build()
8495
val pngPath = expectedDir.buildUpon().appendPath("$accountId.png").build()
8596

@@ -91,13 +102,22 @@ class LocalAvatarImageDataSourceTest {
91102
assertThat(fileManager.deletedPaths).contains(pngPath)
92103
}
93104

94-
private suspend fun getAvatarDir(): Uri {
105+
private suspend fun getAvatarDir(accountId: AccountId): Uri {
95106
return directoryProvider.getFilesDir().buildUpon()
96107
.appendPath(AvatarDataContract.DataSource.LocalAvatarImage.DIRECTORY_NAME)
97108
.build()
98109
}
99110

100-
// Fixed SpyFileManager to match interface return types
111+
// --- Helpers ---
112+
113+
class FakeMimeTypeResolver : MimeTypeResolver {
114+
var mimeTypeToReturn: MimeType? = null
115+
116+
override fun getMimeType(uri: Uri): MimeType? {
117+
return mimeTypeToReturn
118+
}
119+
}
120+
101121
class SpyFileManager : FileManager {
102122
var lastCopySource: Uri? = null
103123
var lastCopyDestination: Uri? = null
@@ -117,5 +137,8 @@ class LocalAvatarImageDataSourceTest {
117137
override suspend fun createDirectories(uri: Uri): Outcome<Unit, FileOperationError> {
118138
return Outcome.Success(Unit)
119139
}
140+
141+
// Add other methods if FileManager interface requires them (e.g., exists, listFiles)
142+
// override suspend fun exists(uri: Uri): Boolean = true
120143
}
121144
}

feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/UpdateAvatarImage.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ internal class UpdateAvatarImage(
2222
): Outcome<Avatar.Image, AccountSettingError> {
2323
val mimeType = mimeTypeResolver.getMimeType(imageUri)
2424

25-
if (mimeType == null || mimeType != MimeType.JPEG) {
25+
// Check for both JPEG and PNG
26+
if (mimeType == null || (mimeType != MimeType.JPEG && mimeType != MimeType.PNG)) {
2627
return Outcome.Failure(
2728
AccountSettingError.UnsupportedFormat(
28-
message = "Only JPEG images are supported. Found: ${mimeType ?: "unknown"}",
29+
message = "Only JPEG and PNG images are supported. Found: ${mimeType ?: "unknown"}",
2930
),
3031
)
3132
}

feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal fun GeneralSettingsScreen(
2525
provider: SettingViewProvider = koinInject(),
2626
builder: SettingsBuilder = koinInject(),
2727
) {
28-
val imagePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
28+
val imagePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
2929
if (uri != null) {
3030
viewModel.event(Event.OnAvatarImagePicked(uri.toKmpUri()))
3131
}
@@ -35,7 +35,7 @@ internal fun GeneralSettingsScreen(
3535
when (effect) {
3636
is Effect.NavigateBack -> onBack()
3737
is Effect.OpenAvatarImagePicker -> {
38-
imagePicker.launch("image/*")
38+
imagePicker.launch(arrayOf("image/jpeg", "image/png"))
3939
}
4040
}
4141
}

0 commit comments

Comments
 (0)