Skip to content

Commit af9cae3

Browse files
committed
Load species on experimentation
Signed-off-by: Kyle Corry <kylecorry31@gmail.com>
1 parent d631146 commit af9cae3

File tree

5 files changed

+184
-56
lines changed

5 files changed

+184
-56
lines changed

app/src/main/java/com/kylecorry/trail_sense/shared/io/FileSubsystem.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class FileSubsystem private constructor(private val context: Context) {
4747
return local.getFile(path, create)
4848
}
4949

50+
fun getDirectory(path: String, create: Boolean = false): File {
51+
return local.getDirectory(path, create)
52+
}
53+
5054
fun list(path: String): List<File> {
5155
return local.list(path)
5256
}
@@ -148,6 +152,11 @@ class FileSubsystem private constructor(private val context: Context) {
148152
get(filename, true)
149153
}
150154

155+
suspend fun createTempDirectory(): File = onIO {
156+
val filename = "${TEMP_DIR}/${UUID.randomUUID()}"
157+
getDirectory(filename, true)
158+
}
159+
151160
fun getLocalPath(file: File): String {
152161
return local.getRelativePath(file)
153162
}

app/src/main/java/com/kylecorry/trail_sense/shared/views/Views.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.kylecorry.trail_sense.shared.views
22

33
import android.content.Context
4+
import android.net.Uri
45
import android.view.View
56
import android.view.ViewGroup
7+
import android.widget.ImageView
68
import android.widget.LinearLayout
79
import android.widget.ScrollView
810
import android.widget.TextView
@@ -54,4 +56,16 @@ object Views {
5456
}
5557
}
5658

59+
fun image(
60+
context: Context,
61+
uri: Uri,
62+
width: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
63+
height: Int = ViewGroup.LayoutParams.WRAP_CONTENT
64+
): View {
65+
return ImageView(context).apply {
66+
setImageURI(uri)
67+
layoutParams = ViewGroup.LayoutParams(width, height)
68+
}
69+
}
70+
5771
}

app/src/main/java/com/kylecorry/trail_sense/tools/experimentation/ExperimentationFragment.kt

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
package com.kylecorry.trail_sense.tools.experimentation
22

3+
import android.graphics.Bitmap
34
import android.os.Bundle
4-
import android.text.method.LinkMovementMethod
5-
import android.text.util.Linkify
5+
import android.util.Size
66
import android.view.LayoutInflater
77
import android.view.View
88
import android.view.ViewGroup
9+
import com.kylecorry.andromeda.alerts.dialog
910
import com.kylecorry.andromeda.core.coroutines.BackgroundMinimumState
11+
import com.kylecorry.andromeda.core.coroutines.onIO
12+
import com.kylecorry.andromeda.core.system.Resources
1013
import com.kylecorry.andromeda.fragments.BoundFragment
1114
import com.kylecorry.andromeda.fragments.inBackground
15+
import com.kylecorry.andromeda.views.list.AsyncListIcon
16+
import com.kylecorry.andromeda.views.list.ListItem
1217
import com.kylecorry.trail_sense.databinding.FragmentExperimentationBinding
1318
import com.kylecorry.trail_sense.shared.io.DeleteTempFilesCommand
1419
import com.kylecorry.trail_sense.shared.io.FileSubsystem
20+
import com.kylecorry.trail_sense.shared.views.Views
1521
import com.kylecorry.trail_sense.tools.species_catalog.Species
1622

