Skip to content

Commit

Permalink
Merge pull request #10 from nijuyonkadesu/share-and-download
Browse files Browse the repository at this point in the history
Share and download
  • Loading branch information
nijuyonkadesu authored Jul 2, 2023
2 parents 7791b8e + d655f00 commit d9b0ac7
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 30 deletions.
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="one.njk.sao.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
26 changes: 21 additions & 5 deletions app/src/main/java/one/njk/sao/CarouselFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import one.njk.sao.adapters.CarouselAdapter
Expand All @@ -29,9 +30,13 @@ import one.njk.sao.viewmodels.ArtsViewModel
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
@AndroidEntryPoint
class CarouselFragment : Fragment(), MenuProvider {

private var _binding: FragmentCarouselBinding? = null
enum class OperatingMode {
SHARE, DOWNLOAD
}

// This property is only valid between onCreateView and
// onDestroyView.
Expand All @@ -48,7 +53,7 @@ class CarouselFragment : Fragment(), MenuProvider {
menuHost.addMenuProvider( this, viewLifecycleOwner, Lifecycle.State.RESUMED)
_binding = FragmentCarouselBinding.inflate(inflater, container, false)

val adapter = CarouselAdapter()
val adapter = CarouselAdapter(viewModel.imageLoader, requireContext(), lifecycleScope)
_binding!!.apply {
lifecycleOwner = viewLifecycleOwner
artsViewModel = viewModel
Expand All @@ -61,26 +66,37 @@ class CarouselFragment : Fragment(), MenuProvider {
true
}
R.id.download -> {
Toast.makeText(context, "download", Toast.LENGTH_SHORT).show()
// TODO: Replace with viewmodel download with URI intent
// No API is exposed to find the current focused child in CarouselLayoutManager
val displayMetrics = resources.displayMetrics
val x = displayMetrics.widthPixels / 2.3f
val y = displayMetrics.heightPixels / 3f

Log.d("screen", "${displayMetrics.widthPixels} -> $x")

val focusedChild = carouselRecyclerView.findChildViewUnder(x, y)
// Since same callback is used for both download and share
operatingMode = OperatingMode.DOWNLOAD
focusedChild?.performClick()
true
}
else -> false
}
}
share.setOnClickListener {
// Get the screen size
// No API was exposed to find the current focused child with Carousel View
val displayMetrics = resources.displayMetrics
val x = displayMetrics.widthPixels / 2.3f
val y = displayMetrics.heightPixels / 3f

Log.d("screen", "${displayMetrics.widthPixels} -> $x")
// TODO: This pixel calculation might break in other DPI setting than default

// Find the child view based on x and y pixel values
val focusedChild = carouselRecyclerView.findChildViewUnder(x, y)
// Since same callback is used for both download and share
operatingMode = OperatingMode.SHARE
focusedChild?.performClick()
}

carouselRecyclerView.adapter = adapter
subscribeUi(adapter)
carouselRecyclerView.addOnScrollListener(
Expand Down
17 changes: 4 additions & 13 deletions app/src/main/java/one/njk/sao/SaoApplication.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
package one.njk.sao

import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp

@Volatile
lateinit var operatingMode: CarouselFragment.OperatingMode
@HiltAndroidApp
class SaoApplication: Application(), ImageLoaderFactory {
class SaoApplication: Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}

// customizing factory class to add GIF Decoder, context.ImageLoader is a singleton
// we obtain from this factory class [see BindingAdapter.kt]
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(applicationContext)
.components {
add(ImageDecoderDecoder.Factory())
}.build()
}
}
}
9 changes: 3 additions & 6 deletions app/src/main/java/one/njk/sao/adapters/BindingAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ package one.njk.sao.adapters
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.databinding.BindingAdapter
import coil.imageLoader
import coil.ImageLoader
import coil.request.ImageRequest

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

