summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar t8952024-01-19 01:06:10 -0500
committerGravatar t8952024-01-19 17:09:36 -0500
commitd79d4d5986e952000624edb244839fd1996be4ae (patch)
treecfced11ce3f3b72bca6bca920c17f7f5082ebbee /src
parentfrontend_common: Add content manager utility functions (diff)
downloadyuzu-d79d4d5986e952000624edb244839fd1996be4ae.tar.gz
yuzu-d79d4d5986e952000624edb244839fd1996be4ae.tar.xz
yuzu-d79d4d5986e952000624edb244839fd1996be4ae.zip
android: Use callback to update progress bar dialogs
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt)44
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt101
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt80
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt5
-rw-r--r--src/android/app/src/main/res/layout/dialog_progress_bar.xml30
10 files changed, 236 insertions, 121 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
index 816336820..b63ece9a4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -156,22 +156,22 @@ class AddonsFragment : Fragment() {
156 descriptionId = R.string.invalid_directory_description 156 descriptionId = R.string.invalid_directory_description
157 ) 157 )
158 if (isValid) { 158 if (isValid) {
159 IndeterminateProgressDialogFragment.newInstance( 159 ProgressDialogFragment.newInstance(
160 requireActivity(), 160 requireActivity(),
161 R.string.installing_game_content, 161 R.string.installing_game_content,
162 false 162 false
163 ) { 163 ) { progressCallback, _ ->
164 val parentDirectoryName = externalAddonDirectory.name 164 val parentDirectoryName = externalAddonDirectory.name
165 val internalAddonDirectory = 165 val internalAddonDirectory =
166 File(args.game.addonDir + parentDirectoryName) 166 File(args.game.addonDir + parentDirectoryName)
167 try { 167 try {
168 externalAddonDirectory.copyFilesTo(internalAddonDirectory) 168 externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
169 } catch (_: Exception) { 169 } catch (_: Exception) {
170 return@newInstance errorMessage 170 return@newInstance errorMessage
171 } 171 }
172 addonViewModel.refreshAddons() 172 addonViewModel.refreshAddons()
173 return@newInstance getString(R.string.addon_installed_successfully) 173 return@newInstance getString(R.string.addon_installed_successfully)
174 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 174 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
175 } else { 175 } else {
176 errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) 176 errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
177 } 177 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index 9dabb9c41..6c758d80b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() {
173 return@registerForActivityResult 173 return@registerForActivityResult
174 } 174 }
175 175
176 IndeterminateProgressDialogFragment.newInstance( 176 ProgressDialogFragment.newInstance(
177 requireActivity(), 177 requireActivity(),
178 R.string.installing_driver, 178 R.string.installing_driver,
179 false 179 false
180 ) { 180 ) { _, _ ->
181 val driverPath = 181 val driverPath =
182 "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}" 182 "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
183 val driverFile = File(driverPath) 183 val driverFile = File(driverPath)
@@ -213,6 +213,6 @@ class DriverManagerFragment : Fragment() {
213 } 213 }
214 } 214 }
215 return@newInstance Any() 215 return@newInstance Any()
216 }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) 216 }.show(childFragmentManager, ProgressDialogFragment.TAG)
217 } 217 }
218} 218}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index b04d1208f..83a845434 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil
44import org.yuzu.yuzu_emu.utils.GameIconUtils 44import org.yuzu.yuzu_emu.utils.GameIconUtils
45import org.yuzu.yuzu_emu.utils.GpuDriverHelper 45import org.yuzu.yuzu_emu.utils.GpuDriverHelper
46import org.yuzu.yuzu_emu.utils.MemoryUtil 46import org.yuzu.yuzu_emu.utils.MemoryUtil
47import java.io.BufferedInputStream
48import java.io.BufferedOutputStream 47import java.io.BufferedOutputStream
49import java.io.File 48import java.io.File
50 49
@@ -357,27 +356,17 @@ class GamePropertiesFragment : Fragment() {
357 return@registerForActivityResult 356 return@registerForActivityResult
358 } 357 }
359 358
360 val inputZip = requireContext().contentResolver.openInputStream(result)
361 val savesFolder = File(args.game.saveDir) 359 val savesFolder = File(args.game.saveDir)
362 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") 360 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
363 cacheSaveDir.mkdir() 361 cacheSaveDir.mkdir()
364 362
365 if (inputZip == null) { 363 ProgressDialogFragment.newInstance(
366 Toast.makeText(
367 YuzuApplication.appContext,
368 getString(R.string.fatal_error),
369 Toast.LENGTH_LONG
370 ).show()
371 return@registerForActivityResult
372 }
373
374 IndeterminateProgressDialogFragment.newInstance(
375 requireActivity(), 364 requireActivity(),
376 R.string.save_files_importing, 365 R.string.save_files_importing,
377 false 366 false
378 ) { 367 ) { _, _ ->
379 try { 368 try {
380 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) 369 FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir)
381 val files = cacheSaveDir.listFiles() 370 val files = cacheSaveDir.listFiles()
382 var savesFolderFile: File? = null 371 var savesFolderFile: File? = null
383 if (files != null) { 372 if (files != null) {
@@ -422,7 +411,7 @@ class GamePropertiesFragment : Fragment() {
422 Toast.LENGTH_LONG 411 Toast.LENGTH_LONG
423 ).show() 412 ).show()
424 } 413 }
425 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 414 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
426 } 415 }
427 416
428 /** 417 /**
@@ -436,11 +425,11 @@ class GamePropertiesFragment : Fragment() {
436 return@registerForActivityResult 425 return@registerForActivityResult
437 } 426 }
438 427
439 IndeterminateProgressDialogFragment.newInstance( 428 ProgressDialogFragment.newInstance(
440 requireActivity(), 429 requireActivity(),
441 R.string.save_files_exporting, 430 R.string.save_files_exporting,
442 false 431 false
443 ) { 432 ) { _, _ ->
444 val saveLocation = args.game.saveDir 433 val saveLocation = args.game.saveDir
445 val zipResult = FileUtil.zipFromInternalStorage( 434 val zipResult = FileUtil.zipFromInternalStorage(
446 File(saveLocation), 435 File(saveLocation),
@@ -452,6 +441,6 @@ class GamePropertiesFragment : Fragment() {
452 TaskState.Completed -> getString(R.string.export_success) 441 TaskState.Completed -> getString(R.string.export_success)
453 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) 442 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
454 } 443 }
455 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 444 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
456 } 445 }
457} 446}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 5b4bf2c9f..7df8e6bf4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState
34import org.yuzu.yuzu_emu.ui.main.MainActivity 34import org.yuzu.yuzu_emu.ui.main.MainActivity
35import org.yuzu.yuzu_emu.utils.DirectoryInitialization 35import org.yuzu.yuzu_emu.utils.DirectoryInitialization
36import org.yuzu.yuzu_emu.utils.FileUtil 36import org.yuzu.yuzu_emu.utils.FileUtil
37import java.io.BufferedInputStream
38import java.io.BufferedOutputStream 37import java.io.BufferedOutputStream
39import java.io.File 38import java.io.File
40import java.math.BigInteger 39import java.math.BigInteger
@@ -195,26 +194,20 @@ class InstallableFragment : Fragment() {
195 return@registerForActivityResult 194 return@registerForActivityResult
196 } 195 }
197 196
198 val inputZip = requireContext().contentResolver.openInputStream(result)
199 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") 197 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
200 cacheSaveDir.mkdir() 198 cacheSaveDir.mkdir()
201 199
202 if (inputZip == null) { 200 ProgressDialogFragment.newInstance(
203 Toast.makeText(
204 YuzuApplication.appContext,
205 getString(R.string.fatal_error),
206 Toast.LENGTH_LONG
207 ).show()
208 return@registerForActivityResult
209 }
210
211 IndeterminateProgressDialogFragment.newInstance(
212 requireActivity(), 201 requireActivity(),
213 R.string.save_files_importing, 202 R.string.save_files_importing,
214 false 203 false
215 ) { 204 ) { progressCallback, _ ->
216 try { 205 try {
217 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) 206 FileUtil.unzipToInternalStorage(
207 result.toString(),
208 cacheSaveDir,
209 progressCallback
210 )
218 val files = cacheSaveDir.listFiles() 211 val files = cacheSaveDir.listFiles()
219 var successfulImports = 0 212 var successfulImports = 0
220 var failedImports = 0 213 var failedImports = 0
@@ -287,7 +280,7 @@ class InstallableFragment : Fragment() {
287 Toast.LENGTH_LONG 280 Toast.LENGTH_LONG
288 ).show() 281 ).show()
289 } 282 }
290 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 283 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
291 } 284 }
292 285
293 private val exportSaves = registerForActivityResult( 286 private val exportSaves = registerForActivityResult(
@@ -297,11 +290,11 @@ class InstallableFragment : Fragment() {
297 return@registerForActivityResult 290 return@registerForActivityResult
298 } 291 }
299 292
300 IndeterminateProgressDialogFragment.newInstance( 293 ProgressDialogFragment.newInstance(
301 requireActivity(), 294 requireActivity(),
302 R.string.save_files_exporting, 295 R.string.save_files_exporting,
303 false 296 false
304 ) { 297 ) { _, _ ->
305 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") 298 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
306 cacheSaveDir.mkdir() 299 cacheSaveDir.mkdir()
307 300
@@ -338,6 +331,6 @@ class InstallableFragment : Fragment() {
338 TaskState.Completed -> getString(R.string.export_success) 331 TaskState.Completed -> getString(R.string.export_success)
339 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) 332 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
340 } 333 }
341 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 334 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
342 } 335 }
343} 336}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
index 8847e5531..d201cb80c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
@@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 23import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
24import org.yuzu.yuzu_emu.model.TaskViewModel 24import org.yuzu.yuzu_emu.model.TaskViewModel
25 25
26class IndeterminateProgressDialogFragment : DialogFragment() { 26class ProgressDialogFragment : DialogFragment() {
27 private val taskViewModel: TaskViewModel by activityViewModels() 27 private val taskViewModel: TaskViewModel by activityViewModels()
28 28
29 private lateinit var binding: DialogProgressBarBinding 29 private lateinit var binding: DialogProgressBarBinding
30 30
31 private val PROGRESS_BAR_RESOLUTION = 1000
32
31 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 33 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
32 val titleId = requireArguments().getInt(TITLE) 34 val titleId = requireArguments().getInt(TITLE)
33 val cancellable = requireArguments().getBoolean(CANCELLABLE) 35 val cancellable = requireArguments().getBoolean(CANCELLABLE)
@@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
61 63
62 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 64 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 super.onViewCreated(view, savedInstanceState) 65 super.onViewCreated(view, savedInstanceState)
66 binding.message.isSelected = true
64 viewLifecycleOwner.lifecycleScope.apply { 67 viewLifecycleOwner.lifecycleScope.apply {
65 launch { 68 launch {
66 repeatOnLifecycle(Lifecycle.State.CREATED) { 69 repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
97 } 100 }
98 } 101 }
99 } 102 }
103 launch {
104 repeatOnLifecycle(Lifecycle.State.CREATED) {
105 taskViewModel.progress.collect {
106 if (it != 0.0) {
107 binding.progressBar.apply {
108 isIndeterminate = false
109 progress = (
110 (it / taskViewModel.maxProgress.value) *
111 PROGRESS_BAR_RESOLUTION
112 ).toInt()
113 min = 0
114 max = PROGRESS_BAR_RESOLUTION
115 }
116 }
117 }
118 }
119 }
120 launch {
121 repeatOnLifecycle(Lifecycle.State.CREATED) {
122 taskViewModel.message.collect {
123 if (it.isEmpty()) {
124 binding.message.visibility = View.GONE
125 } else {
126 binding.message.visibility = View.VISIBLE
127 binding.message.text = it
128 }
129 }
130 }
131 }
100 } 132 }
101 } 133 }
102 134
@@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
108 val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) 140 val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
109 negativeButton.setOnClickListener { 141 negativeButton.setOnClickListener {
110 alertDialog.setTitle(getString(R.string.cancelling)) 142 alertDialog.setTitle(getString(R.string.cancelling))
143 binding.progressBar.isIndeterminate = true
111 taskViewModel.setCancelled(true) 144 taskViewModel.setCancelled(true)
112 } 145 }
113 } 146 }
@@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
122 activity: FragmentActivity, 155 activity: FragmentActivity,
123 titleId: Int, 156 titleId: Int,
124 cancellable: Boolean = false, 157 cancellable: Boolean = false,
125 task: suspend () -> Any 158 task: suspend (
126 ): IndeterminateProgressDialogFragment { 159 progressCallback: (max: Long, progress: Long) -> Boolean,
127 val dialog = IndeterminateProgressDialogFragment() 160 messageCallback: (message: String) -> Unit
161 ) -> Any
162 ): ProgressDialogFragment {
163 val dialog = ProgressDialogFragment()
128 val args = Bundle() 164 val args = Bundle()
129 ViewModelProvider(activity)[TaskViewModel::class.java].task = task 165 ViewModelProvider(activity)[TaskViewModel::class.java].task = task
130 args.putInt(TITLE, titleId) 166 args.putInt(TITLE, titleId)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
index e59c95733..4361eb972 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
8import kotlinx.coroutines.Dispatchers 8import kotlinx.coroutines.Dispatchers
9import kotlinx.coroutines.flow.MutableStateFlow 9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.StateFlow 10import kotlinx.coroutines.flow.StateFlow
11import kotlinx.coroutines.flow.asStateFlow
11import kotlinx.coroutines.launch 12import kotlinx.coroutines.launch
12 13
13class TaskViewModel : ViewModel() { 14class TaskViewModel : ViewModel() {
@@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() {
23 val cancelled: StateFlow<Boolean> get() = _cancelled 24 val cancelled: StateFlow<Boolean> get() = _cancelled
24 private val _cancelled = MutableStateFlow(false) 25 private val _cancelled = MutableStateFlow(false)
25 26
26 lateinit var task: suspend () -> Any 27 private val _progress = MutableStateFlow(0.0)
28 val progress = _progress.asStateFlow()
29
30 private val _maxProgress = MutableStateFlow(0.0)
31 val maxProgress = _maxProgress.asStateFlow()
32
33 private val _message = MutableStateFlow("")
34 val message = _message.asStateFlow()
35
36 lateinit var task: suspend (
37 progressCallback: (max: Long, progress: Long) -> Boolean,
38 messageCallback: (message: String) -> Unit
39 ) -> Any
27 40
28 fun clear() { 41 fun clear() {
29 _result.value = Any() 42 _result.value = Any()
30 _isComplete.value = false 43 _isComplete.value = false
31 _isRunning.value = false 44 _isRunning.value = false
32 _cancelled.value = false 45 _cancelled.value = false
46 _progress.value = 0.0
47 _maxProgress.value = 0.0
48 _message.value = ""
33 } 49 }
34 50
35 fun setCancelled(value: Boolean) { 51 fun setCancelled(value: Boolean) {
@@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() {
43 _isRunning.value = true 59 _isRunning.value = true
44 60
45 viewModelScope.launch(Dispatchers.IO) { 61 viewModelScope.launch(Dispatchers.IO) {
46 val res = task() 62 val res = task(
63 { max, progress ->
64 _maxProgress.value = max.toDouble()
65 _progress.value = progress.toDouble()
66 return@task cancelled.value
67 },
68 { message ->
69 _message.value = message
70 }
71 )
47 _result.value = res 72 _result.value = res
48 _isComplete.value = true 73 _isComplete.value = true
49 _isRunning.value = false 74 _isRunning.value = false
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 644289e25..c2cc29961 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
@@ -38,12 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
38import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 38import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
39import org.yuzu.yuzu_emu.features.settings.model.Settings 39import org.yuzu.yuzu_emu.features.settings.model.Settings
40import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment 40import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
41import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment 41import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
43import org.yuzu.yuzu_emu.model.AddonViewModel 43import org.yuzu.yuzu_emu.model.AddonViewModel
44import org.yuzu.yuzu_emu.model.DriverViewModel 44import org.yuzu.yuzu_emu.model.DriverViewModel
45import org.yuzu.yuzu_emu.model.GamesViewModel 45import org.yuzu.yuzu_emu.model.GamesViewModel
46import org.yuzu.yuzu_emu.model.HomeViewModel 46import org.yuzu.yuzu_emu.model.HomeViewModel
47import org.yuzu.yuzu_emu.model.InstallResult
47import org.yuzu.yuzu_emu.model.TaskState 48import org.yuzu.yuzu_emu.model.TaskState
48import org.yuzu.yuzu_emu.model.TaskViewModel 49import org.yuzu.yuzu_emu.model.TaskViewModel
49import org.yuzu.yuzu_emu.utils.* 50import org.yuzu.yuzu_emu.utils.*
@@ -369,26 +370,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
369 return@registerForActivityResult 370 return@registerForActivityResult
370 } 371 }
371 372
372 val inputZip = contentResolver.openInputStream(result)
373 if (inputZip == null) {
374 Toast.makeText(
375 applicationContext,
376 getString(R.string.fatal_error),
377 Toast.LENGTH_LONG
378 ).show()
379 return@registerForActivityResult
380 }
381
382 val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } 373 val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
383 374
384 val firmwarePath = 375 val firmwarePath =
385 File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") 376 File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
386 val cacheFirmwareDir = File("${cacheDir.path}/registered/") 377 val cacheFirmwareDir = File("${cacheDir.path}/registered/")
387 378
388 val task: () -> Any = { 379 ProgressDialogFragment.newInstance(
380 this,
381 R.string.firmware_installing
382 ) { progressCallback, _ ->
389 var messageToShow: Any 383 var messageToShow: Any
390 try { 384 try {
391 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) 385 FileUtil.unzipToInternalStorage(
386 result.toString(),
387 cacheFirmwareDir,
388 progressCallback
389 )
392 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 390 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
393 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 391 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
394 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { 392 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@@ -404,18 +402,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
404 getString(R.string.save_file_imported_success) 402 getString(R.string.save_file_imported_success)
405 } 403 }
406 } catch (e: Exception) { 404 } catch (e: Exception) {
405 Log.error("[MainActivity] Firmware install failed - ${e.message}")
407 messageToShow = getString(R.string.fatal_error) 406 messageToShow = getString(R.string.fatal_error)
408 } finally { 407 } finally {
409 cacheFirmwareDir.deleteRecursively() 408 cacheFirmwareDir.deleteRecursively()
410 } 409 }
411 messageToShow 410 messageToShow
412 } 411 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
413
414 IndeterminateProgressDialogFragment.newInstance(
415 this,
416 R.string.firmware_installing,
417 task = task
418 ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
419 } 412 }
420 413
421 val getAmiiboKey = 414 val getAmiiboKey =
@@ -474,11 +467,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
474 return@registerForActivityResult 467 return@registerForActivityResult
475 } 468 }
476 469
477 IndeterminateProgressDialogFragment.newInstance( 470 ProgressDialogFragment.newInstance(
478 this@MainActivity, 471 this@MainActivity,
479 R.string.verifying_content, 472 R.string.verifying_content,
480 false 473 false
481 ) { 474 ) { _, _ ->
482 var updatesMatchProgram = true 475 var updatesMatchProgram = true
483 for (document in documents) { 476 for (document in documents) {
484 val valid = NativeLibrary.doesUpdateMatchProgram( 477 val valid = NativeLibrary.doesUpdateMatchProgram(
@@ -501,44 +494,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
501 positiveAction = { homeViewModel.setContentToInstall(documents) } 494 positiveAction = { homeViewModel.setContentToInstall(documents) }
502 ) 495 )
503 } 496 }
504 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 497 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
505 } 498 }
506 499
507 private fun installContent(documents: List<Uri>) { 500 private fun installContent(documents: List<Uri>) {
508 IndeterminateProgressDialogFragment.newInstance( 501 ProgressDialogFragment.newInstance(
509 this@MainActivity, 502 this@MainActivity,
510 R.string.installing_game_content 503 R.string.installing_game_content
511 ) { 504 ) { progressCallback, messageCallback ->
512 var installSuccess = 0 505 var installSuccess = 0
513 var installOverwrite = 0 506 var installOverwrite = 0
514 var errorBaseGame = 0 507 var errorBaseGame = 0
515 var errorExtension = 0 508 var error = 0
516 var errorOther = 0
517 documents.forEach { 509 documents.forEach {
510 messageCallback.invoke(FileUtil.getFilename(it))
518 when ( 511 when (
519 NativeLibrary.installFileToNand( 512 InstallResult.from(
520 it.toString(), 513 NativeLibrary.installFileToNand(
521 FileUtil.getExtension(it) 514 it.toString(),
515 progressCallback
516 )
522 ) 517 )
523 ) { 518 ) {
524 NativeLibrary.InstallFileToNandResult.Success -> { 519 InstallResult.Success -> {
525 installSuccess += 1 520 installSuccess += 1
526 } 521 }
527 522
528 NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { 523 InstallResult.Overwrite -> {
529 installOverwrite += 1 524 installOverwrite += 1
530 } 525 }
531 526
532 NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { 527 InstallResult.BaseInstallAttempted -> {
533 errorBaseGame += 1 528 errorBaseGame += 1
534 } 529 }
535 530
536 NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { 531 InstallResult.Failure -> {
537 errorExtension += 1 532 error += 1
538 }
539
540 else -> {
541 errorOther += 1
542 } 533 }
543 } 534 }
544 } 535 }
@@ -565,7 +556,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
565 ) 556 )
566 installResult.append(separator) 557 installResult.append(separator)
567 } 558 }
568 val errorTotal: Int = errorBaseGame + errorExtension + errorOther 559 val errorTotal: Int = errorBaseGame + error
569 if (errorTotal > 0) { 560 if (errorTotal > 0) {
570 installResult.append(separator) 561 installResult.append(separator)
571 installResult.append( 562 installResult.append(
@@ -582,14 +573,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
582 ) 573 )
583 installResult.append(separator) 574 installResult.append(separator)
584 } 575 }
585 if (errorExtension > 0) { 576 if (error > 0) {
586 installResult.append(separator)
587 installResult.append(
588 getString(R.string.install_game_content_failure_file_extension)
589 )
590 installResult.append(separator)
591 }
592 if (errorOther > 0) {
593 installResult.append( 577 installResult.append(
594 getString(R.string.install_game_content_failure_description) 578 getString(R.string.install_game_content_failure_description)
595 ) 579 )
@@ -608,7 +592,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
608 descriptionString = installResult.toString().trim() 592 descriptionString = installResult.toString().trim()
609 ) 593 )
610 } 594 }
611 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 595 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
612 } 596 }
613 597
614 val exportUserData = registerForActivityResult( 598 val exportUserData = registerForActivityResult(
@@ -618,16 +602,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
618 return@registerForActivityResult 602 return@registerForActivityResult
619 } 603 }
620 604
621 IndeterminateProgressDialogFragment.newInstance( 605 ProgressDialogFragment.newInstance(
622 this, 606 this,
623 R.string.exporting_user_data, 607 R.string.exporting_user_data,
624 true 608 true
625 ) { 609 ) { progressCallback, _ ->
626 val zipResult = FileUtil.zipFromInternalStorage( 610 val zipResult = FileUtil.zipFromInternalStorage(
627 File(DirectoryInitialization.userDirectory!!), 611 File(DirectoryInitialization.userDirectory!!),
628 DirectoryInitialization.userDirectory!!, 612 DirectoryInitialization.userDirectory!!,
629 BufferedOutputStream(contentResolver.openOutputStream(result)), 613 BufferedOutputStream(contentResolver.openOutputStream(result)),
630 taskViewModel.cancelled, 614 progressCallback,
631 compression = false 615 compression = false
632 ) 616 )
633 return@newInstance when (zipResult) { 617 return@newInstance when (zipResult) {
@@ -635,7 +619,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
635 TaskState.Failed -> R.string.export_failed 619 TaskState.Failed -> R.string.export_failed
636 TaskState.Cancelled -> R.string.user_data_export_cancelled 620 TaskState.Cancelled -> R.string.user_data_export_cancelled
637 } 621 }
638 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 622 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
639 } 623 }
640 624
641 val importUserData = 625 val importUserData =
@@ -644,10 +628,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
644 return@registerForActivityResult 628 return@registerForActivityResult
645 } 629 }
646 630
647 IndeterminateProgressDialogFragment.newInstance( 631 ProgressDialogFragment.newInstance(
648 this, 632 this,
649 R.string.importing_user_data 633 R.string.importing_user_data
650 ) { 634 ) { progressCallback, _ ->
651 val checkStream = 635 val checkStream =
652 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) 636 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
653 var isYuzuBackup = false 637 var isYuzuBackup = false
@@ -676,8 +660,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
676 // Copy archive to internal storage 660 // Copy archive to internal storage
677 try { 661 try {
678 FileUtil.unzipToInternalStorage( 662 FileUtil.unzipToInternalStorage(
679 BufferedInputStream(contentResolver.openInputStream(result)), 663 result.toString(),
680 File(DirectoryInitialization.userDirectory!!) 664 File(DirectoryInitialization.userDirectory!!),
665 progressCallback
681 ) 666 )
682 } catch (e: Exception) { 667 } catch (e: Exception) {
683 return@newInstance MessageDialogFragment.newInstance( 668 return@newInstance MessageDialogFragment.newInstance(
@@ -694,6 +679,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
694 driverViewModel.reloadDriverData() 679 driverViewModel.reloadDriverData()
695 680
696 return@newInstance getString(R.string.user_data_import_success) 681 return@newInstance getString(R.string.user_data_import_success)
697 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 682 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
698 } 683 }
699} 684}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index b54a19c65..fc2339f5a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -7,7 +7,6 @@ import android.database.Cursor
7import android.net.Uri 7import android.net.Uri
8import android.provider.DocumentsContract 8import android.provider.DocumentsContract
9import androidx.documentfile.provider.DocumentFile 9import androidx.documentfile.provider.DocumentFile
10import kotlinx.coroutines.flow.StateFlow
11import java.io.BufferedInputStream 10import java.io.BufferedInputStream
12import java.io.File 11import java.io.File
13import java.io.IOException 12import java.io.IOException
@@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
19import org.yuzu.yuzu_emu.model.MinimalDocumentFile 18import org.yuzu.yuzu_emu.model.MinimalDocumentFile
20import org.yuzu.yuzu_emu.model.TaskState 19import org.yuzu.yuzu_emu.model.TaskState
21import java.io.BufferedOutputStream 20import java.io.BufferedOutputStream
21import java.io.OutputStream
22import java.lang.NullPointerException 22import java.lang.NullPointerException
23import java.nio.charset.StandardCharsets 23import java.nio.charset.StandardCharsets
24import java.util.zip.Deflater 24import java.util.zip.Deflater
@@ -283,12 +283,34 @@ object FileUtil {
283 283
284 /** 284 /**
285 * Extracts the given zip file into the given directory. 285 * Extracts the given zip file into the given directory.
286 * @param path String representation of a [Uri] or a typical path delimited by '/'
287 * @param destDir Location to unzip the contents of [path] into
288 * @param progressCallback Lambda that is called with the total number of files and the current
289 * progress through the process. Stops execution as soon as possible if this returns true.
286 */ 290 */
287 @Throws(SecurityException::class) 291 @Throws(SecurityException::class)
288 fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { 292 fun unzipToInternalStorage(
289 ZipInputStream(zipStream).use { zis -> 293 path: String,
294 destDir: File,
295 progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
296 ) {
297 var totalEntries = 0L
298 ZipInputStream(getInputStream(path)).use { zis ->
299 var tempEntry = zis.nextEntry
300 while (tempEntry != null) {
301 tempEntry = zis.nextEntry
302 totalEntries++
303 }
304 }
305
306 var progress = 0L
307 ZipInputStream(getInputStream(path)).use { zis ->
290 var entry: ZipEntry? = zis.nextEntry 308 var entry: ZipEntry? = zis.nextEntry
291 while (entry != null) { 309 while (entry != null) {
310 if (progressCallback.invoke(totalEntries, progress)) {
311 return@use
312 }
313
292 val newFile = File(destDir, entry.name) 314 val newFile = File(destDir, entry.name)
293 val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile 315 val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
294 316
@@ -304,6 +326,7 @@ object FileUtil {
304 newFile.outputStream().use { fos -> zis.copyTo(fos) } 326 newFile.outputStream().use { fos -> zis.copyTo(fos) }
305 } 327 }
306 entry = zis.nextEntry 328 entry = zis.nextEntry
329 progress++
307 } 330 }
308 } 331 }
309 } 332 }
@@ -313,14 +336,15 @@ object FileUtil {
313 * @param inputFile File representation of the item that will be zipped 336 * @param inputFile File representation of the item that will be zipped
314 * @param rootDir Directory containing the inputFile 337 * @param rootDir Directory containing the inputFile
315 * @param outputStream Stream where the zip file will be output 338 * @param outputStream Stream where the zip file will be output
316 * @param cancelled [StateFlow] that reports whether this process has been cancelled 339 * @param progressCallback Lambda that is called with the total number of files and the current
340 * progress through the process. Stops execution as soon as possible if this returns true.
317 * @param compression Disables compression if true 341 * @param compression Disables compression if true
318 */ 342 */
319 fun zipFromInternalStorage( 343 fun zipFromInternalStorage(
320 inputFile: File, 344 inputFile: File,
321 rootDir: String, 345 rootDir: String,
322 outputStream: BufferedOutputStream, 346 outputStream: BufferedOutputStream,
323 cancelled: StateFlow<Boolean>? = null, 347 progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
324 compression: Boolean = true 348 compression: Boolean = true
325 ): TaskState { 349 ): TaskState {
326 try { 350 try {
@@ -330,8 +354,10 @@ object FileUtil {
330 zos.setLevel(Deflater.NO_COMPRESSION) 354 zos.setLevel(Deflater.NO_COMPRESSION)
331 } 355 }
332 356
357 var count = 0L
358 val totalFiles = inputFile.walkTopDown().count().toLong()
333 inputFile.walkTopDown().forEach { file -> 359 inputFile.walkTopDown().forEach { file ->
334 if (cancelled?.value == true) { 360 if (progressCallback.invoke(totalFiles, count)) {
335 return TaskState.Cancelled 361 return TaskState.Cancelled
336 } 362 }
337 363
@@ -343,6 +369,7 @@ object FileUtil {
343 if (file.isFile) { 369 if (file.isFile) {
344 file.inputStream().use { fis -> fis.copyTo(zos) } 370 file.inputStream().use { fis -> fis.copyTo(zos) }
345 } 371 }
372 count++
346 } 373 }
347 } 374 }
348 } 375 }
@@ -356,9 +383,14 @@ object FileUtil {
356 /** 383 /**
357 * Helper function that copies the contents of a DocumentFile folder into a [File] 384 * Helper function that copies the contents of a DocumentFile folder into a [File]
358 * @param file [File] representation of the folder to copy into 385 * @param file [File] representation of the folder to copy into
386 * @param progressCallback Lambda that is called with the total number of files and the current
387 * progress through the process. Stops execution as soon as possible if this returns true.
359 * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa 388 * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
360 */ 389 */
361 fun DocumentFile.copyFilesTo(file: File) { 390 fun DocumentFile.copyFilesTo(
391 file: File,
392 progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
393 ) {
362 file.mkdirs() 394 file.mkdirs()
363 if (!this.isDirectory || !file.isDirectory) { 395 if (!this.isDirectory || !file.isDirectory) {
364 throw IllegalStateException( 396 throw IllegalStateException(
@@ -366,7 +398,13 @@ object FileUtil {
366 ) 398 )
367 } 399 }
368 400
401 var count = 0L
402 val totalFiles = this.listFiles().size.toLong()
369 this.listFiles().forEach { 403 this.listFiles().forEach {
404 if (progressCallback.invoke(totalFiles, count)) {
405 return
406 }
407
370 val newFile = File(file, it.name!!) 408 val newFile = File(file, it.name!!)
371 if (it.isDirectory) { 409 if (it.isDirectory) {
372 newFile.mkdirs() 410 newFile.mkdirs()
@@ -381,6 +419,7 @@ object FileUtil {
381 newFile.outputStream().use { os -> bos.copyTo(os) } 419 newFile.outputStream().use { os -> bos.copyTo(os) }
382 } 420 }
383 } 421 }
422 count++
384 } 423 }
385 } 424 }
386 425
@@ -427,6 +466,18 @@ object FileUtil {
427 } 466 }
428 } 467 }
429 468
469 fun getInputStream(path: String) = if (path.contains("content://")) {
470 Uri.parse(path).inputStream()
471 } else {
472 File(path).inputStream()
473 }
474
475 fun getOutputStream(path: String) = if (path.contains("content://")) {
476 Uri.parse(path).outputStream()
477 } else {
478 File(path).outputStream()
479 }
480
430 @Throws(IOException::class) 481 @Throws(IOException::class)
431 fun getStringFromFile(file: File): String = 482 fun getStringFromFile(file: File): String =
432 String(file.readBytes(), StandardCharsets.UTF_8) 483 String(file.readBytes(), StandardCharsets.UTF_8)
@@ -434,4 +485,19 @@ object FileUtil {
434 @Throws(IOException::class) 485 @Throws(IOException::class)
435 fun getStringFromInputStream(stream: InputStream): String = 486 fun getStringFromInputStream(stream: InputStream): String =
436 String(stream.readBytes(), StandardCharsets.UTF_8) 487 String(stream.readBytes(), StandardCharsets.UTF_8)
488
489 fun DocumentFile.inputStream(): InputStream =
490 YuzuApplication.appContext.contentResolver.openInputStream(uri)!!
491
492 fun DocumentFile.outputStream(): OutputStream =
493 YuzuApplication.appContext.contentResolver.openOutputStream(uri)!!
494
495 fun Uri.inputStream(): InputStream =
496 YuzuApplication.appContext.contentResolver.openInputStream(this)!!
497
498 fun Uri.outputStream(): OutputStream =
499 YuzuApplication.appContext.contentResolver.openOutputStream(this)!!
500
501 fun Uri.asDocumentFile(): DocumentFile? =
502 DocumentFile.fromSingleUri(YuzuApplication.appContext, this)
437} 503}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index a8f9dcc34..81212cbee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
5 5
6import android.net.Uri 6import android.net.Uri
7import android.os.Build 7import android.os.Build
8import java.io.BufferedInputStream
9import java.io.File 8import java.io.File
10import java.io.IOException 9import java.io.IOException
11import org.yuzu.yuzu_emu.NativeLibrary 10import org.yuzu.yuzu_emu.NativeLibrary
@@ -123,7 +122,7 @@ object GpuDriverHelper {
123 // Unzip the driver. 122 // Unzip the driver.
124 try { 123 try {
125 FileUtil.unzipToInternalStorage( 124 FileUtil.unzipToInternalStorage(
126 BufferedInputStream(copiedFile.inputStream()), 125 copiedFile.path,
127 File(driverInstallationPath!!) 126 File(driverInstallationPath!!)
128 ) 127 )
129 } catch (e: SecurityException) { 128 } catch (e: SecurityException) {
@@ -156,7 +155,7 @@ object GpuDriverHelper {
156 // Unzip the driver to the private installation directory 155 // Unzip the driver to the private installation directory
157 try { 156 try {
158 FileUtil.unzipToInternalStorage( 157 FileUtil.unzipToInternalStorage(
159 BufferedInputStream(driver.inputStream()), 158 driver.path,
160 File(driverInstallationPath!!) 159 File(driverInstallationPath!!)
161 ) 160 )
162 } catch (e: SecurityException) { 161 } catch (e: SecurityException) {
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
index 0209ea082..e61aa5294 100644
--- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -1,8 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android" 2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto" 3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/progress_bar"
5 android:layout_width="match_parent" 4 android:layout_width="match_parent"
6 android:layout_height="wrap_content" 5 android:layout_height="wrap_content"
7 android:padding="24dp" 6 android:orientation="vertical">
8 app:trackCornerRadius="4dp" /> 7
8 <com.google.android.material.textview.MaterialTextView
9 android:id="@+id/message"
10 style="@style/TextAppearance.Material3.BodyMedium"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:layout_marginHorizontal="24dp"
14 android:layout_marginTop="12dp"
15 android:layout_marginBottom="6dp"
16 android:ellipsize="marquee"
17 android:marqueeRepeatLimit="marquee_forever"
18 android:requiresFadingEdge="horizontal"
19 android:singleLine="true"
20 android:textAlignment="viewStart"
21 android:visibility="gone" />
22
23 <com.google.android.material.progressindicator.LinearProgressIndicator
24 android:id="@+id/progress_bar"
25 android:layout_width="match_parent"
26 android:layout_height="wrap_content"
27 android:padding="24dp"
28 app:trackCornerRadius="4dp" />
29
30</LinearLayout>