hltb example: backlink hltb result to game list

This commit is contained in:
Wouter Groeneveld 2021-08-17 20:44:27 +02:00
parent 5b5810c35d
commit adde1e8e70
12 changed files with 94 additions and 26 deletions

View File

@ -4,15 +4,26 @@ import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.view.View
import androidx.core.view.isVisible
import java.net.URL
import kotlin.math.roundToInt
fun Uri.toBitmap(activity: Activity): Bitmap {
return BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this))
}
fun Uri.toBitmap(activity: Activity): Bitmap
= BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this))
fun URL.downloadAsImage(): Bitmap =
BitmapFactory.decodeStream(this.openConnection().getInputStream())
fun Bitmap.scaleToWidth(width: Int): Bitmap {
val aspectRatio = this.width.toFloat() / this.height.toFloat()
val height = (width / aspectRatio).roundToInt()
return Bitmap.createScaledBitmap(this, width, height, false)
}
fun View.ensureVisible() {
if(!this.isVisible) {
this.visibility = View.VISIBLE
}
}

View File

@ -10,7 +10,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import be.kuleuven.howlongtobeat.databinding.FragmentGamelistBinding
import be.kuleuven.howlongtobeat.model.Game
import be.kuleuven.howlongtobeat.model.GameRepository
import be.kuleuven.howlongtobeat.model.room.GameRepositoryRoomImpl
class GameListFragment : Fragment(R.layout.fragment_gamelist) {
@ -28,7 +27,7 @@ class GameListFragment : Fragment(R.layout.fragment_gamelist) {
): View? {
binding = FragmentGamelistBinding.inflate(layoutInflater)
main = activity as MainActivity
gameRepository = GameRepositoryRoomImpl(main.applicationContext)
gameRepository = GameRepository.defaultImpl(main.applicationContext)
loadGames()
adapter = GameListAdapter(gameList)

View File

@ -1,12 +1,12 @@
package be.kuleuven.howlongtobeat
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.findFragment
import androidx.recyclerview.widget.RecyclerView
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
import kotlinx.coroutines.Dispatchers
@ -19,8 +19,11 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
inner class HltbResultsViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView)
private lateinit var parentFragment: HltbResultsFragment
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HltbResultsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_hltbresult, parent, false)
parentFragment = parent.findFragment()
return HltbResultsViewHolder(view)
}
@ -28,6 +31,10 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
val itm = items[position]
holder.itemView.apply {
setOnClickListener {
parentFragment.addResultToGameLibrary(itm)
}
findViewById<TextView>(R.id.txtHltbItemResult).text = itm.toString()
val boxArtView = findViewById<ImageView>(R.id.imgHltbItemResult)
@ -35,7 +42,7 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
MainScope().launch{
var art: Bitmap? = null
withContext(Dispatchers.IO) {
art = BitmapFactory.decodeStream(itm.boxartUrl().openConnection().getInputStream())
art = itm.boxartUrl().downloadAsImage()
}
withContext(Dispatchers.Main) {
boxArtView.setImageBitmap(art)

View File

@ -5,13 +5,18 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import be.kuleuven.howlongtobeat.databinding.FragmentHltbresultsBinding
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
import be.kuleuven.howlongtobeat.model.Game
import be.kuleuven.howlongtobeat.model.GameRepository
import com.google.android.material.snackbar.Snackbar
class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
private lateinit var binding: FragmentHltbresultsBinding
private lateinit var adapter: HltbResultsAdapter
private lateinit var gameRepository: GameRepository
override fun onCreateView(
inflater: LayoutInflater,
@ -22,10 +27,18 @@ class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
val resultFromLoadingFragment = arguments?.getSerializable(HowLongToBeatResult.RESULT) as List<HowLongToBeatResult>
gameRepository = GameRepository.defaultImpl(requireContext())
adapter = HltbResultsAdapter(resultFromLoadingFragment)
binding.rvHltbResult.adapter = adapter
binding.rvHltbResult.layoutManager = LinearLayoutManager(this.context)
return binding.root
}
fun addResultToGameLibrary(hltbResult: HowLongToBeatResult) {
gameRepository.save(Game(hltbResult))
Snackbar.make(requireView(), "Added ${hltbResult.title} to library!", Snackbar.LENGTH_LONG).show()
findNavController().navigate(R.id.action_hltbResultsFragment_to_gameListFragment)
}
}

View File

@ -0,0 +1,7 @@
package be.kuleuven.howlongtobeat
import android.graphics.Bitmap
interface ImageRecognizer {
suspend fun recognizeCartCode(image: Bitmap): String?
}

View File

@ -12,7 +12,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import androidx.core.content.PermissionChecker
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import be.kuleuven.howlongtobeat.cartridges.Cartridge
@ -26,12 +25,11 @@ 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 cartRepo: CartridgesRepository
private lateinit var visionClient: GoogleVisionClient
private lateinit var imageRecognizer: ImageRecognizer
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
private lateinit var cameraActivityResult: ActivityResultLauncher<Uri>
@ -49,7 +47,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
main = activity as MainActivity
cartRepo = CartridgesRepository.fromAsset(main.applicationContext)
visionClient = GoogleVisionClient()
imageRecognizer = GoogleVisionClient()
hltbClient = HLTBClient(main.applicationContext)
cameraActivityResult = registerForActivityResult(ActivityResultContracts.TakePicture(), this::cameraSnapTaken)
@ -77,11 +75,12 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
}
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
progress("Unleashing Google Vision on the pic...")
val cartCode = visionClient.findCartCodeViaGoogleVision(pic)
/*
progress("Recognizing game cart from picture...")
val cartCode = imageRecognizer.recognizeCartCode(pic)
if (cartCode == null) {
errorInProgress("Unable to find a code in your pic. Retry?")
errorInProgress("No cart code in your pic found. Retry?")
return
}
@ -89,16 +88,19 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
val foundCart = cartRepo.find(cartCode)
if (foundCart == Cartridge.UNKNOWN_CART) {
errorInProgress("$cartCode is an unknown game cartridge. Retry?")
errorInProgress("$cartCode is an unknown game cart. Retry?")
return
}
progress("Valid cart code $cartCode, looking in HLTB...")
*/
val foundCart = Cartridge("DMG", "Super Mario Land", "DMG-something")
hltbClient.find(foundCart.title) {
Snackbar.make(requireView(), "Found ${it.size} game(s) for cart $cartCode", Snackbar.LENGTH_LONG).show()
Snackbar.make(requireView(), "Found ${it.size} game(s) for cart ${foundCart.code}", Snackbar.LENGTH_LONG).show()
// TODO wat als geen hltb results gevonden?
val bundle = bundleOf(HowLongToBeatResult.RESULT to it, HowLongToBeatResult.CODE to cartCode)
val bundle = bundleOf(HowLongToBeatResult.RESULT to it)
findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle)
}
}
@ -123,6 +125,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
}
private fun createNewTempCameraFile() {
// a <Provider/> should be present in the manifest file.
val tempFile = File.createTempFile("hltbCameraSnap", ".png", main.cacheDir).apply {
createNewFile()
deleteOnExit()
@ -136,9 +139,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
}
private fun progress(msg: String) {
if(!binding.indeterminateBar.isVisible) {
binding.indeterminateBar.visibility = View.VISIBLE
}
binding.indeterminateBar.ensureVisible()
binding.txtLoading.text = msg
}

View File

@ -1,6 +1,7 @@
package be.kuleuven.howlongtobeat.google
import android.graphics.Bitmap
import be.kuleuven.howlongtobeat.ImageRecognizer
import be.kuleuven.howlongtobeat.asEncodedGoogleVisionImage
import be.kuleuven.howlongtobeat.cartridges.Cartridge
import com.google.api.client.http.javanet.NetHttpTransport
@ -14,7 +15,7 @@ import com.google.api.services.vision.v1.model.Feature
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class GoogleVisionClient {
class GoogleVisionClient : ImageRecognizer {
// TODO encrypt and store externally: https://cloud.google.com/docs/authentication/api-keys?hl=en&visit_id=637642790375688006-1838986332&rd=1
private val vision = Vision.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance(), null)
@ -22,7 +23,7 @@ class GoogleVisionClient {
.setApplicationName("How Long To Beat")
.build()
suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
private suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
var response: BatchAnnotateImagesResponse
withContext(Dispatchers.IO) {
val sml2Data = cameraSnap.asEncodedGoogleVisionImage()
@ -51,4 +52,6 @@ class GoogleVisionClient {
}.firstOrNull()
return gbId?.description ?: null
}
override suspend fun recognizeCartCode(image: Bitmap): String? = findCartCodeViaGoogleVision(image)
}

View File

@ -7,7 +7,6 @@ import java.net.URL
data class HowLongToBeatResult(val title: String, val howlong: Double, val boxart: String = "") : java.io.Serializable {
companion object {
const val RESULT = "HowLongToBeatResult"
const val CODE = "CartCode"
}
fun hasBoxart(): Boolean = boxart.startsWith(HLTBClient.DOMAIN)

View File

@ -3,6 +3,7 @@ package be.kuleuven.howlongtobeat.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
@Entity
data class Game(
@ -10,6 +11,10 @@ data class Game(
@ColumnInfo(name = "is_done") var isDone: Boolean = false,
@PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable {
constructor(result: HowLongToBeatResult) : this(result.title)
// TODO more columns (platform? hours, paths of images?)
fun check() {
isDone = true
}

View File

@ -1,8 +1,21 @@
package be.kuleuven.howlongtobeat.model
import android.content.Context
import be.kuleuven.howlongtobeat.model.room.GameRepositoryRoomImpl
interface GameRepository {
companion object {
/**
* This makes it easier to switch between implementations if needed and does not expose the RoomImpl
* Dependency Injection is the better alternative.
*/
fun defaultImpl(appContext: Context): GameRepository = GameRepositoryRoomImpl(appContext)
}
fun load(): List<Game>
fun save(items: List<Game>)
fun save(game: Game)
fun overwrite(items: List<Game>)
}

View File

@ -20,7 +20,13 @@ class GameRepositoryRoomImpl(appContext: Context) :
override fun load(): List<Game> = dao.query()
override fun save(items: List<Game>) {
override fun save(game: Game) {
db.runInTransaction {
dao.insert(listOf(game))
}
}
override fun overwrite(items: List<Game>) {
// You'll learn more about transactions in the database course in the 3rd academic year.
db.runInTransaction {
dao.deleteAll()

View File

@ -23,5 +23,9 @@
<fragment
android:id="@+id/hltbResultsFragment"
android:name="be.kuleuven.howlongtobeat.HltbResultsFragment"
android:label="HltbResultsFragment" />
android:label="HltbResultsFragment" >
<action
android:id="@+id/action_hltbResultsFragment_to_gameListFragment"
app:destination="@id/gameListFragment" />
</fragment>
</navigation>