1723
class ExperimentationFragment : BoundFragment<FragmentExperimentationBinding>() {
1824

19-
private var species by state<Species?>(null)
25+
private var species by state<List<Species>>(emptyList())
26+
private var filter by state("")
2027
private val importer by lazy { SpeciesImportService.create(this) }
2128
private val files by lazy { FileSubsystem.getInstance(requireContext()) }
2229

@@ -28,33 +35,85 @@ class ExperimentationFragment : BoundFragment<FragmentExperimentationBinding>()
2835

2936
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
3037
super.onViewCreated(view, savedInstanceState)
31-
binding.text.movementMethod = LinkMovementMethod.getInstance()
32-
binding.text.autoLinkMask = Linkify.WEB_URLS
3338
inBackground(BackgroundMinimumState.Created) {
34-
species = importer.import()
39+
val tagOrder = listOf(
40+
"Plant",
41+
"Fungus",
42+
"Mammal",
43+
"Bird",
44+
"Reptile",
45+
"Amphibian",
46+
"Fish",
47+
"Insect",
48+
"Arachnid",
49+
"Crustacean",
50+
"Mollusk",
51+
)
52+
species = (importer.import() ?: emptyList()).sortedWith(
53+
compareBy(
54+
{
55+
it.tags.minOfOrNull { tag ->
56+
val order = tagOrder.indexOf(tag)
57+
if (order == -1) tagOrder.size else order
58+
}
59+
},
60+
{ it.name })
61+
)
3562
}
36-
binding.title.setOnClickListener {
37-
inBackground(BackgroundMinimumState.Created) {
38-
species = importer.import()
39-
// TODO: Ask the user if they want to import them, if they do copy the files to local storage and delete temp files
40-
}
63+
64+
binding.search.setOnSearchListener {
65+
filter = it
4166
}
4267
}
4368

4469
override fun onUpdate() {
4570
super.onUpdate()
46-
effect2(species) {
47-
binding.title.title.text = species?.name
48-
binding.text.text = species?.notes
49-
binding.title.subtitle.text = species?.tags?.joinToString(", ")
50-
if (species?.images?.isNotEmpty() == true) {
51-
binding.image.setImageURI(files.uri(species?.images?.firstOrNull() ?: ""))
52-
} else {
53-
binding.image.setImageBitmap(null)
54-
}
71+
effect2(species, filter) {
72+
binding.list.setItems(species.filter { it.name.lowercase().contains(filter.trim()) }
73+
.map {
74+
val firstSentence = it.notes?.substringBefore(".")?.plus(".") ?: ""
75+
ListItem(
76+
it.id,
77+
it.name,
78+
it.tags.joinToString(", ") + "\n\n" + firstSentence.take(200),
79+
icon = AsyncListIcon(
80+
viewLifecycleOwner,
81+
{ loadThumbnail(it) },
82+
size = 48f,
83+
clearOnPause = true
84+
),
85+
) {
86+
dialog(
87+
it.name,
88+
it.notes ?: "",
89+
allowLinks = true,
90+
contentView = Views.image(
91+
requireContext(),
92+
files.uri(it.images.first()),
93+
width = ViewGroup.LayoutParams.MATCH_PARENT,
94+
height = Resources.dp(requireContext(), 200f).toInt()
95+
),
96+
scrollable = true
97+
)
98+
}
99+
})
100+
}
101+
}
102+
103+
private suspend fun loadThumbnail(species: Species): Bitmap = onIO {
104+
val size = Resources.dp(requireContext(), 48f).toInt()
105+
try {
106+
files.bitmap(species.images.first(), Size(size, size)) ?: getDefaultThumbnail()
107+
} catch (e: Exception) {
108+
getDefaultThumbnail()
55109
}
56110
}
57111

112+
private fun getDefaultThumbnail(): Bitmap {
113+
val size = Resources.dp(requireContext(), 48f).toInt()
114+
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
115+
}
116+
58117
override fun onDestroy() {
59118
super.onDestroy()
60119
inBackground {

app/src/main/java/com/kylecorry/trail_sense/tools/experimentation/SpeciesImportService.kt

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.kylecorry.trail_sense.tools.experimentation
22

3+
import com.kylecorry.andromeda.files.ZipUtils
34
import com.kylecorry.andromeda.fragments.AndromedaFragment
45
import com.kylecorry.andromeda.json.JsonConvert
56
import com.kylecorry.luna.coroutines.onIO
@@ -18,31 +19,86 @@ class SpeciesImportService(
1819
private val uriService: UriService,
1920
private val files: FileSubsystem
2021
) :
21-
ImportService<Species> {
22-
override suspend fun import(): Species? = onIO {
22+
ImportService<List<Species>> {
23+
24+
// TODO: Use an enum for tags
25+
private val idToTag = mapOf(
26+
"Africa" to 1,
27+
"Antarctica" to 2,
28+
"Asia" to 3,
29+
"Australia" to 4,
30+
"Europe" to 5,
31+
"North America" to 6,
32+
"South America" to 7,
33+
"Plant" to 8,
34+
"Animal" to 9,
35+
"Fungus" to 10,
36+
"Bird" to 11,
37+
"Mammal" to 12,
38+
"Reptile" to 13,
39+
"Amphibian" to 14,
40+
"Fish" to 15,
41+
"Insect" to 16,
42+
"Arachnid" to 17,
43+
"Crustacean" to 18,
44+
"Mollusk" to 19,
45+
"Forest" to 20,
46+
"Desert" to 21,
47+
"Grassland" to 22,
48+
"Wetland" to 23,
49+
"Mountain" to 24,
50+
"Urban" to 25,
51+
"Marine" to 26,
52+
"Freshwater" to 27,
53+
"Cave" to 28,
54+
"Tundra" to 29,
55+
).map { it.value to it.key }.toMap()
56+
57+
override suspend fun import(): List<Species>? = onIO {
2358
val uri = uriPicker.open(
2459
listOf(
25-
"application/json"
60+
"application/json",
61+
"application/zip"
2662
)
2763
) ?: return@onIO null
2864
val stream = uriService.inputStream(uri) ?: return@onIO null
2965
stream.use {
30-
// TODO: Parse from zip (write images to a temp directory)
31-
return@use parseJson(it)
66+
if (files.getMimeType(uri) == "application/json") {
67+
return@use parseJson(it)
68+
}
69+
return@use parseZip(it)
70+
}
71+
}
72+
73+
private suspend fun parseZip(stream: InputStream): List<Species>? {
74+
val root = files.createTempDirectory()
75+
ZipUtils.unzip(stream, root, MAX_ZIP_FILE_COUNT)
76+
77+
// Parse each file as a JSON file
78+
val species = mutableListOf<Species>()
79+
80+
for (file in root.listFiles() ?: return null) {
81+
if (file.extension == "json") {
82+
species.addAll(parseJson(file.inputStream()) ?: emptyList())
83+
}
3284
}
85+
86+
return species
3387
}
3488

35-
private suspend fun parseJson(stream: InputStream): Species? {
89+
private suspend fun parseJson(stream: InputStream): List<Species>? {
3690
val json = stream.bufferedReader().use { it.readText() }
3791
return try {
3892
val parsed = JsonConvert.fromJson<SpeciesJson>(json) ?: return null
3993
val images = parsed.images.map { saveImage(it) }
40-
Species(
41-
0,
42-
parsed.name,
43-
images,
44-
parsed.tags,
45-
parsed.notes ?: ""
94+
listOf(
95+
Species(
96+
0,
97+
parsed.name,
98+
images,
99+
parsed.tags.mapNotNull { idToTag[it] },
100+
parsed.notes ?: ""
101+
)
46102
)
47103
} catch (e: Exception) {
48104
null
@@ -61,7 +117,7 @@ class SpeciesImportService(
61117
class SpeciesJson {
62118
var name: String = ""
63119
var images: List<String> = emptyList()
64-
var tags: List<String> = emptyList()
120+
var tags: List<Int> = emptyList()
65121
var notes: String? = null
66122
}
67123

@@ -73,5 +129,7 @@ class SpeciesImportService(
73129
FileSubsystem.getInstance(fragment.requireContext())
74130
)
75131
}
132+
133+
private const val MAX_ZIP_FILE_COUNT = 10000
76134
}
77135
}
Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
33
android:layout_width="match_parent"
4-
android:layout_height="match_parent">
4+
android:layout_height="match_parent"
5+
android:orientation="vertical">
56

6-
<LinearLayout
7+
<com.kylecorry.trail_sense.shared.views.SearchView
8+
android:id="@+id/search"
79
android:layout_width="match_parent"
810
android:layout_height="wrap_content"
9-
android:orientation="vertical">
11+
android:padding="16dp" />
1012

11-
<com.kylecorry.andromeda.views.toolbar.Toolbar
12-
android:id="@+id/title"
13-
android:layout_width="match_parent"
14-
android:layout_height="wrap_content" />
15-
16-
<com.kylecorry.andromeda.views.image.AsyncImageView
17-
android:id="@+id/image"
18-
android:layout_width="match_parent"
19-
android:layout_height="150dp" />
20-
21-
<TextView
22-
android:id="@+id/text"
23-
android:layout_width="match_parent"
24-
android:layout_height="wrap_content"
25-
android:padding="16dp" />
26-
27-
28-
</LinearLayout>
29-
</ScrollView>
13+
<com.kylecorry.andromeda.views.list.AndromedaListView
14+
android:id="@+id/list"
15+
android:layout_width="match_parent"
16+
android:layout_height="match_parent" />
17+
</LinearLayout>

0 commit comments

Comments
 (0)