Browse Source
Merge pull request #11603 from t895/consolidate-installs
Merge pull request #11603 from t895/consolidate-installs
android: Consolidate installers to one fragmentnce_cpp
committed by
GitHub
33 changed files with 616 additions and 421 deletions
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
-
49src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
-
7src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
-
48src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
-
214src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
-
18src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
-
138src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
-
13src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt
-
6src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
-
248src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
67src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
-
22src/android/app/src/main/jni/native.cpp
-
71src/android/app/src/main/res/layout/card_installable.xml
-
61src/android/app/src/main/res/layout/fragment_about.xml
-
31src/android/app/src/main/res/layout/fragment_installables.xml
-
7src/android/app/src/main/res/navigation/home_navigation.xml
-
1src/android/app/src/main/res/values-de/strings.xml
-
1src/android/app/src/main/res/values-es/strings.xml
-
1src/android/app/src/main/res/values-fr/strings.xml
-
1src/android/app/src/main/res/values-it/strings.xml
-
1src/android/app/src/main/res/values-ja/strings.xml
-
1src/android/app/src/main/res/values-ko/strings.xml
-
1src/android/app/src/main/res/values-nb/strings.xml
-
1src/android/app/src/main/res/values-pl/strings.xml
-
1src/android/app/src/main/res/values-pt-rBR/strings.xml
-
1src/android/app/src/main/res/values-pt-rPT/strings.xml
-
1src/android/app/src/main/res/values-ru/strings.xml
-
1src/android/app/src/main/res/values-uk/strings.xml
-
6src/android/app/src/main/res/values-w600dp/integers.xml
-
1src/android/app/src/main/res/values-zh-rCN/strings.xml
-
1src/android/app/src/main/res/values-zh-rTW/strings.xml
-
2src/android/app/src/main/res/values/integers.xml
-
10src/android/app/src/main/res/values/strings.xml
@ -0,0 +1,49 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
package org.yuzu.yuzu_emu.adapters |
|||
|
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding |
|||
import org.yuzu.yuzu_emu.model.Installable |
|||
|
|||
class InstallableAdapter(private val installables: List<Installable>) : |
|||
RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { |
|||
override fun onCreateViewHolder( |
|||
parent: ViewGroup, |
|||
viewType: Int |
|||
): InstallableAdapter.InstallableViewHolder { |
|||
val binding = |
|||
CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
|||
return InstallableViewHolder(binding) |
|||
} |
|||
|
|||
override fun getItemCount(): Int = installables.size |
|||
|
|||
override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = |
|||
holder.bind(installables[position]) |
|||
|
|||
inner class InstallableViewHolder(val binding: CardInstallableBinding) : |
|||
RecyclerView.ViewHolder(binding.root) { |
|||
lateinit var installable: Installable |
|||
|
|||
fun bind(installable: Installable) { |
|||
this.installable = installable |
|||
|
|||
binding.title.setText(installable.titleId) |
|||
binding.description.setText(installable.descriptionId) |
|||
|
|||
if (installable.install != null) { |
|||
binding.buttonInstall.visibility = View.VISIBLE |
|||
binding.buttonInstall.setOnClickListener { installable.install.invoke() } |
|||
} |
|||
if (installable.export != null) { |
|||
binding.buttonExport.visibility = View.VISIBLE |
|||
binding.buttonExport.setOnClickListener { installable.export.invoke() } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,214 +0,0 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
package org.yuzu.yuzu_emu.fragments |
|||
|
|||
import android.app.Dialog |
|||
import android.content.Intent |
|||
import android.net.Uri |
|||
import android.os.Bundle |
|||
import android.provider.DocumentsContract |
|||
import android.widget.Toast |
|||
import androidx.activity.result.ActivityResultLauncher |
|||
import androidx.activity.result.contract.ActivityResultContracts |
|||
import androidx.appcompat.app.AppCompatActivity |
|||
import androidx.documentfile.provider.DocumentFile |
|||
import androidx.fragment.app.DialogFragment |
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
|||
import java.io.BufferedOutputStream |
|||
import java.io.File |
|||
import java.io.FileOutputStream |
|||
import java.io.FilenameFilter |
|||
import java.time.LocalDateTime |
|||
import java.time.format.DateTimeFormatter |
|||
import java.util.zip.ZipEntry |
|||
import java.util.zip.ZipOutputStream |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
import kotlinx.coroutines.withContext |
|||
import org.yuzu.yuzu_emu.R |
|||
import org.yuzu.yuzu_emu.YuzuApplication |
|||
import org.yuzu.yuzu_emu.features.DocumentProvider |
|||
import org.yuzu.yuzu_emu.getPublicFilesDir |
|||
import org.yuzu.yuzu_emu.utils.FileUtil |
|||
|
|||
class ImportExportSavesFragment : DialogFragment() { |
|||
private val context = YuzuApplication.appContext |
|||
private val savesFolder = |
|||
"${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" |
|||
|
|||
// Get first subfolder in saves folder (should be the user folder) |
|||
private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" |
|||
private var lastZipCreated: File? = null |
|||
|
|||
private lateinit var startForResultExportSave: ActivityResultLauncher<Intent> |
|||
private lateinit var documentPicker: ActivityResultLauncher<Array<String>> |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
val activity = requireActivity() as AppCompatActivity |
|||
|
|||
val activityResultRegistry = requireActivity().activityResultRegistry |
|||
startForResultExportSave = activityResultRegistry.register( |
|||
"startForResultExportSaveKey", |
|||
ActivityResultContracts.StartActivityForResult() |
|||
) { |
|||
File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively() |
|||
} |
|||
documentPicker = activityResultRegistry.register( |
|||
"documentPickerKey", |
|||
ActivityResultContracts.OpenDocument() |
|||
) { |
|||
it?.let { uri -> importSave(uri, activity) } |
|||
} |
|||
} |
|||
|
|||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { |
|||
return if (savesFolderRoot == "") { |
|||
MaterialAlertDialogBuilder(requireContext()) |
|||
.setTitle(R.string.manage_save_data) |
|||
.setMessage(R.string.import_export_saves_no_profile) |
|||
.setPositiveButton(android.R.string.ok, null) |
|||
.show() |
|||
} else { |
|||
MaterialAlertDialogBuilder(requireContext()) |
|||
.setTitle(R.string.manage_save_data) |
|||
.setMessage(R.string.manage_save_data_description) |
|||
.setNegativeButton(R.string.export_saves) { _, _ -> |
|||
exportSave() |
|||
} |
|||
.setPositiveButton(R.string.import_saves) { _, _ -> |
|||
documentPicker.launch(arrayOf("application/zip")) |
|||
} |
|||
.setNeutralButton(android.R.string.cancel, null) |
|||
.show() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Zips the save files located in the given folder path and creates a new zip file with the current date and time. |
|||
* @return true if the zip file is successfully created, false otherwise. |
|||
*/ |
|||
private fun zipSave(): Boolean { |
|||
try { |
|||
val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp") |
|||
tempFolder.mkdirs() |
|||
val saveFolder = File(savesFolderRoot) |
|||
val outputZipFile = File( |
|||
tempFolder, |
|||
"yuzu saves - ${ |
|||
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) |
|||
}.zip" |
|||
) |
|||
outputZipFile.createNewFile() |
|||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> |
|||
saveFolder.walkTopDown().forEach { file -> |
|||
val zipFileName = |
|||
file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") |
|||
if (zipFileName == "") { |
|||
return@forEach |
|||
} |
|||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") |
|||
zos.putNextEntry(entry) |
|||
if (file.isFile) { |
|||
file.inputStream().use { fis -> fis.copyTo(zos) } |
|||
} |
|||
} |
|||
} |
|||
lastZipCreated = outputZipFile |
|||
} catch (e: Exception) { |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent. |
|||
*/ |
|||
private fun exportSave() { |
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
val wasZipCreated = zipSave() |
|||
val lastZipFile = lastZipCreated |
|||
if (!wasZipCreated || lastZipFile == null) { |
|||
withContext(Dispatchers.Main) { |
|||
Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show() |
|||
} |
|||
return@launch |
|||
} |
|||
|
|||
withContext(Dispatchers.Main) { |
|||
val file = DocumentFile.fromSingleUri( |
|||
context, |
|||
DocumentsContract.buildDocumentUri( |
|||
DocumentProvider.AUTHORITY, |
|||
"${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" |
|||
) |
|||
)!! |
|||
val intent = Intent(Intent.ACTION_SEND) |
|||
.setDataAndType(file.uri, "application/zip") |
|||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
|||
.putExtra(Intent.EXTRA_STREAM, file.uri) |
|||
startForResultExportSave.launch(Intent.createChooser(intent, "Share save file")) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Imports the save files contained in the zip file, and replaces any existing ones with the new save file. |
|||
* @param zipUri The Uri of the zip file containing the save file(s) to import. |
|||
*/ |
|||
private fun importSave(zipUri: Uri, activity: AppCompatActivity) { |
|||
val inputZip = context.contentResolver.openInputStream(zipUri) |
|||
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. |
|||
var validZip = false |
|||
val savesFolder = File(savesFolderRoot) |
|||
val cacheSaveDir = File("${context.cacheDir.path}/saves/") |
|||
cacheSaveDir.mkdir() |
|||
|
|||
if (inputZip == null) { |
|||
Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) |
|||
.show() |
|||
return |
|||
} |
|||
|
|||
val filterTitleId = |
|||
FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } |
|||
|
|||
try { |
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
FileUtil.unzip(inputZip, cacheSaveDir) |
|||
cacheSaveDir.list(filterTitleId)?.forEach { savePath -> |
|||
File(savesFolder, savePath).deleteRecursively() |
|||
File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) |
|||
validZip = true |
|||
} |
|||
|
|||
withContext(Dispatchers.Main) { |
|||
if (!validZip) { |
|||
MessageDialogFragment.newInstance( |
|||
requireActivity(), |
|||
titleId = R.string.save_file_invalid_zip_structure, |
|||
descriptionId = R.string.save_file_invalid_zip_structure_description |
|||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG) |
|||
return@withContext |
|||
} |
|||
Toast.makeText( |
|||
context, |
|||
context.getString(R.string.save_file_imported_success), |
|||
Toast.LENGTH_LONG |
|||
).show() |
|||
} |
|||
|
|||
cacheSaveDir.deleteRecursively() |
|||
} |
|||
} catch (e: Exception) { |
|||
Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) |
|||
.show() |
|||
} |
|||
} |
|||
|
|||
companion object { |
|||
const val TAG = "ImportExportSavesFragment" |
|||
} |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
package org.yuzu.yuzu_emu.fragments |
|||
|
|||
import android.os.Bundle |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.core.view.ViewCompat |
|||
import androidx.core.view.WindowInsetsCompat |
|||
import androidx.core.view.updatePadding |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.navigation.findNavController |
|||
import androidx.recyclerview.widget.GridLayoutManager |
|||
import com.google.android.material.transition.MaterialSharedAxis |
|||
import org.yuzu.yuzu_emu.R |
|||
import org.yuzu.yuzu_emu.adapters.InstallableAdapter |
|||
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding |
|||
import org.yuzu.yuzu_emu.model.HomeViewModel |
|||
import org.yuzu.yuzu_emu.model.Installable |
|||
import org.yuzu.yuzu_emu.ui.main.MainActivity |
|||
|
|||
class InstallableFragment : Fragment() { |
|||
private var _binding: FragmentInstallablesBinding? = null |
|||
private val binding get() = _binding!! |
|||
|
|||
private val homeViewModel: HomeViewModel by activityViewModels() |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) |
|||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
|||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
|||
} |
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View { |
|||
_binding = FragmentInstallablesBinding.inflate(layoutInflater) |
|||
return binding.root |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
|
|||
val mainActivity = requireActivity() as MainActivity |
|||
|
|||
homeViewModel.setNavigationVisibility(visible = false, animated = true) |
|||
homeViewModel.setStatusBarShadeVisibility(visible = false) |
|||
|
|||
binding.toolbarInstallables.setNavigationOnClickListener { |
|||
binding.root.findNavController().popBackStack() |
|||
} |
|||
|
|||
val installables = listOf( |
|||
Installable( |
|||
R.string.user_data, |
|||
R.string.user_data_description, |
|||
install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, |
|||
export = { mainActivity.exportUserData.launch("export.zip") } |
|||
), |
|||
Installable( |
|||
R.string.install_game_content, |
|||
R.string.install_game_content_description, |
|||
install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } |
|||
), |
|||
Installable( |
|||
R.string.install_firmware, |
|||
R.string.install_firmware_description, |
|||
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } |
|||
), |
|||
if (mainActivity.savesFolderRoot != "") { |
|||
Installable( |
|||
R.string.manage_save_data, |
|||
R.string.import_export_saves_description, |
|||
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, |
|||
export = { mainActivity.exportSave() } |
|||
) |
|||
} else { |
|||
Installable( |
|||
R.string.manage_save_data, |
|||
R.string.import_export_saves_description, |
|||
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } |
|||
) |
|||
}, |
|||
Installable( |
|||
R.string.install_prod_keys, |
|||
R.string.install_prod_keys_description, |
|||
install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } |
|||
), |
|||
Installable( |
|||
R.string.install_amiibo_keys, |
|||
R.string.install_amiibo_keys_description, |
|||
install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } |
|||
) |
|||
) |
|||
|
|||
binding.listInstallables.apply { |
|||
layoutManager = GridLayoutManager( |
|||
requireContext(), |
|||
resources.getInteger(R.integer.grid_columns) |
|||
) |
|||
adapter = InstallableAdapter(installables) |
|||
} |
|||
|
|||
setInsets() |
|||
} |
|||
|
|||
private fun setInsets() = |
|||
ViewCompat.setOnApplyWindowInsetsListener( |
|||
binding.root |
|||
) { _: View, windowInsets: WindowInsetsCompat -> |
|||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) |
|||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) |
|||
|
|||
val leftInsets = barInsets.left + cutoutInsets.left |
|||
val rightInsets = barInsets.right + cutoutInsets.right |
|||
|
|||
val mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams |
|||
mlpAppBar.leftMargin = leftInsets |
|||
mlpAppBar.rightMargin = rightInsets |
|||
binding.toolbarInstallables.layoutParams = mlpAppBar |
|||
|
|||
val mlpScrollAbout = |
|||
binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams |
|||
mlpScrollAbout.leftMargin = leftInsets |
|||
mlpScrollAbout.rightMargin = rightInsets |
|||
binding.listInstallables.layoutParams = mlpScrollAbout |
|||
|
|||
binding.listInstallables.updatePadding(bottom = barInsets.bottom) |
|||
|
|||
windowInsets |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
package org.yuzu.yuzu_emu.model |
|||
|
|||
import androidx.annotation.StringRes |
|||
|
|||
data class Installable( |
|||
@StringRes val titleId: Int, |
|||
@StringRes val descriptionId: Int, |
|||
val install: (() -> Unit)? = null, |
|||
val export: (() -> Unit)? = null |
|||
) |
|||
@ -0,0 +1,71 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
style="?attr/materialCardViewOutlinedStyle" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginHorizontal="16dp" |
|||
android:layout_marginVertical="12dp"> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_margin="16dp" |
|||
android:orientation="horizontal" |
|||
android:layout_gravity="center"> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginEnd="16dp" |
|||
android:layout_weight="1" |
|||
android:orientation="vertical"> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/title" |
|||
style="@style/TextAppearance.Material3.TitleMedium" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:text="@string/user_data" |
|||
android:textAlignment="viewStart" /> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/description" |
|||
style="@style/TextAppearance.Material3.BodyMedium" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="6dp" |
|||
android:text="@string/user_data_description" |
|||
android:textAlignment="viewStart" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
<Button |
|||
android:id="@+id/button_export" |
|||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_gravity="center_vertical" |
|||
android:contentDescription="@string/export" |
|||
android:tooltipText="@string/export" |
|||
android:visibility="gone" |
|||
app:icon="@drawable/ic_export" |
|||
tools:visibility="visible" /> |
|||
|
|||
<Button |
|||
android:id="@+id/button_install" |
|||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_gravity="center_vertical" |
|||
android:layout_marginStart="12dp" |
|||
android:contentDescription="@string/string_import" |
|||
android:tooltipText="@string/string_import" |
|||
android:visibility="gone" |
|||
app:icon="@drawable/ic_import" |
|||
tools:visibility="visible" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
</com.google.android.material.card.MaterialCardView> |
|||
@ -0,0 +1,31 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
android:id="@+id/coordinator_licenses" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:background="?attr/colorSurface"> |
|||
|
|||
<com.google.android.material.appbar.AppBarLayout |
|||
android:id="@+id/appbar_installables" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:fitsSystemWindows="true"> |
|||
|
|||
<com.google.android.material.appbar.MaterialToolbar |
|||
android:id="@+id/toolbar_installables" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="?attr/actionBarSize" |
|||
app:title="@string/manage_yuzu_data" |
|||
app:navigationIcon="@drawable/ic_back" /> |
|||
|
|||
</com.google.android.material.appbar.AppBarLayout> |
|||
|
|||
<androidx.recyclerview.widget.RecyclerView |
|||
android:id="@+id/list_installables" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:clipToPadding="false" |
|||
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> |
|||
|
|||
</androidx.coordinatorlayout.widget.CoordinatorLayout> |
|||
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
|
|||
<integer name="grid_columns">2</integer> |
|||
|
|||
</resources> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue