Skip to content

Commit 6968a3d

Browse files
authored
Merge pull request #44 from davcres/feature/import-export-history
import export history
2 parents b93f81a + f7a8a5d commit 6968a3d

15 files changed

Lines changed: 461 additions & 65 deletions

File tree

app/detekt-baseline.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
<ID>MagicNumber:HistoryMonthCard.kt$100</ID>
4545
<ID>MagicNumber:HistoryMonthCard.kt$5000</ID>
4646
<ID>MagicNumber:HistoryMonthDetailBottomSheet.kt$0.80f</ID>
47+
<ID>MagicNumber:ImportHistoryUseCase.kt$ImportHistoryUseCase$5</ID>
48+
<ID>MagicNumber:ImportHistoryUseCase.kt$ImportHistoryUseCase$6</ID>
4749
<ID>MagicNumber:InvestmentEntity.kt$10</ID>
4850
<ID>MagicNumber:InvestmentEntity.kt$5</ID>
4951
<ID>MagicNumber:InvestmentEntity.kt$6</ID>
@@ -184,6 +186,9 @@
184186
<ID>ReturnCount:GlobalMarketRegion.kt$GlobalMarketRegion.Companion$fun from(value: String?): GlobalMarketRegion</ID>
185187
<ID>ReturnCount:String.kt$fun String.isValidIsin(): Boolean</ID>
186188
<ID>SpreadOperator:PortfolioScreen.kt$(*currentTabBatch.toTypedArray())</ID>
189+
<ID>TooGenericExceptionThrown:FileRepositoryImpl.kt$FileRepositoryImpl$throw Exception("Failed to create MediaStore entry")</ID>
190+
<ID>TooGenericExceptionThrown:FileRepositoryImpl.kt$FileRepositoryImpl$throw Exception("Failed to open input stream")</ID>
191+
<ID>TooGenericExceptionThrown:FileRepositoryImpl.kt$FileRepositoryImpl$throw Exception("Failed to open output stream")</ID>
187192
<ID>TooManyFunctions:PortfolioViewModel.kt$PortfolioViewModel : ViewModel</ID>
188193
<ID>UnusedParameter:ComicBubble.kt$onDismiss: () -&gt; Unit</ID>
189194
<ID>UnusedParameter:Double.kt$decimals: Int = 2</ID>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.davidcrespo.onewallet.data.repository
2+
3+
import android.content.ContentValues
4+
import android.content.Context
5+
import android.os.Environment
6+
import android.provider.MediaStore
7+
import androidx.core.net.toUri
8+
import com.davidcrespo.onewallet.domain.di.DispatcherProvider
9+
import com.davidcrespo.onewallet.domain.repository.FileRepository
10+
import kotlinx.coroutines.withContext
11+
import java.io.OutputStreamWriter
12+
13+
class FileRepositoryImpl(
14+
private val context: Context,
15+
private val dispatcher: DispatcherProvider
16+
) : FileRepository {
17+
18+
override suspend fun saveToDownloads(fileName: String, content: String): Result<Unit> = withContext(dispatcher.io) {
19+
runCatching {
20+
val contentResolver = context.contentResolver
21+
val contentValues = ContentValues().apply {
22+
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
23+
put(MediaStore.MediaColumns.MIME_TYPE, "text/csv")
24+
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
25+
}
26+
27+
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
28+
?: throw Exception("Failed to create MediaStore entry")
29+
30+
contentResolver.openOutputStream(uri)?.use { outputStream ->
31+
OutputStreamWriter(outputStream).use { writer ->
32+
writer.write(content)
33+
}
34+
} ?: throw Exception("Failed to open output stream")
35+
}
36+
}
37+
38+
override suspend fun readFromUri(uriString: String): Result<String> = withContext(dispatcher.io) {
39+
runCatching {
40+
val uri = uriString.toUri()
41+
context.contentResolver.openInputStream(uri)?.use { inputStream ->
42+
inputStream.bufferedReader().use { it.readText() }
43+
} ?: throw Exception("Failed to open input stream")
44+
}
45+
}
46+
}

app/src/main/java/com/davidcrespo/onewallet/di/RepositoryModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import com.davidcrespo.onewallet.domain.repository.OnboardingRepository
1010
import com.davidcrespo.onewallet.domain.repository.PortfolioRepository
1111
import com.davidcrespo.onewallet.domain.repository.PriceAlertNotificationRepository
1212
import com.davidcrespo.onewallet.domain.repository.ThemeRepository
13+
import com.davidcrespo.onewallet.data.repository.FileRepositoryImpl
14+
import com.davidcrespo.onewallet.domain.repository.FileRepository
15+
import org.koin.android.ext.koin.androidContext
1316
import org.koin.dsl.module
1417

1518
val repositoryModule = module {
@@ -22,4 +25,5 @@ val repositoryModule = module {
2225
single<OnboardingRepository> { OnboardingRepositoryImpl(get()) }
2326
single<ThemeRepository> { ThemeRepositoryImpl(get()) }
2427
single<PriceAlertNotificationRepository> { PriceAlertNotificationRepositoryImpl(get()) }
28+
single<FileRepository> { FileRepositoryImpl(androidContext(), get()) }
2529
}

app/src/main/java/com/davidcrespo/onewallet/di/UseCaseModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.davidcrespo.onewallet.di
22

33
import com.davidcrespo.onewallet.domain.usecase.appRoot.GetThemeUseCase
44
import com.davidcrespo.onewallet.domain.usecase.appRoot.SetThemeUseCase
5+
import com.davidcrespo.onewallet.domain.usecase.history.ExportHistoryUseCase
56
import com.davidcrespo.onewallet.domain.usecase.history.GetMonthlyHistoryUseCase
7+
import com.davidcrespo.onewallet.domain.usecase.history.ImportHistoryUseCase
68
import com.davidcrespo.onewallet.domain.usecase.market.AddMarketAssetToPortfolioUseCase
79
import com.davidcrespo.onewallet.domain.usecase.market.GetGlobalMarketAssetsUseCase
810
import com.davidcrespo.onewallet.domain.usecase.market.GetUSMarketAssetsUseCase
@@ -33,6 +35,8 @@ val useCaseModule = module {
3335

3436
single { SaveMonthlyPortfolioUseCase(get()) }
3537
single { GetMonthlyHistoryUseCase(get()) }
38+
single { ImportHistoryUseCase(get()) }
39+
single { ExportHistoryUseCase(get()) }
3640

3741
single { GetThemeUseCase(get()) }
3842
single { SetThemeUseCase(get()) }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.davidcrespo.onewallet.domain.repository
2+
3+
interface FileRepository {
4+
suspend fun saveToDownloads(fileName: String, content: String): Result<Unit>
5+
suspend fun readFromUri(uriString: String): Result<String>
6+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.davidcrespo.onewallet.domain.usecase.history
2+
3+
import com.davidcrespo.onewallet.domain.repository.PortfolioRepository
4+
5+
class ExportHistoryUseCase(
6+
private val portfolioRepository: PortfolioRepository
7+
) {
8+
suspend operator fun invoke(): Result<String> {
9+
return runCatching {
10+
val history = portfolioRepository.getMonthsPortfolio()
11+
val csv = StringBuilder()
12+
// Header
13+
csv.append("Symbol;Name;Quantity;Price;PreviousPrice;Currency;Type;Year;Month\n")
14+
history.forEach {
15+
csv.append("${it.symbol};${it.name};${it.quantity};${it.price};${it.previousPrice};${it.currency.code};${it.type.name};${it.year};${it.month}\n")
16+
}
17+
csv.toString()
18+
}
19+
}
20+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.davidcrespo.onewallet.domain.usecase.history
2+
3+
import com.davidcrespo.onewallet.domain.model.investment.Currency
4+
import com.davidcrespo.onewallet.domain.model.investment.Investment
5+
import com.davidcrespo.onewallet.domain.model.investment.InvestmentType
6+
import com.davidcrespo.onewallet.domain.repository.PortfolioRepository
7+
8+
class ImportHistoryUseCase(
9+
private val portfolioRepository: PortfolioRepository
10+
) {
11+
suspend operator fun invoke(csvContent: String): Result<Unit> {
12+
return runCatching {
13+
val lines = csvContent.lines()
14+
if (lines.size < 2) return@runCatching // Header + at least one line
15+
16+
val investments = lines.drop(1).filter { it.isNotBlank() }.map { line ->
17+
val parts = line.split(";")
18+
Investment(
19+
symbol = parts[0],
20+
name = parts[1],
21+
quantity = parts[2].toDouble(),
22+
price = parts[3].toDouble(),
23+
previousPrice = parts[4].toDouble(),
24+
currency = Currency(parts[5]),
25+
type = InvestmentType.valueOf(parts[6]),
26+
year = parts[7].toInt(),
27+
month = parts[8].toInt()
28+
)
29+
}
30+
portfolioRepository.addOrUpdateItems(investments)
31+
}
32+
}
33+
}

app/src/main/java/com/davidcrespo/onewallet/presentation/history/HistoryContract.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ data class HistoryUiState(
1515
val selectedInvestment: InvestmentView? = null,
1616
val selectedPreviousInvestment: InvestmentView? = null,
1717
val selectedCurrency: CurrencyView = CurrencyView.get(EUR),
18-
val isLoading: Boolean = false
18+
val isLoading: Boolean = false,
19+
val showFilePicker: Boolean = false,
20+
val error: String? = null,
21+
val successMessage: String? = null
1922
)
2023

2124
sealed interface HistoryIntent {
@@ -25,4 +28,8 @@ sealed interface HistoryIntent {
2528
data class SelectInvestment(val investment: InvestmentView) : HistoryIntent
2629
data object DismissBottomSheet : HistoryIntent
2730
data object DismissInvestmentDetail : HistoryIntent
31+
data object ImportHistory : HistoryIntent
32+
data object ExportHistory : HistoryIntent
33+
data class OnFileSelected(val uri: String) : HistoryIntent
34+
data object ClearMessages : HistoryIntent
2835
}

0 commit comments

Comments
 (0)