// Custom ImageLoader with GIF support obtained from factory class
val imageLoader = imgView.context.imageLoader
@BindingAdapter("imageUrl", "imageLoader")
fun bindImage(imgView: ImageView, imgUrl: String?, imageLoader: ImageLoader) {

if(!imgUrl.isNullOrEmpty()){
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Expand Down
116 changes: 112 additions & 4 deletions app/src/main/java/one/njk/sao/adapters/CarouselAdapter.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
package one.njk.sao.adapters

import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import one.njk.sao.CarouselFragment
import one.njk.sao.databinding.CarouselViewWaifuBinding
import one.njk.sao.models.Waifu
import one.njk.sao.operatingMode
import java.io.File
import java.io.OutputStream

class CarouselAdapter: ListAdapter<Waifu, CarouselAdapter.WaifuViewHolder>(DiffCallback) {
class CarouselAdapter(
val imageLoader: ImageLoader,
val context: Context,
val lifecycleScope: CoroutineScope
): ListAdapter<Waifu, CarouselAdapter.WaifuViewHolder>(DiffCallback) {

// List Adapter makes uses of this to choose whether to update an item in a list
companion object DiffCallback : DiffUtil.ItemCallback<Waifu>() {
Expand All @@ -21,22 +41,110 @@ class CarouselAdapter: ListAdapter<Waifu, CarouselAdapter.WaifuViewHolder>(DiffC
}
}

class WaifuViewHolder(private val binding: CarouselViewWaifuBinding):
class WaifuViewHolder(
private val binding: CarouselViewWaifuBinding,
val imageLoaderHilt: ImageLoader,
val context: Context,
val lifecycleScope: CoroutineScope
):
RecyclerView.ViewHolder(binding.root) {
@OptIn(ExperimentalCoilApi::class)
fun bind(waifu: Waifu){
binding.apply {
this.waifu = waifu
this.imageLoader = imageLoaderHilt
executePendingBindings()
}
binding.carouselItemContainer.setOnClickListener {
Log.d("url", waifu.url)
lifecycleScope.launch {
Log.d("url", waifu.url)

imageLoaderHilt.diskCache!!.openSnapshot(waifu.url)?.let {
val file = it.data.toFile()
Log.d("url", file.length().toString())
it.close()

if(operatingMode == CarouselFragment.OperatingMode.SHARE) {
val uri = uriFromCacheDir(file, waifu.url)
launchShareIntent(uri)
}
else {
saveToPicturesDir(file, waifu.url)
Toast.makeText(context, "Downloaded", Toast.LENGTH_SHORT).show()
}
}
}
}
}
fun launchShareIntent(uri: Uri){
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "image/*"
putExtra(Intent.EXTRA_STREAM, uri)
}
context.startActivity(Intent.createChooser(shareIntent, "Share Image"))
}

fun uriFromCacheDir(file: File, url: String): Uri{
val filename = getFilename(url)
val ext = getFileExtension(url)
val shareDir = File(context.cacheDir, "share_cache")

val path = file.copyTo(
File(shareDir,
"$filename.$ext"),
true)

return FileProvider.getUriForFile(context, "one.njk.sao.fileprovider",path)
}

fun saveToPicturesDir(file: File, url: String) {
val filename = getFilename(url)
val ext = getFileExtension(url)

var imageUri: Uri?
var fos: OutputStream?

val contentValues = ContentValues().apply {
val mimeType = when (ext) {
"jpeg" -> "image/jpeg"
"jpg" -> "image/jpeg"
"png" -> "image/png"
"gif" -> "image/gif"
else -> "image/jpeg"
}

put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Video.Media.IS_PENDING, 1)
}
context.contentResolver.also { resolver ->
imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
fos = imageUri?.let { resolver.openOutputStream(it) }
}

fos?.write(file.readBytes())

contentValues.clear()
contentValues.put(MediaStore.Video.Media.IS_PENDING, 0)
context.contentResolver.update(imageUri!!, contentValues, null, null)
}
fun getFilename(url: String): String {
val endpoint = url.split('.')
return endpoint[endpoint.lastIndex - 1].split('/').last()
}

fun getFileExtension(url: String): String {
return url.split('.').last()
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WaifuViewHolder {
return WaifuViewHolder(
CarouselViewWaifuBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CarouselViewWaifuBinding.inflate(LayoutInflater.from(parent.context), parent, false),
imageLoader,
context,
lifecycleScope
)
}

Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/one/njk/sao/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package one.njk.sao.di

import android.content.Context
import coil.ImageLoader
import coil.decode.ImageDecoderDecoder
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import one.njk.sao.network.WaifuApiService
import retrofit2.Retrofit
Expand Down Expand Up @@ -35,4 +39,18 @@ object AppModule {

return retrofit.create(WaifuApiService::class.java)
}

@Provides
@Singleton
fun provideImageLoader(
@ApplicationContext context: Context
): ImageLoader {
// customizing add GIF Decoder
val imageLoader = ImageLoader.Builder(context)
.components {
add(ImageDecoderDecoder.Factory())
}.build()

return imageLoader
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/one/njk/sao/viewmodels/ArtsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import coil.ImageLoader
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -57,7 +58,8 @@ val nsfwCategories = listOf("waifu", "neko", "trap", "blowjob")

@HiltViewModel
class ArtsViewModel @Inject constructor(
private val waifuApiService: WaifuApiService
private val waifuApiService: WaifuApiService,
val imageLoader: ImageLoader
) : ViewModel() {
// TODO: 0 handle network disconnect crash
// TODO: 3 Survive process deaths (using savedstate handle & lifecycle methods)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/layout/carousel_view_waifu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<variable
name="waifu"
type="one.njk.sao.models.Waifu" />
<variable
name="imageLoader"
type="coil.ImageLoader" />
</data>

<com.google.android.material.carousel.MaskableFrameLayout
Expand All @@ -26,6 +29,7 @@
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:imageUrl="@{waifu.url}"
app:imageLoader="@{imageLoader}"
tools:src="@tools:sample/backgrounds/scenic" />
</com.google.android.material.carousel.MaskableFrameLayout>
</layout>
6 changes: 6 additions & 0 deletions app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="share_cache"
path="share_cache/" />
</paths>
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dagger-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", versi
dagger-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger"}
dagger-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" }
coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil = { module = "io.coil-kt:coil-base", version.ref = "coil" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil"}
recycler-view = { module = "androidx.recyclerview:recyclerview", version.ref = "recycler-view" }
retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
Expand Down

0 comments on commit d9b0ac7

Please sign in to comment.