Skip to content

Commit 9323f40

Browse files
authored
Merge pull request #55 from SimonMarquis/coroutines-and-supspend-functions
Fix misleading usage of coroutines and suspend functions
2 parents 970c990 + 29aa55f commit 9323f40

File tree

8 files changed

+65
-66
lines changed

8 files changed

+65
-66
lines changed

core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,22 @@ import java.io.File
2626
* It does not perform any actual file operations.
2727
*/
2828
class TestFileProvider : LocalFileProvider {
29-
override fun saveBitmapToFile(
29+
override suspend fun saveBitmapToFile(
3030
bitmap: Bitmap,
3131
file: File,
32-
): File {
32+
) {
3333
TODO("Not yet implemented")
3434
}
3535

36-
override fun getFileFromCache(fileName: String): File {
36+
override suspend fun getFileFromCache(fileName: String): File {
3737
TODO("Not yet implemented")
3838
}
3939

40-
override fun createCacheFile(fileName: String): File {
40+
override suspend fun createCacheFile(fileName: String): File {
4141
TODO("Not yet implemented")
4242
}
4343

44-
override fun saveToSharedStorage(
44+
override suspend fun saveToSharedStorage(
4545
file: File,
4646
fileName: String,
4747
mimeType: String,
@@ -53,11 +53,11 @@ class TestFileProvider : LocalFileProvider {
5353
TODO("Not yet implemented")
5454
}
5555

56-
override fun copyToInternalStorage(uri: Uri): File {
56+
override suspend fun copyToInternalStorage(uri: Uri): File {
5757
return File("")
5858
}
5959

60-
override fun saveUriToSharedStorage(
60+
override suspend fun saveUriToSharedStorage(
6161
inputUri: Uri,
6262
fileName: String,
6363
mimeType: String,

core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,109 +22,114 @@ import android.net.Uri
2222
import android.os.Build
2323
import android.os.Environment
2424
import android.provider.MediaStore
25+
import androidx.annotation.WorkerThread
2526
import androidx.core.content.FileProvider
27+
import kotlinx.coroutines.CoroutineDispatcher
28+
import kotlinx.coroutines.Dispatchers
29+
import kotlinx.coroutines.withContext
2630
import java.io.File
27-
import java.io.FileInputStream
2831
import java.io.FileOutputStream
2932
import java.io.IOException
33+
import java.nio.file.Files
3034
import java.util.UUID
3135
import javax.inject.Inject
36+
import javax.inject.Named
3237
import javax.inject.Singleton
3338

3439
interface LocalFileProvider {
35-
fun saveBitmapToFile(bitmap: Bitmap, file: File): File
36-
fun getFileFromCache(fileName: String): File
37-
fun createCacheFile(fileName: String): File
38-
fun saveToSharedStorage(file: File, fileName: String, mimeType: String): Uri
40+
@WorkerThread
41+
suspend fun saveBitmapToFile(bitmap: Bitmap, file: File)
42+
@WorkerThread
43+
suspend fun getFileFromCache(fileName: String): File
44+
@WorkerThread
45+
suspend fun createCacheFile(fileName: String): File
46+
@WorkerThread
47+
suspend fun saveToSharedStorage(file: File, fileName: String, mimeType: String): Uri
3948
fun sharingUriForFile(file: File): Uri
40-
fun copyToInternalStorage(uri: Uri): File
41-
fun saveUriToSharedStorage(
42-
inputUri: Uri,
43-
fileName: String,
44-
mimeType: String,
45-
): Uri
49+
@WorkerThread
50+
suspend fun copyToInternalStorage(uri: Uri): File
51+
@WorkerThread
52+
suspend fun saveUriToSharedStorage(inputUri: Uri, fileName: String, mimeType: String): Uri
4653
}
4754

4855
@Singleton
49-
open class LocalFileProviderImpl @Inject constructor(val application: Context) : LocalFileProvider {
56+
open class LocalFileProviderImpl @Inject constructor(
57+
val application: Context,
58+
@Named("IO")
59+
val ioDispatcher: CoroutineDispatcher,
60+
) : LocalFileProvider {
5061

51-
override fun saveBitmapToFile(bitmap: Bitmap, file: File): File {
62+
override suspend fun saveBitmapToFile(bitmap: Bitmap, file: File) = withContext(ioDispatcher) {
5263
var outputStream: FileOutputStream? = null
5364
try {
5465
outputStream = FileOutputStream(file)
5566
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
5667
outputStream.flush()
57-
return file
5868
} catch (e: IOException) {
5969
throw e
6070
} finally {
6171
outputStream?.close()
6272
}
6373
}
64-
override fun getFileFromCache(fileName: String): File {
65-
return File(application.cacheDir, fileName)
74+
75+
override suspend fun getFileFromCache(fileName: String): File = withContext(ioDispatcher) {
76+
File(application.cacheDir, fileName)
6677
}
6778

6879
@Throws(IOException::class)
69-
override fun createCacheFile(fileName: String): File {
80+
override suspend fun createCacheFile(fileName: String): File = withContext(ioDispatcher) {
7081
val cacheDir = application.cacheDir
7182
val imageFile = File(cacheDir, fileName)
7283
if (!imageFile.createNewFile()) {
7384
throw IOException("Unable to create file: ${imageFile.absolutePath}")
7485
}
75-
return imageFile
86+
return@withContext imageFile
7687
}
7788

78-
override fun saveToSharedStorage(
89+
override suspend fun saveToSharedStorage(
7990
file: File,
8091
fileName: String,
8192
mimeType: String,
82-
): Uri {
93+
): Uri = withContext(ioDispatcher) {
8394
val (uri, contentValues) = createSharedStorageEntry(fileName, mimeType)
8495
saveFileToUri(file, uri)
8596
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
8697
contentValues.put(MediaStore.Images.ImageColumns.IS_PENDING, 0)
8798
}
8899
application.contentResolver.update(uri, contentValues, null, null)
89-
return uri
100+
return@withContext uri
90101
}
91102

92-
override fun saveUriToSharedStorage(
103+
override suspend fun saveUriToSharedStorage(
93104
inputUri: Uri,
94105
fileName: String,
95106
mimeType: String,
96-
): Uri {
107+
): Uri = withContext(ioDispatcher) {
97108
val (newUri, contentValues) = createSharedStorageEntry(fileName, mimeType)
98109
application.contentResolver.openOutputStream(newUri)?.use { outputStream ->
99110
application.contentResolver.openInputStream(inputUri)?.use { inputStream ->
100-
val buffer = ByteArray(4 * 1024) // 4 KB buffer size - adjust as needed
101-
var bytesRead: Int
102-
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
103-
outputStream.write(buffer, 0, bytesRead)
104-
}
111+
inputStream.copyTo(outputStream)
105112
}
106113
} ?: throw IOException("Failed to open output stream.")
107114
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
108115
contentValues.put(MediaStore.Images.ImageColumns.IS_PENDING, 0)
109116
}
110117
application.contentResolver.update(newUri, contentValues, null, null)
111-
return newUri
118+
return@withContext newUri
112119
}
113120

114121
@Throws(IOException::class)
122+
@WorkerThread
115123
private fun saveFileToUri(file: File, uri: Uri) {
116124
application.contentResolver.openOutputStream(uri)?.use { outputStream ->
117-
FileInputStream(file).use { inputStream ->
118-
val buffer = ByteArray(4 * 1024) // 4 KB buffer size - adjust as needed
119-
var bytesRead: Int
120-
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
121-
outputStream.write(buffer, 0, bytesRead)
122-
}
125+
file.inputStream().use { inputStream ->
126+
inputStream.copyTo(outputStream)
123127
}
124128
} ?: throw IOException("Failed to open output stream for uri: $uri")
125129
}
126130

127131
@Throws(IOException::class)
132+
@WorkerThread
128133
private fun createSharedStorageEntry(fileName: String, mimeType: String): Pair<Uri, ContentValues> {
129134
val resolver = application.contentResolver
130135
val contentValues = ContentValues().apply {
@@ -160,14 +165,14 @@ open class LocalFileProviderImpl @Inject constructor(val application: Context) :
160165
}
161166

162167
@Throws(IOException::class)
163-
override fun copyToInternalStorage(uri: Uri): File {
168+
override suspend fun copyToInternalStorage(uri: Uri): File = withContext(ioDispatcher) {
164169
val uuid = UUID.randomUUID()
165170
val file = File(application.cacheDir, "temp_file_$uuid")
166171
application.contentResolver.openInputStream(uri)?.use { inputStream ->
167172
file.outputStream().use { outputStream ->
168173
inputStream.copyTo(outputStream)
169174
}
170175
}
171-
return file
176+
return@withContext file
172177
}
173178
}

data/src/main/java/com/android/developers/androidify/data/DataModule.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ internal object DataModule {
4242

4343
@Provides
4444
@Singleton
45-
fun provideLocalFileProvider(@ApplicationContext appContext: Context): LocalFileProvider =
46-
LocalFileProviderImpl(appContext)
45+
fun provideLocalFileProvider(@ApplicationContext appContext: Context, @Named("IO") ioDispatcher: CoroutineDispatcher): LocalFileProvider =
46+
LocalFileProviderImpl(appContext, ioDispatcher)
4747

4848
@Provides
4949
@Singleton

data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ internal class ImageGenerationRepositoryImpl @Inject constructor(
103103

104104
override suspend fun saveImage(imageBitmap: Bitmap): Uri {
105105
val cacheFile = localFileProvider.createCacheFile("shared_image_${UUID.randomUUID()}.jpg")
106-
val file = localFileProvider.saveBitmapToFile(imageBitmap, cacheFile)
107-
return localFileProvider.sharingUriForFile(file)
106+
localFileProvider.saveBitmapToFile(imageBitmap, cacheFile)
107+
return localFileProvider.sharingUriForFile(cacheFile)
108108
}
109109

110110
override suspend fun saveImageToExternalStorage(imageBitmap: Bitmap): Uri {

feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,19 @@ import com.android.developers.androidify.data.TextGenerationRepository
3535
import com.android.developers.androidify.util.LocalFileProvider
3636
import dagger.hilt.android.lifecycle.HiltViewModel
3737
import dagger.hilt.android.qualifiers.ApplicationContext
38-
import kotlinx.coroutines.CoroutineDispatcher
39-
import kotlinx.coroutines.Dispatchers
40-
import kotlinx.coroutines.cancel
38+
import kotlinx.coroutines.Job
4139
import kotlinx.coroutines.flow.MutableStateFlow
4240
import kotlinx.coroutines.flow.StateFlow
4341
import kotlinx.coroutines.flow.update
4442
import kotlinx.coroutines.launch
4543
import javax.inject.Inject
46-
import javax.inject.Named
4744

4845
@HiltViewModel
4946
class CreationViewModel @Inject constructor(
5047
val internetConnectivityManager: InternetConnectivityManager,
5148
val imageGenerationRepository: ImageGenerationRepository,
5249
val textGenerationRepository: TextGenerationRepository,
5350
val fileProvider: LocalFileProvider,
54-
@Named("IO")
55-
val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
5651
@ApplicationContext
5752
val context: Context,
5853
) : ViewModel() {
@@ -74,6 +69,9 @@ class CreationViewModel @Inject constructor(
7469
val snackbarHostState: StateFlow<SnackbarHostState>
7570
get() = _snackbarHostState
7671

72+
private var promptGenerationJob: Job? = null
73+
private var imageGenerationJob: Job? = null
74+
7775
fun onImageSelected(uri: Uri?) {
7876
_uiState.update {
7977
it.copy(
@@ -96,7 +94,8 @@ class CreationViewModel @Inject constructor(
9694
}
9795

9896
fun onPromptGenerationClicked() {
99-
viewModelScope.launch(ioDispatcher) {
97+
promptGenerationJob?.cancel()
98+
promptGenerationJob = viewModelScope.launch {
10099
Log.d("CreationViewModel", "Generating prompt...")
101100
_uiState.update {
102101
it.copy(promptGenerationInProgress = true)
@@ -122,7 +121,8 @@ class CreationViewModel @Inject constructor(
122121
}
123122

124123
fun startClicked() {
125-
viewModelScope.launch(ioDispatcher) {
124+
imageGenerationJob?.cancel()
125+
imageGenerationJob = viewModelScope.launch {
126126
if (internetConnectivityManager.isInternetAvailable()) {
127127
try {
128128
_uiState.update {
@@ -196,7 +196,8 @@ class CreationViewModel @Inject constructor(
196196
}
197197

198198
fun cancelInProgressTask() {
199-
ioDispatcher.cancel()
199+
promptGenerationJob?.cancel()
200+
imageGenerationJob?.cancel()
200201
_uiState.update {
201202
it.copy(screenState = ScreenState.EDIT)
202203
}

feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ class CreationViewModelTest {
5757
imageGenerationRepository,
5858
TestTextGenerationRepository(),
5959
TestFileProvider(),
60-
UnconfinedTestDispatcher(),
6160
context = RuntimeEnvironment.getApplication(),
6261
)
6362
}

feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,16 @@ import androidx.lifecycle.ViewModel
2222
import androidx.lifecycle.viewModelScope
2323
import com.android.developers.androidify.data.ImageGenerationRepository
2424
import dagger.hilt.android.lifecycle.HiltViewModel
25-
import kotlinx.coroutines.CoroutineDispatcher
26-
import kotlinx.coroutines.Dispatchers
2725
import kotlinx.coroutines.flow.MutableStateFlow
2826
import kotlinx.coroutines.flow.StateFlow
2927
import kotlinx.coroutines.flow.asStateFlow
3028
import kotlinx.coroutines.flow.update
3129
import kotlinx.coroutines.launch
3230
import javax.inject.Inject
33-
import javax.inject.Named
3431

3532
@HiltViewModel
3633
class ResultsViewModel @Inject constructor(
3734
val imageGenerationRepository: ImageGenerationRepository,
38-
@Named("IO")
39-
val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
4035
) : ViewModel() {
4136

4237
private val _state = MutableStateFlow(ResultState())
@@ -58,7 +53,7 @@ class ResultsViewModel @Inject constructor(
5853
}
5954

6055
fun shareClicked() {
61-
viewModelScope.launch(ioDispatcher) {
56+
viewModelScope.launch {
6257
val resultUrl = state.value.resultImageBitmap
6358
if (resultUrl != null) {
6459
val imageFileUri = imageGenerationRepository.saveImage(resultUrl)
@@ -70,7 +65,7 @@ class ResultsViewModel @Inject constructor(
7065
}
7166
}
7267
fun downloadClicked() {
73-
viewModelScope.launch(ioDispatcher) {
68+
viewModelScope.launch {
7469
val resultBitmap = state.value.resultImageBitmap
7570
val originalImage = state.value.originalImageUrl
7671
if (originalImage != null) {

feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ class ResultsViewModelTest {
4848
fun setup() {
4949
viewModel = ResultsViewModel(
5050
FakeImageGenerationRepository(),
51-
UnconfinedTestDispatcher(),
5251
)
5352
}
5453

0 commit comments

Comments
 (0)