Skip to content

Commit 5779511

Browse files
authored
Merge pull request #14 from E-D-W-I-N/image-downloading
Added [#13] recipe image downloading
2 parents 0c28d44 + 5d4bb96 commit 5779511

File tree

9 files changed

+143
-21
lines changed

9 files changed

+143
-21
lines changed

app/src/main/AndroidManifest.xml

+4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
34
package="com.edwin.recipeapp">
45

56
<uses-permission android:name="android.permission.INTERNET" />
67
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8+
<uses-permission
9+
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
10+
tools:ignore="ScopedStorage" />
711

812
<application
913
android:name=".RecipeApplication"

app/src/main/java/com/edwin/recipeapp/database/RecipeDao.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ interface RecipeDao {
1111

1212
fun getRecipes(query: String, sortOrder: SortOrder): Flow<List<Recipe>> =
1313
when (sortOrder) {
14-
SortOrder.BY_NAME -> getAllRecipesByName(query)
15-
SortOrder.BY_DATE -> getAllRecipesByDate(query)
14+
SortOrder.BY_NAME -> getAllRecipesByName("%$query%")
15+
SortOrder.BY_DATE -> getAllRecipesByDate("%$query%")
1616
}
1717

18-
@Query("SELECT * FROM recipe WHERE name OR description OR instructions LIKE '%' || :searchQuery || '%' ORDER BY name")
18+
@Query("SELECT * FROM recipe WHERE name LIKE :searchQuery OR description LIKE :searchQuery OR instructions LIKE :searchQuery ORDER BY name")
1919
fun getAllRecipesByName(searchQuery: String): Flow<List<Recipe>>
2020

21-
@Query("SELECT * FROM recipe WHERE name OR description OR instructions LIKE '%' || :searchQuery || '%' ORDER BY lastUpdated")
21+
@Query("SELECT * FROM recipe WHERE name LIKE :searchQuery OR description LIKE :searchQuery OR instructions LIKE :searchQuery ORDER BY lastUpdated")
2222
fun getAllRecipesByDate(searchQuery: String): Flow<List<Recipe>>
2323

2424
@Insert(onConflict = OnConflictStrategy.REPLACE)

app/src/main/java/com/edwin/recipeapp/presentation/ui/recipeDetails/RecipeDetailsFragment.kt

+6-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ class RecipeDetailsFragment : Fragment(R.layout.recipe_details_fragment) {
4848
viewPagerPictures.adapter = viewPagerAdapterPicture
4949
TabLayoutMediator(pagerTabLayout, viewPagerPictures) { _, _ -> }.attach()
5050

51-
viewPagerRecommended.adapter = viewPagerAdapterRecommended
52-
viewPagerRecommended.offscreenPageLimit = 3
53-
viewPagerRecommended.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
54-
viewPagerRecommended.setPageTransformer(compositePageTransformer)
51+
viewPagerRecommended.apply {
52+
adapter = viewPagerAdapterRecommended
53+
offscreenPageLimit = 3
54+
getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
55+
setPageTransformer(compositePageTransformer)
56+
}
5557

5658
viewModel.recipe?.observe(viewLifecycleOwner, { result ->
5759
viewPagerAdapterPicture.submitList(result.data?.images)
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,102 @@
11
package com.edwin.recipeapp.presentation.ui.recipeDetails.recipePicture
22

3+
import android.Manifest
4+
import android.content.pm.PackageManager
5+
import android.graphics.Bitmap
6+
import android.graphics.drawable.Drawable
37
import android.os.Bundle
48
import android.view.Menu
59
import android.view.MenuInflater
10+
import android.view.MenuItem
611
import android.view.View
12+
import android.widget.Toast
13+
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
14+
import androidx.appcompat.app.AlertDialog
15+
import androidx.core.content.ContextCompat
716
import androidx.fragment.app.Fragment
817
import androidx.fragment.app.viewModels
918
import com.bumptech.glide.Glide
19+
import com.bumptech.glide.request.target.CustomTarget
20+
import com.bumptech.glide.request.transition.Transition
1021
import com.edwin.recipeapp.R
1122
import com.edwin.recipeapp.databinding.RecipePicuterFragmentBinding
23+
import com.edwin.recipeapp.util.saveToGallery
24+
1225

1326
class RecipePictureFragment : Fragment(R.layout.recipe_picuter_fragment) {
1427

1528
private val viewModel: RecipePictureViewModel by viewModels()
1629

30+
private val requestPermissionLauncher =
31+
registerForActivityResult(RequestPermission()
32+
) { isGranted: Boolean ->
33+
if (isGranted) {
34+
Toast.makeText(requireContext(), "Permission granted", Toast.LENGTH_LONG).show()
35+
} else {
36+
Toast.makeText(requireContext(), "Permission denied", Toast.LENGTH_LONG).show()
37+
}
38+
}
39+
1740
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
1841
super.onViewCreated(view, savedInstanceState)
1942
val binding = RecipePicuterFragmentBinding.bind(view)
20-
Glide.with(view)
21-
.load(viewModel.imageUrl)
22-
.into(binding.recipePicture)
43+
Glide.with(requireContext())
44+
.asBitmap()
45+
.load(viewModel.imageUrl)
46+
.into(binding.recipePicture)
2347
setHasOptionsMenu(true)
2448
}
2549

2650
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
2751
inflater.inflate(R.menu.menu_fragment_recipe_picture, menu)
2852
super.onCreateOptionsMenu(menu, inflater)
2953
}
54+
55+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
56+
return if (item.itemId == R.id.action_download) {
57+
checkPermission()
58+
true
59+
} else {
60+
super.onOptionsItemSelected(item)
61+
}
62+
}
63+
64+
private fun checkPermission() {
65+
when {
66+
ContextCompat.checkSelfPermission(
67+
requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
68+
== PackageManager.PERMISSION_GRANTED -> {
69+
saveImage()
70+
}
71+
shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) -> {
72+
showDialog()
73+
}
74+
else -> requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
75+
}
76+
}
77+
78+
private fun saveImage() {
79+
Glide.with(this)
80+
.asBitmap()
81+
.load(viewModel.imageUrl)
82+
.into(object : CustomTarget<Bitmap>() {
83+
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
84+
resource.saveToGallery(requireContext())
85+
}
86+
87+
override fun onLoadCleared(placeholder: Drawable?) {}
88+
})
89+
Toast.makeText(requireContext(), "Image downloaded", Toast.LENGTH_LONG).show()
90+
}
91+
92+
private fun showDialog() {
93+
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply { }
94+
.setMessage("Permission to access your storage is required to use this function")
95+
.setTitle("Permission required")
96+
.setPositiveButton("OK") { _, _ ->
97+
requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
98+
}
99+
.setNegativeButton("No, thanks") { _, _ -> }
100+
.create().show()
101+
}
30102
}

app/src/main/java/com/edwin/recipeapp/presentation/ui/recipesList/RecipeListFragment.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ class RecipeListFragment : Fragment(R.layout.recipe_list_fragment) {
3030

3131
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
3232
super.onViewCreated(view, savedInstanceState)
33-
setHasOptionsMenu(true)
3433
val binding = RecipeListFragmentBinding.bind(view)
3534

3635
val onRecipeClick = OnItemClickListener<Recipe> { viewModel.onRecipeSelected(it.uuid) }
@@ -48,9 +47,9 @@ class RecipeListFragment : Fragment(R.layout.recipe_list_fragment) {
4847
recipeAdapter.submitList(result.data)
4948

5049
binding.progressBar.isVisible =
51-
result is Resource.Loading && result.data.isNullOrEmpty()
50+
result is Resource.Loading && result.data.isNullOrEmpty()
5251
binding.textViewError.isVisible =
53-
result is Resource.Error && result.data.isNullOrEmpty()
52+
result is Resource.Error && result.data.isNullOrEmpty()
5453
binding.textViewError.text = result.error?.localizedMessage
5554
})
5655

@@ -59,14 +58,15 @@ class RecipeListFragment : Fragment(R.layout.recipe_list_fragment) {
5958
when (event) {
6059
is RecipeListViewModel.RecipeListEvents.NavigateToRecipeDetailScreen -> {
6160
val action =
62-
RecipeListFragmentDirections.actionRecipeListFragmentToRecipeDetailsFragment(
63-
event.uuid
64-
)
61+
RecipeListFragmentDirections.actionRecipeListFragmentToRecipeDetailsFragment(
62+
event.uuid
63+
)
6564
findNavController().navigate(action)
6665
}
6766
}
6867
}
6968
}
69+
setHasOptionsMenu(true)
7070
}
7171

