howlongtobeat-android/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt

175 lines
6.7 KiB
Kotlin

package be.kuleuven.howlongtobeat
import android.Manifest
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import androidx.core.content.PermissionChecker
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import be.kuleuven.howlongtobeat.cartridges.CartridgeFinderViaDuckDuckGo
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepositoryGekkioFi
import be.kuleuven.howlongtobeat.cartridges.findFirstCartridgeForRepos
import be.kuleuven.howlongtobeat.databinding.FragmentLoadingBinding
import be.kuleuven.howlongtobeat.google.GoogleVisionClient
import be.kuleuven.howlongtobeat.hltb.HLTBClient
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.io.File
class LoadingFragment : Fragment(R.layout.fragment_loading) {
private lateinit var hltbClient: HLTBClient
private lateinit var cartRepos: List<CartridgesRepository>
private lateinit var imageRecognizer: ImageRecognizer
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
private lateinit var cameraActivityResult: ActivityResultLauncher<Uri>
private lateinit var main: MainActivity
private lateinit var binding: FragmentLoadingBinding
private var snapshot: Uri? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLoadingBinding.inflate(layoutInflater)
main = activity as MainActivity
// If we fail to find info in the first repo, it falls back to the second one: a (scraped) DuckDuckGo search.
cartRepos = listOf(
CartridgesRepositoryGekkioFi.fromAsset(main.applicationContext),
CartridgeFinderViaDuckDuckGo(main.applicationContext)
)
imageRecognizer = GoogleVisionClient()
hltbClient = HLTBClient(main.applicationContext)
cameraActivityResult = registerForActivityResult(ActivityResultContracts.TakePicture(), this::cameraSnapTaken)
cameraPermissionActivityResult = registerForActivityResult(ActivityResultContracts.RequestPermission(), this::cameraPermissionAcceptedOrDenied)
binding.btnRetryAfterLoading.setOnClickListener {
tryToMakeCameraSnap()
}
return binding.root
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
val inProgress = savedInstanceState?.getBoolean("inprogress") ?: false
if(!inProgress) {
// Don't do this in onCreateView, things go awry if you rotate the smartphone!
tryToMakeCameraSnap()
}
super.onViewStateRestored(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.run {
putBoolean("inprogress", snapshot != null)
}
super.onSaveInstanceState(outState)
}
private fun cameraSnapTaken(succeeded: Boolean) {
if(!succeeded || snapshot == null) {
errorInProgress("Photo could not be saved, try again?")
return
}
progress("Scaling image for upload...")
val bitmap = snapshot!!.toBitmap(requireContentResolver()).scaleToWidth(1600)
MainScope().launch{
try {
findGameBasedOnCameraSnap(bitmap)
} catch (errorDuringFind: UnableToFindGameException) {
errorInProgress("${errorDuringFind.message}\nRetry?")
}
}
}
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
var picToAnalyze = pic
// Uncomment this line if you want to stub out camera pictures
// picToAnalyze = BitmapFactory.decodeResource(resources, R.drawable.sml2)
progress("Recognizing game cart from picture...")
val cartCode = imageRecognizer.recognizeCartCode(picToAnalyze)
?: throw UnableToFindGameException("No cart code in your pic found")
progress("Found cart code $cartCode\nLooking in DBs for matching game...")
val foundCart = findFirstCartridgeForRepos(cartCode, cartRepos)
?: throw UnableToFindGameException("$cartCode is an unknown game cart.")
progress("Valid cart code: $cartCode\n Looking in HLTB for ${foundCart.title}...")
val hltbResults = hltbClient.find(foundCart)
?: throw UnableToFindGameException("HLTB does not know ${foundCart.title}")
Snackbar.make(requireView(), "Found ${hltbResults.size} game(s) for cart ${foundCart.code}", Snackbar.LENGTH_LONG).show()
val bundle = bundleOf(
HowLongToBeatResult.RESULT to hltbResults,
HowLongToBeatResult.SNAPSHOT_URI to snapshot.toString()
)
findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle)
}
private fun cameraPermissionAcceptedOrDenied(succeeded: Boolean) {
if(succeeded) {
makeCameraSnap()
} else {
errorInProgress("Camera permission required!")
}
}
private fun tryToMakeCameraSnap() {
binding.btnRetryAfterLoading.hide()
progress("Making snapshot with camera...")
if(PermissionChecker.checkSelfPermission(main, Manifest.permission.CAMERA) != PermissionChecker.PERMISSION_GRANTED) {
cameraPermissionActivityResult.launch(Manifest.permission.CAMERA)
} else {
makeCameraSnap()
}
}
private fun createNewTempCameraFile() {
// a <Provider/> should be present in the manifest file.
val tempFile = File.createTempFile("hltbCameraSnap", ".png", main.cacheDir).apply {
createNewFile()
deleteOnExit()
}
snapshot = FileProvider.getUriForFile(main.applicationContext, "${BuildConfig.APPLICATION_ID}.provider", tempFile)
}
private fun makeCameraSnap() {
createNewTempCameraFile()
cameraActivityResult.launch(snapshot)
}
private fun progress(msg: String) {
binding.indeterminateBar.ensureVisible()
binding.txtLoading.text = msg
}
private fun errorInProgress(msg: String) {
snapshot = null
progress(msg)
binding.indeterminateBar.visibility = View.GONE
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
binding.btnRetryAfterLoading.show()
}
}