Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Error screen #28

Merged
merged 4 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'com.android.application'
id("com.github.triplet.play") version "3.6.0"
id 'kotlin-android'
id "com.google.firebase.crashlytics"
}

apply plugin: 'kotlin-parcelize'
Expand Down Expand Up @@ -60,6 +61,7 @@ dependencies {
implementation project(":algorithms")
implementation platform('com.google.firebase:firebase-bom:28.4.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation "com.google.firebase:firebase-crashlytics"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation 'androidx.activity:activity-ktx:1.8.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
Expand All @@ -69,8 +71,10 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
testImplementation 'junit:junit:4.13.2'
testImplementation("io.mockk:mockk:1.13.9")
testImplementation "org.mockito:mockito-core:4.8.1"
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand Down
58 changes: 36 additions & 22 deletions app/src/main/java/com/randomimagecreator/ImageCreator.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
package com.randomimagecreator

import android.content.ContentResolver
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.randomimagecreator.algorithms.ImageCreating
import com.randomimagecreator.algorithms.Mandelbrot
import com.randomimagecreator.algorithms.Pixelated
import com.randomimagecreator.algorithms.SierpinskiCarpet
import com.randomimagecreator.algorithms.SolidColor
import com.randomimagecreator.common.BitmapCreator
import com.randomimagecreator.common.HsvToHexConverter
import com.randomimagecreator.common.ImageSaver
import com.randomimagecreator.configuration.Configuration
import com.randomimagecreator.configuration.ImagePattern
import com.randomimagecreator.result.ImageCreationResult
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlin.system.measureTimeMillis

class ImageCreator {
var bitmapSaveNotifier: MutableSharedFlow<Nothing?> = MutableSharedFlow()
class ImageCreator(
private val imageSaver: ImageSaver = ImageSaver(),
private val bitmapCreator: BitmapCreator = BitmapCreator()
) {
val bitmapSafeNotifier get() = _bitmapSaveNotifier.asSharedFlow()
private var _bitmapSaveNotifier: MutableSharedFlow<Nothing?> = MutableSharedFlow()

suspend fun create(
contentResolver: ContentResolver,
saveDirectory: DocumentFile,
configuration: Configuration
): ImageCreationResult {
): Result<ImageCreationResult> {
val width = configuration.width
val height = configuration.height
val algorithm: ImageCreating = when (configuration.pattern) {
Expand All @@ -38,38 +43,47 @@ class ImageCreator {
val images = mutableListOf<Array<String>>()
val creationDurationInMilliseconds = measureTimeMillis {
for (i in 0..<configuration.amount) {
val image = algorithm.createImage()
val image = try {
algorithm.createImage()
} catch (exception: Exception) {
return Result.failure(ImageCreatingAlgorithmError())
}
val flattenedImage = image.flatMap { it.asIterable() }.toTypedArray()
images.add(flattenedImage)
}
}

val bitmaps = images.map { bitmap ->
Bitmap.createBitmap(
bitmap.map { pixel -> Color.parseColor(pixel) }.toIntArray(),
configuration.width,
configuration.height,
Bitmap.Config.ARGB_8888
).also {
it.setHasAlpha(false)
val bitmaps = try {
images.map { bitmap ->
bitmapCreator.create(bitmap, configuration.width, configuration.height)
}
} catch (exception: IllegalArgumentException) {
return Result.failure(BitmapCreationError())
}

val uris = mutableListOf<Uri>()
val saveDurationInMilliseconds = measureTimeMillis {
for (bitmap in bitmaps) {
bitmapSaveNotifier.emit(null)
val uri = ImageSaver.saveBitmap(
bitmap,
contentResolver,
saveDirectory,
configuration.format,
)
val uri = try {
imageSaver.saveBitmap(
bitmap,
contentResolver,
saveDirectory,
configuration.format,
)
} catch (exception: Exception) {
return Result.failure(BitmapSaveError())
}
_bitmapSaveNotifier.emit(null)
uri?.let { uris.add(it) }
}
}

val durationInMilliseconds = creationDurationInMilliseconds + saveDurationInMilliseconds
return ImageCreationResult(uris, durationInMilliseconds)
return Result.success(ImageCreationResult(uris, durationInMilliseconds))
}
}

class ImageCreatingAlgorithmError : Error()
class BitmapCreationError : Error()
class BitmapSaveError : Error()
7 changes: 7 additions & 0 deletions app/src/main/java/com/randomimagecreator/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.google.firebase.FirebaseApp
import com.randomimagecreator.choosesavedirectory.ChooseSaveDirectoryFragment
import com.randomimagecreator.common.Analytics
import com.randomimagecreator.configuration.ConfigurationFragment
import com.randomimagecreator.error.ErrorFragment
import com.randomimagecreator.loading.LoadingFragment
import com.randomimagecreator.result.ResultFragment
import kotlinx.coroutines.launch
Expand All @@ -21,6 +23,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.setup()
FirebaseApp.initializeApp(applicationContext)
replaceFragment(ConfigurationFragment(), isRootFragment = true)
lifecycleScope.launch {
viewModel.navigationRequestBroadcaster.collect(::onNavigationRequest)
Expand Down Expand Up @@ -54,6 +57,10 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
replaceFragment(LoadingFragment(), LoadingFragment.TAG)
}

is Screen.Error -> {
replaceFragment(ErrorFragment())
}

is Screen.Result -> {
replaceFragment(ResultFragment())
}
Expand Down
22 changes: 12 additions & 10 deletions app/src/main/java/com/randomimagecreator/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package com.randomimagecreator
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import androidx.core.database.getStringOrNull
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.randomimagecreator.common.errors.SaveDirectoryMissingError
import com.randomimagecreator.common.extensions.query
import com.randomimagecreator.configuration.Configuration
Expand All @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch

private const val TAG = "MainViewModel"

/**
* ViewModel used throughout the app.
Expand Down Expand Up @@ -61,15 +60,18 @@ class MainViewModel : ViewModel() {
val saveDirectoryUri = configuration.saveDirectory ?: throw SaveDirectoryMissingError()
val saveDirectory =
DocumentFile.fromTreeUri(context, saveDirectoryUri) ?: throw SaveDirectoryMissingError()

val config = configuration.copy(width = -1)
viewModelScope.launch(Dispatchers.IO) {
try {
imageCreationResult =
imageCreator.create(context.contentResolver, saveDirectory, configuration)
navigationRequestBroadcaster.emit(Screen.Result)
} catch (exception: Exception) {
Log.e(TAG, "", exception)
}
imageCreator.create(context.contentResolver, saveDirectory, config).fold(
onSuccess = {
imageCreationResult = it
navigationRequestBroadcaster.emit(Screen.Result)
},
onFailure = {
FirebaseCrashlytics.getInstance().recordException(it)
navigationRequestBroadcaster.emit(Screen.Error)
}
)
}
}

Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/randomimagecreator/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ sealed class Screen {
*/
data object Loading : Screen()

/**
* Screen showing an error occurred during image generation.
*/
data object Error : Screen()

/**
* Screen showing images that got created.
*/
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/com/randomimagecreator/common/BitmapCreator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.randomimagecreator.common

import android.graphics.Bitmap
import android.graphics.Color

/**
* Wrapper class for creating Bitmaps.
*/
class BitmapCreator {
fun create(
bitmap: Array<String>,
width: Int,
height: Int
): Bitmap {
return Bitmap.createBitmap(
bitmap.map { pixel -> Color.parseColor(pixel) }.toIntArray(),
width,
height,
Bitmap.Config.ARGB_8888
).also {
it.setHasAlpha(false)
}
}
}
35 changes: 5 additions & 30 deletions app/src/main/java/com/randomimagecreator/common/ImageSaver.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.randomimagecreator.common

import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
Expand All @@ -10,39 +9,15 @@ import com.randomimagecreator.configuration.ImageFileFormat
/**
* Helper class for saving images.
*/
object ImageSaver {
class ImageSaver {
/**
* Saves the given array of bitmaps to the provided [directory].
* Saves the given bitmap to the provided [saveDirectory].
*
* @param bitmaps A list of all bitmaps to save.
* @param context The context of the caller.
* @param directory Directory selected by user for saving.
* @param bitmap Bitmap to save.
* @param contentResolver Resolver for creating the outputstream.
* @param saveDirectory Directory selected by user for saving.
* @param format Format to save the bitmap in.
* @param notifier Notifier that emits the amount of saved bitmaps.
*/
fun saveBitmaps(
bitmaps: List<Bitmap>,
context: Context,
directory: Uri,
format: ImageFileFormat,
// notifier: MutableSharedFlow<Nothing?>
): List<Uri> {
val rootDocumentFile = DocumentFile.fromTreeUri(context, directory)
assert(rootDocumentFile != null) { "root document file shouldn't be null" }

val bitmapUris = mutableListOf<Uri>()

for (bitmap in bitmaps) {
saveBitmap(bitmap, context.contentResolver, rootDocumentFile!!, format)?.let {
bitmapUris.add(it)
// scope.launch {
// notifier.emit(null)
// }
}
}
return bitmapUris
}

fun saveBitmap(
bitmap: Bitmap,
contentResolver: ContentResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.randomimagecreator.error

import androidx.fragment.app.Fragment
import com.randomimagecreator.R

/**
* Provides feedback to the user an error occurred during image generation.
*/
class ErrorFragment : Fragment(R.layout.fragment_error)
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
numberOfSavedImagesTextField = view.findViewById(R.id.loading_saved_amount)

viewLifecycleOwner.lifecycleScope.launch {
viewModel.imageCreator.bitmapSaveNotifier.collect {
viewModel.imageCreator.bitmapSafeNotifier.collect {
numberOfSavedImages++
updateNumberOfSavedImages()
}
Expand Down
53 changes: 53 additions & 0 deletions app/src/main/res/layout/fragment_error.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_begin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="28dp" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="28dp" />

<include
android:id="@+id/shared_logo"
layout="@layout/shared_top_logo"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/error_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/error_title"
android:textSize="18sp"
android:textStyle="bold"
android:textAlignment="center"
app:layout_constraintEnd_toStartOf="@id/guideline_end"
app:layout_constraintStart_toEndOf="@id/guideline_begin"
app:layout_constraintTop_toBottomOf="@id/shared_logo"
app:layout_constraintBottom_toBottomOf="parent"/>

<TextView
android:id="@+id/error_instructions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/error_instructions"
android:textSize="14sp"
android:textStyle="normal"
android:textAlignment="center"
android:paddingTop="4dp"
app:layout_constraintEnd_toStartOf="@id/guideline_end"
app:layout_constraintStart_toEndOf="@id/guideline_begin"
app:layout_constraintTop_toBottomOf="@id/error_title" />

</androidx.constraintlayout.widget.ConstraintLayout>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@
<string name="image_creator_pixelated">Pixelated</string>
<string name="image_creator_mandelbrot">Mandelbrot</string>
<string name="image_creator_sierpinski">Sierpinski carpet</string>
<string name="error_title">An error occurred :(</string>
<string name="error_instructions">Please try it again with a different configuration</string>
</resources>
Loading
Loading