7272
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

app/src/main/java/com/edwin/recipeapp/repository/RecipeRepository.kt

-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import com.edwin.recipeapp.database.RecipeDatabase
55
import com.edwin.recipeapp.network.RecipeApi
66
import com.edwin.recipeapp.util.SortOrder
77
import com.edwin.recipeapp.util.networkBoundResource
8-
import kotlinx.coroutines.delay
98
import javax.inject.Inject
109

1110
class RecipeRepository @Inject constructor(
@@ -19,7 +18,6 @@ class RecipeRepository @Inject constructor(
1918
recipeDao.getRecipes(query, sortOrder)
2019
},
2120
fetch = {
22-
delay(1000)
2321
api.getRecipes()
2422
},
2523
saveFetchResult = { recipes ->
@@ -35,7 +33,6 @@ class RecipeRepository @Inject constructor(
3533
recipeDao.getRecipeDetails(uuid)
3634
},
3735
fetch = {
38-
delay(1000)
3936
api.getRecipeDetails(uuid)
4037
},
4138
saveFetchResult = { response ->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.edwin.recipeapp.util
2+
3+
import android.content.ContentValues
4+
import android.content.Context
5+
import android.graphics.Bitmap
6+
import android.os.Build
7+
import android.os.Environment
8+
import android.provider.MediaStore
9+
import java.io.File
10+
import java.io.FileOutputStream
11+
import java.io.OutputStream
12+
13+
fun Bitmap.saveToGallery(context: Context) {
14+
val filename = "${System.currentTimeMillis()}.png"
15+
val write: (OutputStream) -> Boolean = {
16+
this.compress(Bitmap.CompressFormat.PNG, 100, it)
17+
}
18+
19+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
20+
val contentValues = ContentValues().apply {
21+
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
22+
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
23+
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/RecipeApp")
24+
}
25+
26+
context.contentResolver.let {
27+
it.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)?.let { uri ->
28+
it.openOutputStream(uri)?.let(write)
29+
}
30+
}
31+
} else {
32+
val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString() + File.separator + "RecipeApp"
33+
val file = File(imagesDir)
34+
if (!file.exists()) {
35+
file.mkdir()
36+
}
37+
val image = File(imagesDir, filename)
38+
write(FileOutputStream(image))
39+
}
40+
}

app/src/main/res/menu/menu_fragment_recipe_picture.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<menu xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:app="http://schemas.android.com/apk/res-auto">
44
<item
5-
android:title="@string/download"
5+
android:id="@+id/action_download"
66
android:icon="@drawable/ic_save"
7+
android:title="@string/download"
78
app:showAsAction="always" />
89
</menu>

app/src/main/res/values/styles.xml

+6
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@
1616
<item name="android:minHeight">30dp</item>
1717
<item name="android:maxHeight">30dp</item>
1818
</style>
19+
20+
<style name="AlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert">
21+
<item name="colorPrimary">@color/black</item>
22+
<item name="colorPrimaryDark">@color/white</item>
23+
<item name="colorAccent">@color/purple_500</item>
24+
</style>
1925
</resources>

0 commit comments

Comments
 (0)