summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-04-03 05:01:54 -0400
committerGravatar bunnei2023-06-03 00:05:49 -0700
commit9d7a60346f46c02dccc5f19702c7a997676faa2e (patch)
treedd8e36bb1d53c46eb3401f6914aac8bd9d74f1a6 /src/android
parentandroid: Adjust game icon loading (diff)
downloadyuzu-9d7a60346f46c02dccc5f19702c7a997676faa2e.tar.gz
yuzu-9d7a60346f46c02dccc5f19702c7a997676faa2e.tar.xz
yuzu-9d7a60346f46c02dccc5f19702c7a997676faa2e.zip
android: Remove game database
The content provider + database solution was excessive and is now replaced with the simple file checks from before but turned into an array list held within a viewmodel.
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/build.gradle4
-rw-r--r--src/android/app/src/main/AndroidManifest.xml17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt179
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt263
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt130
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt18
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt45
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt33
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt24
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt72
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt16
18 files changed, 154 insertions, 773 deletions
diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle
index 22f2d4b80..a82d2706b 100644
--- a/src/android/app/build.gradle
+++ b/src/android/app/build.gradle
@@ -140,10 +140,6 @@ dependencies {
140 implementation "io.coil-kt:coil:2.2.2" 140 implementation "io.coil-kt:coil:2.2.2"
141 implementation 'androidx.core:core-splashscreen:1.0.0' 141 implementation 'androidx.core:core-splashscreen:1.0.0'
142 implementation 'androidx.window:window:1.0.0' 142 implementation 'androidx.window:window:1.0.0'
143
144 // Allows FRP-style asynchronous operations in Android.
145 implementation 'io.reactivex:rxandroid:1.2.1'
146 implementation 'com.nononsenseapps:filepicker:4.2.1'
147 implementation 'org.ini4j:ini4j:0.5.4' 143 implementation 'org.ini4j:ini4j:0.5.4'
148 implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 144 implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
149 implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' 145 implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index a5c063d52..18539af80 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -66,23 +66,6 @@
66 <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/> 66 <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
67 67
68 <provider 68 <provider
69 android:name="org.yuzu.yuzu_emu.model.GameProvider"
70 android:authorities="${applicationId}.provider"
71 android:enabled="true"
72 android:exported="false">
73 </provider>
74
75 <provider
76 android:name="androidx.core.content.FileProvider"
77 android:authorities="${applicationId}.filesprovider"
78 android:exported="false"
79 android:grantUriPermissions="true">
80 <meta-data
81 android:name="android.support.FILE_PROVIDER_PATHS"
82 android:resource="@xml/nnf_provider_paths" />
83 </provider>
84
85 <provider
86 android:name=".features.DocumentProvider" 69 android:name=".features.DocumentProvider"
87 android:authorities="${applicationId}.user" 70 android:authorities="${applicationId}.user"
88 android:grantUriPermissions="true" 71 android:grantUriPermissions="true"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index a0c5c5c25..f81b4da40 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -7,7 +7,6 @@ import android.app.Application
7import android.app.NotificationChannel 7import android.app.NotificationChannel
8import android.app.NotificationManager 8import android.app.NotificationManager
9import android.content.Context 9import android.content.Context
10import org.yuzu.yuzu_emu.model.GameDatabase
11import org.yuzu.yuzu_emu.utils.DirectoryInitialization 10import org.yuzu.yuzu_emu.utils.DirectoryInitialization
12import org.yuzu.yuzu_emu.utils.DocumentsTree 11import org.yuzu.yuzu_emu.utils.DocumentsTree
13import org.yuzu.yuzu_emu.utils.GpuDriverHelper 12import org.yuzu.yuzu_emu.utils.GpuDriverHelper
@@ -45,12 +44,9 @@ class YuzuApplication : Application() {
45 44
46 // TODO(bunnei): Disable notifications until we support app suspension. 45 // TODO(bunnei): Disable notifications until we support app suspension.
47 //createNotificationChannel(); 46 //createNotificationChannel();
48 databaseHelper = GameDatabase(this)
49 } 47 }
50 48
51 companion object { 49 companion object {
52 var databaseHelper: GameDatabase? = null
53
54 @JvmField 50 @JvmField
55 var documentsTree: DocumentsTree? = null 51 var documentsTree: DocumentsTree? = null
56 lateinit var application: YuzuApplication 52 lateinit var application: YuzuApplication
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index 7a0969a55..024676185 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -3,11 +3,8 @@
3 3
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.database.Cursor
7import android.database.DataSetObserver
8import android.graphics.Bitmap 6import android.graphics.Bitmap
9import android.graphics.BitmapFactory 7import android.graphics.BitmapFactory
10import android.net.Uri
11import android.view.LayoutInflater 8import android.view.LayoutInflater
12import android.view.View 9import android.view.View
13import android.view.ViewGroup 10import android.view.ViewGroup
@@ -16,7 +13,6 @@ import androidx.appcompat.app.AppCompatActivity
16import androidx.lifecycle.lifecycleScope 13import androidx.lifecycle.lifecycleScope
17import androidx.recyclerview.widget.RecyclerView 14import androidx.recyclerview.widget.RecyclerView
18import coil.load 15import coil.load
19import com.google.android.material.color.MaterialColors
20import kotlinx.coroutines.Dispatchers 16import kotlinx.coroutines.Dispatchers
21import kotlinx.coroutines.launch 17import kotlinx.coroutines.launch
22import kotlinx.coroutines.withContext 18import kotlinx.coroutines.withContext
@@ -25,31 +21,16 @@ import org.yuzu.yuzu_emu.R
25import org.yuzu.yuzu_emu.databinding.CardGameBinding 21import org.yuzu.yuzu_emu.databinding.CardGameBinding
26import org.yuzu.yuzu_emu.activities.EmulationActivity 22import org.yuzu.yuzu_emu.activities.EmulationActivity
27import org.yuzu.yuzu_emu.model.Game 23import org.yuzu.yuzu_emu.model.Game
28import org.yuzu.yuzu_emu.model.GameDatabase 24import kotlin.collections.ArrayList
29import org.yuzu.yuzu_emu.utils.Log
30import org.yuzu.yuzu_emu.viewholders.GameViewHolder
31import java.util.*
32import java.util.stream.Stream
33 25
34/** 26/**
35 * This adapter gets its information from a database Cursor. This fact, paired with the usage of 27 * This adapter gets its information from a database Cursor. This fact, paired with the usage of
36 * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) 28 * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
37 * large dataset. 29 * large dataset.
38 */ 30 */
39class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapter<GameViewHolder>(), 31class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) :
32 RecyclerView.Adapter<GameAdapter.GameViewHolder>(),
40 View.OnClickListener { 33 View.OnClickListener {
41 private var cursor: Cursor? = null
42 private val observer: GameDataSetObserver?
43 private var isDatasetValid = false
44
45 /**
46 * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
47 * display no data until a Cursor is supplied by a CursorLoader.
48 */
49 init {
50 observer = GameDataSetObserver()
51 }
52
53 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { 34 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
54 // Create a new view. 35 // Create a new view.
55 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) 36 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context))
@@ -60,131 +41,55 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte
60 } 41 }
61 42
62 override fun onBindViewHolder(holder: GameViewHolder, position: Int) { 43 override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
63 if (isDatasetValid) { 44 holder.bind(games[position])
64 if (cursor!!.moveToPosition(position)) {
65 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
66 val game = Game(
67 cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE),
68 cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
69 cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS),
70 cursor!!.getString(GameDatabase.GAME_COLUMN_PATH),
71 cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID),
72 cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
73 )
74 holder.game = game
75
76 holder.binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
77 activity.lifecycleScope.launch {
78 val bitmap = decodeGameIcon(game.path)
79 holder.binding.imageGameScreen.load(bitmap) {
80 error(R.drawable.no_icon)
81 crossfade(true)
82 }
83 }
84
85 holder.binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
86 holder.binding.textGameCaption.text = game.company
87
88 if (game.company.isEmpty()) {
89 holder.binding.textGameCaption.visibility = View.GONE
90 }
91
92 val backgroundColorId =
93 if (isValidGame(holder.game.path)) R.attr.colorSurface else R.attr.colorErrorContainer
94 val itemView = holder.itemView
95 itemView.setBackgroundColor(
96 MaterialColors.getColor(
97 itemView,
98 backgroundColorId
99 )
100 )
101 } else {
102 Log.error("[GameAdapter] Can't bind view; Cursor is not valid.")
103 }
104 } else {
105 Log.error("[GameAdapter] Can't bind view; dataset is not valid.")
106 }
107 } 45 }
108 46
109 override fun getItemCount(): Int { 47 override fun getItemCount(): Int {
110 if (isDatasetValid && cursor != null) { 48 return games.size
111 return cursor!!.count
112 }
113 Log.error("[GameAdapter] Dataset is not valid.")
114 return 0
115 } 49 }
116 50
117 /** 51 /**
118 * Return the contents of the _id column for a given row. 52 * Launches the game that was clicked on.
119 * 53 *
120 * @param position The row for which Android wants an ID. 54 * @param view The card representing the game the user wants to play.
121 * @return A valid ID from the database, or 0 if not available.
122 */ 55 */
123 override fun getItemId(position: Int): Long { 56 override fun onClick(view: View) {
124 if (isDatasetValid && cursor != null) { 57 val holder = view.tag as GameViewHolder
125 if (cursor!!.moveToPosition(position)) { 58 EmulationActivity.launch((view.context as AppCompatActivity), holder.game)
126 return cursor!!.getLong(GameDatabase.COLUMN_DB_ID)
127 }
128 }
129 Log.error("[GameAdapter] Dataset is not valid.")
130 return 0
131 } 59 }
132 60
133 /** 61 inner class GameViewHolder(val binding: CardGameBinding) :
134 * Tell Android whether or not each item in the dataset has a stable identifier. 62 RecyclerView.ViewHolder(binding.root) {
135 * Which it does, because it's a database, so always tell Android 'true'. 63 lateinit var game: Game
136 *
137 * @param hasStableIds ignored.
138 */
139 override fun setHasStableIds(hasStableIds: Boolean) {
140 super.setHasStableIds(true)
141 }
142 64
143 /** 65 init {
144 * When a load is finished, call this to replace the existing data with the newly-loaded 66 itemView.tag = this
145 * data.
146 *
147 * @param cursor The newly-loaded Cursor.
148 */
149 fun swapCursor(cursor: Cursor) {
150 // Sanity check.
151 if (cursor === this.cursor) {
152 return
153 } 67 }
154 68
155 // Before getting rid of the old cursor, disassociate it from the Observer. 69 fun bind(game: Game) {
156 val oldCursor = this.cursor 70 this.game = game
157 if (oldCursor != null && observer != null) { 71
158 oldCursor.unregisterDataSetObserver(observer) 72 binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
159 } 73 activity.lifecycleScope.launch {
160 this.cursor = cursor 74 val bitmap = decodeGameIcon(game.path)
161 isDatasetValid = if (this.cursor != null) { 75 binding.imageGameScreen.load(bitmap) {
162 // Attempt to associate the new Cursor with the Observer. 76 error(R.drawable.no_icon)
163 if (observer != null) { 77 crossfade(true)
164 this.cursor!!.registerDataSetObserver(observer) 78 }
165 } 79 }
166 true
167 } else {
168 false
169 }
170 notifyDataSetChanged()
171 }
172 80
173 /** 81 binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
174 * Launches the game that was clicked on. 82 binding.textGameCaption.text = game.company
175 *
176 * @param view The card representing the game the user wants to play.
177 */
178 override fun onClick(view: View) {
179 val holder = view.tag as GameViewHolder
180 EmulationActivity.launch((view.context as AppCompatActivity), holder.game)
181 }
182 83
183 private fun isValidGame(path: String): Boolean { 84 if (game.company.isEmpty()) {
184 return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz") 85 binding.textGameCaption.visibility = View.GONE
185 .noneMatch { suffix: String? ->
186 path.lowercase(Locale.getDefault()).endsWith(suffix!!)
187 } 86 }
87 }
88 }
89
90 fun swapData(games: ArrayList<Game>) {
91 this.games = games
92 notifyDataSetChanged()
188 } 93 }
189 94
190 private fun decodeGameIcon(uri: String): Bitmap? { 95 private fun decodeGameIcon(uri: String): Bitmap? {
@@ -196,18 +101,4 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte
196 BitmapFactory.Options() 101 BitmapFactory.Options()
197 ) 102 )
198 } 103 }
199
200 private inner class GameDataSetObserver : DataSetObserver() {
201 override fun onChanged() {
202 super.onChanged()
203 isDatasetValid = true
204 notifyDataSetChanged()
205 }
206
207 override fun onInvalidated() {
208 super.onInvalidated()
209 isDatasetValid = false
210 notifyDataSetChanged()
211 }
212 }
213} 104}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
index 91f6c5d75..db494e40f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -3,11 +3,8 @@
3 3
4package org.yuzu.yuzu_emu.model 4package org.yuzu.yuzu_emu.model
5 5
6import android.content.ContentValues
7import android.database.Cursor
8import android.os.Parcelable 6import android.os.Parcelable
9import kotlinx.parcelize.Parcelize 7import kotlinx.parcelize.Parcelize
10import java.nio.file.Paths
11import java.util.HashSet 8import java.util.HashSet
12 9
13@Parcelize 10@Parcelize
@@ -23,40 +20,5 @@ class Game(
23 val extensions: Set<String> = HashSet( 20 val extensions: Set<String> = HashSet(
24 listOf(".xci", ".nsp", ".nca", ".nro") 21 listOf(".xci", ".nsp", ".nca", ".nro")
25 ) 22 )
26
27 @JvmStatic
28 fun asContentValues(
29 title: String?,
30 description: String?,
31 regions: String?,
32 path: String?,
33 gameId: String,
34 company: String?
35 ): ContentValues {
36 var realGameId = gameId
37 val values = ContentValues()
38 if (realGameId.isEmpty()) {
39 // Homebrew, etc. may not have a game ID, use filename as a unique identifier
40 realGameId = Paths.get(path).fileName.toString()
41 }
42 values.put(GameDatabase.KEY_GAME_TITLE, title)
43 values.put(GameDatabase.KEY_GAME_DESCRIPTION, description)
44 values.put(GameDatabase.KEY_GAME_REGIONS, regions)
45 values.put(GameDatabase.KEY_GAME_PATH, path)
46 values.put(GameDatabase.KEY_GAME_ID, realGameId)
47 values.put(GameDatabase.KEY_GAME_COMPANY, company)
48 return values
49 }
50
51 fun fromCursor(cursor: Cursor): Game {
52 return Game(
53 cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
54 cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
55 cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
56 cursor.getString(GameDatabase.GAME_COLUMN_PATH),
57 cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
58 cursor.getString(GameDatabase.GAME_COLUMN_CAPTION)
59 )
60 }
61 } 23 }
62} 24}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt
deleted file mode 100644
index c66183516..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt
+++ /dev/null
@@ -1,263 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import android.content.Context
7import android.database.Cursor
8import android.database.sqlite.SQLiteDatabase
9import android.database.sqlite.SQLiteOpenHelper
10import android.net.Uri
11import org.yuzu.yuzu_emu.NativeLibrary
12import org.yuzu.yuzu_emu.utils.FileUtil
13import org.yuzu.yuzu_emu.utils.Log
14import rx.Observable
15import rx.Subscriber
16import java.io.File
17import java.util.*
18
19/**
20 * A helper class that provides several utilities simplifying interaction with
21 * the SQLite database.
22 */
23class GameDatabase(private val context: Context) :
24 SQLiteOpenHelper(context, "games.db", null, DB_VERSION) {
25 override fun onCreate(database: SQLiteDatabase) {
26 Log.debug("[GameDatabase] GameDatabase - Creating database...")
27 execSqlAndLog(database, SQL_CREATE_GAMES)
28 execSqlAndLog(database, SQL_CREATE_FOLDERS)
29 }
30
31 override fun onDowngrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
32 Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..")
33 execSqlAndLog(database, SQL_DELETE_FOLDERS)
34 execSqlAndLog(database, SQL_CREATE_FOLDERS)
35 execSqlAndLog(database, SQL_DELETE_GAMES)
36 execSqlAndLog(database, SQL_CREATE_GAMES)
37 }
38
39 override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
40 Log.info(
41 "[GameDatabase] Upgrading database from schema version $oldVersion to $newVersion"
42 )
43
44 // Delete all the games
45 execSqlAndLog(database, SQL_DELETE_GAMES)
46 execSqlAndLog(database, SQL_CREATE_GAMES)
47 }
48
49 fun resetDatabase(database: SQLiteDatabase) {
50 execSqlAndLog(database, SQL_DELETE_FOLDERS)
51 execSqlAndLog(database, SQL_CREATE_FOLDERS)
52 execSqlAndLog(database, SQL_DELETE_GAMES)
53 execSqlAndLog(database, SQL_CREATE_GAMES)
54 }
55
56 fun scanLibrary(database: SQLiteDatabase) {
57 // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
58 val fileCursor = database.query(
59 TABLE_NAME_GAMES,
60 null, // Get all columns.
61 null, // Get all rows.
62 null,
63 null, // No grouping.
64 null,
65 null
66 ) // Order of games is irrelevant.
67
68 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
69 fileCursor.moveToPosition(-1)
70 while (fileCursor.moveToNext()) {
71 val gamePath = fileCursor.getString(GAME_COLUMN_PATH)
72 val game = File(gamePath)
73 if (!game.exists()) {
74 database.delete(
75 TABLE_NAME_GAMES,
76 "$KEY_DB_ID = ?",
77 arrayOf(fileCursor.getLong(COLUMN_DB_ID).toString())
78 )
79 }
80 }
81
82 // Get a cursor listing all the folders the user has added to the library.
83 val folderCursor = database.query(
84 TABLE_NAME_FOLDERS,
85 null, // Get all columns.
86 null, // Get all rows.
87 null,
88 null, // No grouping.
89 null,
90 null
91 ) // Order of folders is irrelevant.
92
93
94 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
95 folderCursor.moveToPosition(-1)
96
97 // Iterate through all results of the DB query (i.e. all folders in the library.)
98 while (folderCursor.moveToNext()) {
99 val folderPath = folderCursor.getString(FOLDER_COLUMN_PATH)
100 val folderUri = Uri.parse(folderPath)
101 // If the folder is empty because it no longer exists, remove it from the library.
102 if (FileUtil.listFiles(context, folderUri).isEmpty()) {
103 Log.error(
104 "[GameDatabase] Folder no longer exists. Removing from the library: $folderPath"
105 )
106 database.delete(
107 TABLE_NAME_FOLDERS,
108 "$KEY_DB_ID = ?",
109 arrayOf(folderCursor.getLong(COLUMN_DB_ID).toString())
110 )
111 }
112 addGamesRecursive(database, folderUri, Game.extensions, 3)
113 }
114 fileCursor.close()
115 folderCursor.close()
116 database.close()
117 }
118
119 private fun addGamesRecursive(
120 database: SQLiteDatabase,
121 parent: Uri,
122 allowedExtensions: Set<String>,
123 depth: Int
124 ) {
125 if (depth <= 0)
126 return
127
128 // Ensure keys are loaded so that ROM metadata can be decrypted.
129 NativeLibrary.ReloadKeys()
130 val children = FileUtil.listFiles(context, parent)
131 for (file in children) {
132 if (file.isDirectory) {
133 addGamesRecursive(database, file.uri, Game.extensions, depth - 1)
134 } else {
135 val filename = file.uri.toString()
136 val extensionStart = filename.lastIndexOf('.')
137 if (extensionStart > 0) {
138 val fileExtension = filename.substring(extensionStart)
139
140 // Check that the file has an extension we care about before trying to read out of it.
141 if (allowedExtensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
142 attemptToAddGame(database, filename)
143 }
144 }
145 }
146 }
147 }
148 // Pass the result cursor to the consumer.
149
150 // Tell the consumer we're done; it will unsubscribe implicitly.
151 val games: Observable<Cursor?>
152 get() = Observable.create { subscriber: Subscriber<in Cursor?> ->
153 Log.info("[GameDatabase] Reading games list...")
154 val database = readableDatabase
155 val resultCursor = database.query(
156 TABLE_NAME_GAMES,
157 null,
158 null,
159 null,
160 null,
161 null,
162 "$KEY_GAME_TITLE ASC"
163 )
164
165 // Pass the result cursor to the consumer.
166 subscriber.onNext(resultCursor)
167
168 // Tell the consumer we're done; it will unsubscribe implicitly.
169 subscriber.onCompleted()
170 }
171
172 private fun execSqlAndLog(database: SQLiteDatabase, sql: String) {
173 Log.verbose("[GameDatabase] Executing SQL: $sql")
174 database.execSQL(sql)
175 }
176
177 companion object {
178 const val COLUMN_DB_ID = 0
179 const val GAME_COLUMN_PATH = 1
180 const val GAME_COLUMN_TITLE = 2
181 const val GAME_COLUMN_DESCRIPTION = 3
182 const val GAME_COLUMN_REGIONS = 4
183 const val GAME_COLUMN_GAME_ID = 5
184 const val GAME_COLUMN_CAPTION = 6
185 const val FOLDER_COLUMN_PATH = 1
186 const val KEY_DB_ID = "_id"
187 const val KEY_GAME_PATH = "path"
188 const val KEY_GAME_TITLE = "title"
189 const val KEY_GAME_DESCRIPTION = "description"
190 const val KEY_GAME_REGIONS = "regions"
191 const val KEY_GAME_ID = "game_id"
192 const val KEY_GAME_COMPANY = "company"
193 const val KEY_FOLDER_PATH = "path"
194 const val TABLE_NAME_FOLDERS = "folders"
195 const val TABLE_NAME_GAMES = "games"
196 private const val DB_VERSION = 2
197 private const val TYPE_PRIMARY = " INTEGER PRIMARY KEY"
198 private const val TYPE_INTEGER = " INTEGER"
199 private const val TYPE_STRING = " TEXT"
200 private const val CONSTRAINT_UNIQUE = " UNIQUE"
201 private const val SEPARATOR = ", "
202 private const val SQL_CREATE_GAMES = ("CREATE TABLE " + TABLE_NAME_GAMES + "("
203 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
204 + KEY_GAME_PATH + TYPE_STRING + SEPARATOR
205 + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
206 + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
207 + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
208 + KEY_GAME_ID + TYPE_STRING + SEPARATOR
209 + KEY_GAME_COMPANY + TYPE_STRING + ")")
210 private const val SQL_CREATE_FOLDERS = ("CREATE TABLE " + TABLE_NAME_FOLDERS + "("
211 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
212 + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")")
213 private const val SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS $TABLE_NAME_FOLDERS"
214 private const val SQL_DELETE_GAMES = "DROP TABLE IF EXISTS $TABLE_NAME_GAMES"
215 private fun attemptToAddGame(database: SQLiteDatabase, filePath: String) {
216 var name = NativeLibrary.GetTitle(filePath)
217
218 // If the game's title field is empty, use the filename.
219 if (name.isEmpty()) {
220 name = filePath.substring(filePath.lastIndexOf("/") + 1)
221 }
222 var gameId = NativeLibrary.GetGameId(filePath)
223
224 // If the game's ID field is empty, use the filename without extension.
225 if (gameId.isEmpty()) {
226 gameId = filePath.substring(
227 filePath.lastIndexOf("/") + 1,
228 filePath.lastIndexOf(".")
229 )
230 }
231 val game = Game.asContentValues(
232 name,
233 NativeLibrary.GetDescription(filePath).replace("\n", " "),
234 NativeLibrary.GetRegions(filePath),
235 filePath,
236 gameId,
237 NativeLibrary.GetCompany(filePath)
238 )
239
240 // Try to update an existing game first.
241 val rowsMatched = database.update(
242 TABLE_NAME_GAMES, // Which table to update.
243 game, // The values to fill the row with.
244 "$KEY_GAME_ID = ?", arrayOf(
245 game.getAsString(
246 KEY_GAME_ID
247 )
248 )
249 )
250 // The ? in WHERE clause is replaced with this,
251 // which is provided as an array because there
252 // could potentially be more than one argument.
253
254 // If update fails, insert a new game instead.
255 if (rowsMatched == 0) {
256 Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE))
257 database.insert(TABLE_NAME_GAMES, null, game)
258 } else {
259 Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE))
260 }
261 }
262 }
263}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt
deleted file mode 100644
index 5d8e5cc54..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt
+++ /dev/null
@@ -1,130 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import android.content.ContentProvider
7import android.content.ContentValues
8import android.database.Cursor
9import android.database.sqlite.SQLiteDatabase
10import android.net.Uri
11import org.yuzu.yuzu_emu.BuildConfig
12import org.yuzu.yuzu_emu.utils.Log
13
14/**
15 * Provides an interface allowing Activities to interact with the SQLite database.
16 * CRUD methods in this class can be called by Activities using getContentResolver().
17 */
18class GameProvider : ContentProvider() {
19 private var mDbHelper: GameDatabase? = null
20 override fun onCreate(): Boolean {
21 Log.info("[GameProvider] Creating Content Provider...")
22 mDbHelper = GameDatabase(context!!)
23 return true
24 }
25
26 override fun query(
27 uri: Uri,
28 projection: Array<String>?,
29 selection: String?,
30 selectionArgs: Array<String>?,
31 sortOrder: String?
32 ): Cursor? {
33 Log.info("[GameProvider] Querying URI: $uri")
34 val db = mDbHelper!!.readableDatabase
35 val table = uri.lastPathSegment
36 if (table == null) {
37 Log.error("[GameProvider] Badly formatted URI: $uri")
38 return null
39 }
40 val cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder)
41 cursor.setNotificationUri(context!!.contentResolver, uri)
42 return cursor
43 }
44
45 override fun getType(uri: Uri): String? {
46 Log.verbose("[GameProvider] Getting MIME type for URI: $uri")
47 val lastSegment = uri.lastPathSegment
48 if (lastSegment == null) {
49 Log.error("[GameProvider] Badly formatted URI: $uri")
50 return null
51 }
52 if (lastSegment == GameDatabase.TABLE_NAME_FOLDERS) {
53 return MIME_TYPE_FOLDER
54 } else if (lastSegment == GameDatabase.TABLE_NAME_GAMES) {
55 return MIME_TYPE_GAME
56 }
57 Log.error("[GameProvider] Unknown MIME type for URI: $uri")
58 return null
59 }
60
61 override fun insert(uri: Uri, values: ContentValues?): Uri {
62 var realUri = uri
63 Log.info("[GameProvider] Inserting row at URI: $realUri")
64 val database = mDbHelper!!.writableDatabase
65 val table = realUri.lastPathSegment
66 if (table != null) {
67 if (table == RESET_LIBRARY) {
68 mDbHelper!!.resetDatabase(database)
69 return realUri
70 }
71 if (table == REFRESH_LIBRARY) {
72 Log.info(
73 "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."
74 )
75 mDbHelper!!.scanLibrary(database)
76 return realUri
77 }
78 val id =
79 database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE)
80
81 // If insertion was successful...
82 if (id > 0) {
83 // If we just added a folder, add its contents to the game list.
84 if (table == GameDatabase.TABLE_NAME_FOLDERS) {
85 mDbHelper!!.scanLibrary(database)
86 }
87
88 // Notify the UI that its contents should be refreshed.
89 context!!.contentResolver.notifyChange(realUri, null)
90 realUri = Uri.withAppendedPath(realUri, id.toString())
91 } else {
92 Log.error("[GameProvider] Row already exists: $realUri id: $id")
93 }
94 } else {
95 Log.error("[GameProvider] Badly formatted URI: $realUri")
96 }
97 database.close()
98 return realUri
99 }
100
101 override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
102 Log.error("[GameProvider] Delete operations unsupported. URI: $uri")
103 return 0
104 }
105
106 override fun update(
107 uri: Uri, values: ContentValues?, selection: String?,
108 selectionArgs: Array<String>?
109 ): Int {
110 Log.error("[GameProvider] Update operations unsupported. URI: $uri")
111 return 0
112 }
113
114 companion object {
115 const val REFRESH_LIBRARY = "refresh"
116 const val RESET_LIBRARY = "reset"
117 private const val AUTHORITY = "content://${BuildConfig.APPLICATION_ID}.provider"
118
119 @JvmField
120 val URI_FOLDER: Uri = Uri.parse("$AUTHORITY/${GameDatabase.TABLE_NAME_FOLDERS}/")
121
122 @JvmField
123 val URI_REFRESH: Uri = Uri.parse("$AUTHORITY/$REFRESH_LIBRARY/")
124
125 @JvmField
126 val URI_RESET: Uri = Uri.parse("$AUTHORITY/$RESET_LIBRARY/")
127 const val MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.yuzu.folder"
128 const val MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.yuzu.game"
129 }
130}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
new file mode 100644
index 000000000..fde99f1a2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -0,0 +1,18 @@
1package org.yuzu.yuzu_emu.model
2
3import androidx.lifecycle.LiveData
4import androidx.lifecycle.MutableLiveData
5import androidx.lifecycle.ViewModel
6
7class GamesViewModel : ViewModel() {
8 private val _games = MutableLiveData<ArrayList<Game>>()
9 val games: LiveData<ArrayList<Game>> get() = _games
10
11 init {
12 _games.value = ArrayList()
13 }
14
15 fun setGames(games: ArrayList<Game>) {
16 _games.value = games
17 }
18}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 441c9da9c..4885bc4bc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat
18import androidx.core.view.WindowInsetsCompat 18import androidx.core.view.WindowInsetsCompat
19import androidx.core.view.updatePadding 19import androidx.core.view.updatePadding
20import androidx.lifecycle.lifecycleScope 20import androidx.lifecycle.lifecycleScope
21import androidx.preference.PreferenceManager
21import com.google.android.material.dialog.MaterialAlertDialogBuilder 22import com.google.android.material.dialog.MaterialAlertDialogBuilder
22import kotlinx.coroutines.Dispatchers 23import kotlinx.coroutines.Dispatchers
23import kotlinx.coroutines.launch 24import kotlinx.coroutines.launch
@@ -28,7 +29,6 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
28import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 29import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
29import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 30import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
30import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity 31import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
31import org.yuzu.yuzu_emu.model.GameProvider
32import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment 32import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment
33import org.yuzu.yuzu_emu.utils.* 33import org.yuzu.yuzu_emu.utils.*
34import java.io.IOException 34import java.io.IOException
@@ -82,11 +82,6 @@ class MainActivity : AppCompatActivity(), MainView {
82 ) 82 )
83 } 83 }
84 84
85 override fun onResume() {
86 super.onResume()
87 presenter.addDirIfNeeded(AddDirectoryHelper(this))
88 }
89
90 override fun onCreateOptionsMenu(menu: Menu): Boolean { 85 override fun onCreateOptionsMenu(menu: Menu): Boolean {
91 menuInflater.inflate(R.menu.menu_game_grid, menu) 86 menuInflater.inflate(R.menu.menu_game_grid, menu)
92 return true 87 return true
@@ -99,11 +94,6 @@ class MainActivity : AppCompatActivity(), MainView {
99 binding.toolbarMain.subtitle = version 94 binding.toolbarMain.subtitle = version
100 } 95 }
101 96
102 override fun refresh() {
103 contentResolver.insert(GameProvider.URI_REFRESH, null)
104 refreshFragment()
105 }
106
107 override fun launchSettingsActivity(menuTag: String) { 97 override fun launchSettingsActivity(menuTag: String) {
108 SettingsActivity.launch(this, menuTag, "") 98 SettingsActivity.launch(this, menuTag, "")
109 } 99 }
@@ -185,10 +175,9 @@ class MainActivity : AppCompatActivity(), MainView {
185 175
186 // When a new directory is picked, we currently will reset the existing games 176 // When a new directory is picked, we currently will reset the existing games
187 // database. This effectively means that only one game directory is supported. 177 // database. This effectively means that only one game directory is supported.
188 // TODO(bunnei): Consider fixing this in the future, or removing code for this. 178 PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
189 contentResolver.insert(GameProvider.URI_RESET, null) 179 .putString(GameHelper.KEY_GAME_PATH, result.toString())
190 // Add the new directory 180 .apply()
191 presenter.onDirectorySelected(result.toString())
192 } 181 }
193 182
194 private val getProdKey = 183 private val getProdKey =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt
index 554542e05..a7ddc333f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt
@@ -5,17 +5,12 @@ package org.yuzu.yuzu_emu.ui.main
5 5
6import org.yuzu.yuzu_emu.BuildConfig 6import org.yuzu.yuzu_emu.BuildConfig
7import org.yuzu.yuzu_emu.R 7import org.yuzu.yuzu_emu.R
8import org.yuzu.yuzu_emu.YuzuApplication
9import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 8import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
10import org.yuzu.yuzu_emu.utils.AddDirectoryHelper
11 9
12class MainPresenter(private val view: MainView) { 10class MainPresenter(private val view: MainView) {
13 private var dirToAdd: String? = null
14
15 fun onCreate() { 11 fun onCreate() {
16 val versionName = BuildConfig.VERSION_NAME 12 val versionName = BuildConfig.VERSION_NAME
17 view.setVersionString(versionName) 13 view.setVersionString(versionName)
18 refreshGameList()
19 } 14 }
20 15
21 private fun launchFileListActivity(request: Int) { 16 private fun launchFileListActivity(request: Int) {
@@ -48,23 +43,6 @@ class MainPresenter(private val view: MainView) {
48 return false 43 return false
49 } 44 }
50 45
51 fun addDirIfNeeded(helper: AddDirectoryHelper) {
52 if (dirToAdd != null) {
53 helper.addDirectory(dirToAdd) { view.refresh() }
54 dirToAdd = null
55 }
56 }
57
58 fun onDirectorySelected(dir: String?) {
59 dirToAdd = dir
60 }
61
62 private fun refreshGameList() {
63 val databaseHelper = YuzuApplication.databaseHelper
64 databaseHelper!!.scanLibrary(databaseHelper.writableDatabase)
65 view.refresh()
66 }
67
68 companion object { 46 companion object {
69 const val REQUEST_ADD_DIRECTORY = 1 47 const val REQUEST_ADD_DIRECTORY = 1
70 const val REQUEST_INSTALL_KEYS = 2 48 const val REQUEST_INSTALL_KEYS = 2
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt
index dab3abe7c..4dc9f0706 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt
@@ -17,10 +17,7 @@ interface MainView {
17 */ 17 */
18 fun setVersionString(version: String) 18 fun setVersionString(version: String)
19 19
20 /**
21 * Tell the view to refresh its contents.
22 */
23 fun refresh()
24 fun launchSettingsActivity(menuTag: String) 20 fun launchSettingsActivity(menuTag: String)
21
25 fun launchFileListActivity(request: Int) 22 fun launchFileListActivity(request: Int)
26} 23}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt
index dcfac1b2a..443a37cd2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt
@@ -3,7 +3,6 @@
3 3
4package org.yuzu.yuzu_emu.ui.platform 4package org.yuzu.yuzu_emu.ui.platform
5 5
6import android.database.Cursor
7import android.os.Bundle 6import android.os.Bundle
8import android.view.LayoutInflater 7import android.view.LayoutInflater
9import android.view.View 8import android.view.View
@@ -13,36 +12,40 @@ import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsCompat 12import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding 13import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
15import androidx.lifecycle.ViewModelProvider
16import com.google.android.material.color.MaterialColors 16import com.google.android.material.color.MaterialColors
17import org.yuzu.yuzu_emu.R 17import org.yuzu.yuzu_emu.R
18import org.yuzu.yuzu_emu.YuzuApplication
19import org.yuzu.yuzu_emu.adapters.GameAdapter 18import org.yuzu.yuzu_emu.adapters.GameAdapter
20import org.yuzu.yuzu_emu.databinding.FragmentGridBinding 19import org.yuzu.yuzu_emu.databinding.FragmentGridBinding
21import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager 20import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
21import org.yuzu.yuzu_emu.model.GamesViewModel
22import org.yuzu.yuzu_emu.utils.GameHelper
22 23
23class PlatformGamesFragment : Fragment(), PlatformGamesView { 24class PlatformGamesFragment : Fragment() {
24 private val presenter = PlatformGamesPresenter(this)
25
26 private var _binding: FragmentGridBinding? = null 25 private var _binding: FragmentGridBinding? = null
27 private val binding get() = _binding!! 26 private val binding get() = _binding!!
28 27
28 private lateinit var gamesViewModel: GamesViewModel
29
29 override fun onCreateView( 30 override fun onCreateView(
30 inflater: LayoutInflater, 31 inflater: LayoutInflater,
31 container: ViewGroup?, 32 container: ViewGroup?,
32 savedInstanceState: Bundle? 33 savedInstanceState: Bundle?
33 ): View { 34 ): View {
34 presenter.onCreateView()
35 _binding = FragmentGridBinding.inflate(inflater) 35 _binding = FragmentGridBinding.inflate(inflater)
36 return binding.root 36 return binding.root
37 } 37 }
38 38
39 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 39 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
40 gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java]
41
40 binding.gridGames.apply { 42 binding.gridGames.apply {
41 layoutManager = AutofitGridLayoutManager( 43 layoutManager = AutofitGridLayoutManager(
42 requireContext(), 44 requireContext(),
43 requireContext().resources.getDimensionPixelSize(R.dimen.card_width) 45 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
44 ) 46 )
45 adapter = GameAdapter(requireActivity() as AppCompatActivity) 47 adapter =
48 GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!)
46 } 49 }
47 50
48 // Add swipe down to refresh gesture 51 // Add swipe down to refresh gesture
@@ -59,7 +62,19 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView {
59 MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) 62 MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary)
60 ) 63 )
61 64
65 gamesViewModel.games.observe(viewLifecycleOwner) {
66 (binding.gridGames.adapter as GameAdapter).swapData(it)
67 updateTextView()
68 }
69
62 setInsets() 70 setInsets()
71
72 refresh()
73 }
74
75 override fun onResume() {
76 super.onResume()
77 refresh()
63 } 78 }
64 79
65 override fun onDestroyView() { 80 override fun onDestroyView() {
@@ -67,20 +82,8 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView {
67 _binding = null 82 _binding = null
68 } 83 }
69 84
70 override fun refresh() { 85 fun refresh() {
71 val databaseHelper = YuzuApplication.databaseHelper 86 gamesViewModel.setGames(GameHelper.getGames())
72 databaseHelper!!.scanLibrary(databaseHelper.writableDatabase)
73 presenter.refresh()
74 updateTextView()
75 }
76
77 override fun showGames(games: Cursor) {
78 if (_binding == null)
79 return
80
81 if (binding.gridGames.adapter != null) {
82 (binding.gridGames.adapter as GameAdapter).swapCursor(games)
83 }
84 updateTextView() 87 updateTextView()
85 } 88 }
86 89
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt
deleted file mode 100644
index 0b9da5f57..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt
+++ /dev/null
@@ -1,33 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.platform
5
6import android.database.Cursor
7import org.yuzu.yuzu_emu.YuzuApplication
8import org.yuzu.yuzu_emu.utils.Log
9import rx.android.schedulers.AndroidSchedulers
10import rx.schedulers.Schedulers
11
12class PlatformGamesPresenter(private val view: PlatformGamesView) {
13 fun onCreateView() {
14 loadGames()
15 }
16
17 fun refresh() {
18 Log.debug("[PlatformGamesPresenter] : Refreshing...")
19 loadGames()
20 }
21
22 private fun loadGames() {
23 Log.debug("[PlatformGamesPresenter] : Loading games...")
24 val databaseHelper = YuzuApplication.databaseHelper
25 databaseHelper!!.games
26 .subscribeOn(Schedulers.io())
27 .observeOn(AndroidSchedulers.mainThread())
28 .subscribe { games: Cursor? ->
29 Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...")
30 view.showGames(games!!)
31 }
32 }
33}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt
deleted file mode 100644
index 4132e560f..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt
+++ /dev/null
@@ -1,24 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.platform
5
6import android.database.Cursor
7
8/**
9 * Abstraction for a screen representing a single platform's games.
10 */
11interface PlatformGamesView {
12 /**
13 * Tell the view to refresh its contents.
14 */
15 fun refresh()
16
17 /**
18 * To be called when an asynchronous database read completes. Passes the
19 * result, in this case a [Cursor], to the view.
20 *
21 * @param games A Cursor containing the games read from the database.
22 */
23 fun showGames(games: Cursor)
24}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt
deleted file mode 100644
index 9041a7bee..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt
+++ /dev/null
@@ -1,8 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6interface Action1<T> {
7 fun call(t: T?)
8}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt
deleted file mode 100644
index acec7ba5e..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt
+++ /dev/null
@@ -1,30 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.content.AsyncQueryHandler
7import android.content.ContentValues
8import android.content.Context
9import android.net.Uri
10import org.yuzu.yuzu_emu.model.GameDatabase
11import org.yuzu.yuzu_emu.model.GameProvider
12
13class AddDirectoryHelper(private val context: Context) {
14 fun addDirectory(dir: String?, onAddUnit: () -> Unit) {
15 val handler: AsyncQueryHandler = object : AsyncQueryHandler(context.contentResolver) {
16 override fun onInsertComplete(token: Int, cookie: Any?, uri: Uri) {
17 onAddUnit.invoke()
18 }
19 }
20
21 val file = ContentValues()
22 file.put(GameDatabase.KEY_FOLDER_PATH, dir)
23 handler.startInsert(
24 0, // We don't need to identify this call to the handler
25 null, // We don't need to pass additional data to the handler
26 GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
27 file
28 )
29 }
30}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
new file mode 100644
index 000000000..6dfd8b7f8
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -0,0 +1,72 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.net.Uri
7import androidx.preference.PreferenceManager
8import org.yuzu.yuzu_emu.NativeLibrary
9import org.yuzu.yuzu_emu.YuzuApplication
10import org.yuzu.yuzu_emu.model.Game
11import java.util.*
12import kotlin.collections.ArrayList
13
14object GameHelper {
15 const val KEY_GAME_PATH = "game_path"
16
17 fun getGames(): ArrayList<Game> {
18 val games = ArrayList<Game>()
19 val context = YuzuApplication.appContext
20 val gamesDir =
21 PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
22 val gamesUri = Uri.parse(gamesDir)
23
24 // Ensure keys are loaded so that ROM metadata can be decrypted.
25 NativeLibrary.ReloadKeys()
26
27 val children = FileUtil.listFiles(context, gamesUri)
28 for (file in children) {
29 if (!file.isDirectory) {
30 val filename = file.uri.toString()
31 val extensionStart = filename.lastIndexOf('.')
32 if (extensionStart > 0) {
33 val fileExtension = filename.substring(extensionStart)
34
35 // Check that the file has an extension we care about before trying to read out of it.
36 if (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
37 games.add(getGame(filename))
38 }
39 }
40 }
41 }
42
43 return games
44 }
45
46 private fun getGame(filePath: String): Game {
47 var name = NativeLibrary.GetTitle(filePath)
48
49 // If the game's title field is empty, use the filename.
50 if (name.isEmpty()) {
51 name = filePath.substring(filePath.lastIndexOf("/") + 1)
52 }
53 var gameId = NativeLibrary.GetGameId(filePath)
54
55 // If the game's ID field is empty, use the filename without extension.
56 if (gameId.isEmpty()) {
57 gameId = filePath.substring(
58 filePath.lastIndexOf("/") + 1,
59 filePath.lastIndexOf(".")
60 )
61 }
62
63 return Game(
64 name,
65 NativeLibrary.GetDescription(filePath).replace("\n", " "),
66 NativeLibrary.GetRegions(filePath),
67 filePath,
68 gameId,
69 NativeLibrary.GetCompany(filePath)
70 )
71 }
72}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt
deleted file mode 100644
index 51420448f..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt
+++ /dev/null
@@ -1,16 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.viewholders
5
6import androidx.recyclerview.widget.RecyclerView
7import org.yuzu.yuzu_emu.databinding.CardGameBinding
8import org.yuzu.yuzu_emu.model.Game
9
10class GameViewHolder(val binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) {
11 lateinit var game: Game
12
13 init {
14 itemView.tag = this
15 }
16}