diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt b/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt index 77aaac5..4ca38a7 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt index 46b9bb6..8df3b12 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt @@ -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) diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt index c2bd817..06ce954 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt @@ -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) : 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) : Recycle val itm = items[position] holder.itemView.apply { + setOnClickListener { + parentFragment.addResultToGameLibrary(itm) + } + findViewById(R.id.txtHltbItemResult).text = itm.toString() val boxArtView = findViewById(R.id.imgHltbItemResult) @@ -35,7 +42,7 @@ class HltbResultsAdapter(private val items: List) : 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) diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt index 212349d..ffbf936 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt @@ -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 + 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) + } } \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/ImageRecognizer.kt b/app/src/main/java/be/kuleuven/howlongtobeat/ImageRecognizer.kt new file mode 100644 index 0000000..c2f5679 --- /dev/null +++ b/app/src/main/java/be/kuleuven/howlongtobeat/ImageRecognizer.kt @@ -0,0 +1,7 @@ +package be.kuleuven.howlongtobeat + +import android.graphics.Bitmap + +interface ImageRecognizer { + suspend fun recognizeCartCode(image: Bitmap): String? +} \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt index 3a20a2a..724474d 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt @@ -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 private lateinit var cameraActivityResult: ActivityResultLauncher @@ -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 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 } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/google/GoogleVisionClient.kt b/app/src/main/java/be/kuleuven/howlongtobeat/google/GoogleVisionClient.kt index cbe61e3..e71f39b 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/google/GoogleVisionClient.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/google/GoogleVisionClient.kt @@ -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) } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt index 475a2bc..23e5e7f 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt @@ -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) diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt b/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt index 365263a..d6b4818 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt @@ -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 } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt b/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt index e21fe64..b5e6620 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt @@ -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 - fun save(items: List) + fun save(game: Game) + + fun overwrite(items: List) } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt b/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt index e19a700..905edeb 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt @@ -20,7 +20,13 @@ class GameRepositoryRoomImpl(appContext: Context) : override fun load(): List = dao.query() - override fun save(items: List) { + override fun save(game: Game) { + db.runInTransaction { + dao.insert(listOf(game)) + } + } + + override fun overwrite(items: List) { // You'll learn more about transactions in the database course in the 3rd academic year. db.runInTransaction { dao.deleteAll() diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 12211e4..e312296 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -23,5 +23,9 @@ + android:label="HltbResultsFragment" > + + \ No newline at end of file