summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-10-30 19:29:00 -0400
committerGravatar Charles Lombardo2023-10-31 14:41:40 -0400
commite8cb8b2668c86ddad527cb8ff7de7f992080dece (patch)
tree18adfa82dd5ea699cd8f7bf7ad6d7e73d525469e
parentMerge pull request #11922 from t895/simplify-card-layout (diff)
downloadyuzu-e8cb8b2668c86ddad527cb8ff7de7f992080dece.tar.gz
yuzu-e8cb8b2668c86ddad527cb8ff7de7f992080dece.tar.xz
yuzu-e8cb8b2668c86ddad527cb8ff7de7f992080dece.zip
android: Implement applet launcher
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt32
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt90
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt72
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt113
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CabinetLauncherDialogFragment.kt41
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Applet.kt55
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt2
-rw-r--r--src/android/app/src/main/jni/native.cpp45
-rw-r--r--src/android/app/src/main/res/drawable/ic_album.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_applet.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_edit.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_mii.xml18
-rw-r--r--src/android/app/src/main/res/drawable/ic_refresh.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_restore.xml9
-rw-r--r--src/android/app/src/main/res/layout/card_applet_option.xml57
-rw-r--r--src/android/app/src/main/res/layout/dialog_list.xml15
-rw-r--r--src/android/app/src/main/res/layout/dialog_list_item.xml30
-rw-r--r--src/android/app/src/main/res/layout/fragment_applet_launcher.xml31
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml15
-rw-r--r--src/android/app/src/main/res/values/strings.xml18
24 files changed, 703 insertions, 9 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index e2c5b6acd..07f1b4842 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -252,7 +252,7 @@ object NativeLibrary {
252 252
253 external fun reloadKeys(): Boolean 253 external fun reloadKeys(): Boolean
254 254
255 external fun initializeEmulation() 255 external fun initializeSystem()
256 256
257 external fun defaultCPUCore(): Int 257 external fun defaultCPUCore(): Int
258 258
@@ -506,6 +506,36 @@ object NativeLibrary {
506 external fun initializeEmptyUserDirectory() 506 external fun initializeEmptyUserDirectory()
507 507
508 /** 508 /**
509 * Gets the launch path for a given applet. It is the caller's responsibility to also
510 * set the system's current applet ID before trying to launch the nca given by this function.
511 *
512 * @param id The applet entry ID
513 * @return The applet's launch path
514 */
515 external fun getAppletLaunchPath(id: Long): String
516
517 /**
518 * Sets the system's current applet ID before launching.
519 *
520 * @param appletId One of the ids in the Service::AM::Applets::AppletId enum
521 */
522 external fun setCurrentAppletId(appletId: Int)
523
524 /**
525 * Sets the cabinet mode for launching the cabinet applet.
526 *
527 * @param cabinetMode One of the modes that corresponds to the enum in Service::NFP::CabinetMode
528 */
529 external fun setCabinetMode(cabinetMode: Int)
530
531 /**
532 * Checks whether NAND contents are available and valid.
533 *
534 * @return 'true' if firmware is available
535 */
536 external fun isFirmwareAvailable(): Boolean
537
538 /**
509 * Button type for use in onTouchEvent 539 * Button type for use in onTouchEvent
510 */ 540 */
511 object ButtonType { 541 object ButtonType {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
new file mode 100644
index 000000000..a21a705c1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
@@ -0,0 +1,90 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup
9import android.widget.Toast
10import androidx.core.content.res.ResourcesCompat
11import androidx.fragment.app.FragmentActivity
12import androidx.navigation.findNavController
13import androidx.recyclerview.widget.RecyclerView
14import org.yuzu.yuzu_emu.HomeNavigationDirections
15import org.yuzu.yuzu_emu.NativeLibrary
16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.YuzuApplication
18import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding
19import org.yuzu.yuzu_emu.model.Applet
20import org.yuzu.yuzu_emu.model.AppletInfo
21import org.yuzu.yuzu_emu.model.Game
22
23class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
24 RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(),
25 View.OnClickListener {
26
27 override fun onCreateViewHolder(
28 parent: ViewGroup,
29 viewType: Int
30 ): AppletAdapter.AppletViewHolder {
31 CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
32 .apply { root.setOnClickListener(this@AppletAdapter) }
33 .also { return AppletViewHolder(it) }
34 }
35
36 override fun onBindViewHolder(holder: AppletViewHolder, position: Int) =
37 holder.bind(applets[position])
38
39 override fun getItemCount(): Int = applets.size
40
41 override fun onClick(view: View) {
42 val applet = (view.tag as AppletViewHolder).applet
43 val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
44 if (appletPath.isEmpty()) {
45 Toast.makeText(
46 YuzuApplication.appContext,
47 R.string.applets_error_applet,
48 Toast.LENGTH_SHORT
49 ).show()
50 return
51 }
52
53 if (applet.appletInfo == AppletInfo.Cabinet) {
54 view.findNavController()
55 .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
56 return
57 }
58
59 NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
60 val appletGame = Game(
61 title = YuzuApplication.appContext.getString(applet.titleId),
62 path = appletPath
63 )
64 val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
65 view.findNavController().navigate(action)
66 }
67
68 inner class AppletViewHolder(val binding: CardAppletOptionBinding) :
69 RecyclerView.ViewHolder(binding.root) {
70 lateinit var applet: Applet
71
72 init {
73 itemView.tag = this
74 }
75
76 fun bind(applet: Applet) {
77 this.applet = applet
78
79 binding.title.setText(applet.titleId)
80 binding.description.setText(applet.descriptionId)
81 binding.icon.setImageDrawable(
82 ResourcesCompat.getDrawable(
83 binding.icon.context.resources,
84 applet.iconId,
85 binding.icon.context.theme
86 )
87 )
88 }
89 }
90}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt
new file mode 100644
index 000000000..e7b7c0f2f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.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.adapters
5
6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup
9import androidx.core.content.res.ResourcesCompat
10import androidx.fragment.app.Fragment
11import androidx.navigation.fragment.findNavController
12import androidx.recyclerview.widget.RecyclerView
13import org.yuzu.yuzu_emu.HomeNavigationDirections
14import org.yuzu.yuzu_emu.NativeLibrary
15import org.yuzu.yuzu_emu.R
16import org.yuzu.yuzu_emu.YuzuApplication
17import org.yuzu.yuzu_emu.databinding.DialogListItemBinding
18import org.yuzu.yuzu_emu.model.CabinetMode
19import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder
20import org.yuzu.yuzu_emu.model.AppletInfo
21import org.yuzu.yuzu_emu.model.Game
22
23class CabinetLauncherDialogAdapter(val fragment: Fragment) :
24 RecyclerView.Adapter<CabinetModeViewHolder>(),
25 View.OnClickListener {
26 private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size)
27
28 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder {
29 DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
30 .apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) }
31 .also { return CabinetModeViewHolder(it) }
32 }
33
34 override fun getItemCount(): Int = cabinetModes.size
35
36 override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) =
37 holder.bind(cabinetModes[position])
38
39 override fun onClick(view: View) {
40 val mode = (view.tag as CabinetModeViewHolder).cabinetMode
41 val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
42 NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
43 NativeLibrary.setCabinetMode(mode.id)
44 val appletGame = Game(
45 title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
46 path = appletPath
47 )
48 val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
49 fragment.findNavController().navigate(action)
50 }
51
52 inner class CabinetModeViewHolder(val binding: DialogListItemBinding) :
53 RecyclerView.ViewHolder(binding.root) {
54 lateinit var cabinetMode: CabinetMode
55
56 init {
57 itemView.tag = this
58 }
59
60 fun bind(cabinetMode: CabinetMode) {
61 this.cabinetMode = cabinetMode
62 binding.icon.setImageDrawable(
63 ResourcesCompat.getDrawable(
64 binding.icon.context.resources,
65 cabinetMode.iconId,
66 binding.icon.context.theme
67 )
68 )
69 binding.title.setText(cabinetMode.titleId)
70 }
71 }
72}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt
new file mode 100644
index 000000000..1f66b440d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt
@@ -0,0 +1,113 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import androidx.core.view.ViewCompat
11import androidx.core.view.WindowInsetsCompat
12import androidx.core.view.updatePadding
13import androidx.fragment.app.Fragment
14import androidx.fragment.app.activityViewModels
15import androidx.navigation.findNavController
16import androidx.recyclerview.widget.GridLayoutManager
17import com.google.android.material.transition.MaterialSharedAxis
18import org.yuzu.yuzu_emu.R
19import org.yuzu.yuzu_emu.adapters.AppletAdapter
20import org.yuzu.yuzu_emu.databinding.FragmentAppletLauncherBinding
21import org.yuzu.yuzu_emu.model.Applet
22import org.yuzu.yuzu_emu.model.AppletInfo
23import org.yuzu.yuzu_emu.model.HomeViewModel
24
25class AppletLauncherFragment : Fragment() {
26 private var _binding: FragmentAppletLauncherBinding? = null
27 private val binding get() = _binding!!
28
29 private val homeViewModel: HomeViewModel by activityViewModels()
30
31 override fun onCreate(savedInstanceState: Bundle?) {
32 super.onCreate(savedInstanceState)
33 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
34 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
35 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
36 }
37
38 override fun onCreateView(
39 inflater: LayoutInflater,
40 container: ViewGroup?,
41 savedInstanceState: Bundle?
42 ): View {
43 _binding = FragmentAppletLauncherBinding.inflate(inflater)
44 return binding.root
45 }
46
47 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
48 super.onViewCreated(view, savedInstanceState)
49 homeViewModel.setNavigationVisibility(visible = false, animated = true)
50 homeViewModel.setStatusBarShadeVisibility(visible = false)
51
52 binding.toolbarApplets.setNavigationOnClickListener {
53 binding.root.findNavController().popBackStack()
54 }
55
56 val applets = listOf(
57 Applet(
58 R.string.album_applet,
59 R.string.album_applet_description,
60 R.drawable.ic_album,
61 AppletInfo.PhotoViewer
62 ),
63 Applet(
64 R.string.cabinet_applet,
65 R.string.cabinet_applet_description,
66 R.drawable.ic_nfc,
67 AppletInfo.Cabinet
68 ),
69 Applet(
70 R.string.mii_edit_applet,
71 R.string.mii_edit_applet_description,
72 R.drawable.ic_mii,
73 AppletInfo.MiiEdit
74 )
75 )
76
77 binding.listApplets.apply {
78 layoutManager = GridLayoutManager(
79 requireContext(),
80 resources.getInteger(R.integer.grid_columns)
81 )
82 adapter = AppletAdapter(requireActivity(), applets)
83 }
84
85 setInsets()
86 }
87
88 private fun setInsets() =
89 ViewCompat.setOnApplyWindowInsetsListener(
90 binding.root
91 ) { _: View, windowInsets: WindowInsetsCompat ->
92 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
93 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
94
95 val leftInsets = barInsets.left + cutoutInsets.left
96 val rightInsets = barInsets.right + cutoutInsets.right
97
98 val mlpAppBar = binding.toolbarApplets.layoutParams as ViewGroup.MarginLayoutParams
99 mlpAppBar.leftMargin = leftInsets
100 mlpAppBar.rightMargin = rightInsets
101 binding.toolbarApplets.layoutParams = mlpAppBar
102
103 val mlpListApplets =
104 binding.listApplets.layoutParams as ViewGroup.MarginLayoutParams
105 mlpListApplets.leftMargin = leftInsets
106 mlpListApplets.rightMargin = rightInsets
107 binding.listApplets.layoutParams = mlpListApplets
108
109 binding.listApplets.updatePadding(bottom = barInsets.bottom)
110
111 windowInsets
112 }
113}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CabinetLauncherDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CabinetLauncherDialogFragment.kt
new file mode 100644
index 000000000..5933677fd
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CabinetLauncherDialogFragment.kt
@@ -0,0 +1,41 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import androidx.fragment.app.DialogFragment
12import androidx.recyclerview.widget.LinearLayoutManager
13import com.google.android.material.dialog.MaterialAlertDialogBuilder
14import org.yuzu.yuzu_emu.R
15import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter
16import org.yuzu.yuzu_emu.databinding.DialogListBinding
17
18class CabinetLauncherDialogFragment : DialogFragment() {
19 private lateinit var binding: DialogListBinding
20
21 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
22 binding = DialogListBinding.inflate(layoutInflater)
23 binding.dialogList.apply {
24 layoutManager = LinearLayoutManager(requireContext())
25 adapter = CabinetLauncherDialogAdapter(this@CabinetLauncherDialogFragment)
26 }
27
28 return MaterialAlertDialogBuilder(requireContext())
29 .setTitle(R.string.cabinet_launcher)
30 .setView(binding.root)
31 .create()
32 }
33
34 override fun onCreateView(
35 inflater: LayoutInflater,
36 container: ViewGroup?,
37 savedInstanceState: Bundle?
38 ): View {
39 return binding.root
40 }
41}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index f273c880a..6e19fc6c0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -30,6 +30,7 @@ import androidx.recyclerview.widget.GridLayoutManager
30import com.google.android.material.transition.MaterialSharedAxis 30import com.google.android.material.transition.MaterialSharedAxis
31import org.yuzu.yuzu_emu.BuildConfig 31import org.yuzu.yuzu_emu.BuildConfig
32import org.yuzu.yuzu_emu.HomeNavigationDirections 32import org.yuzu.yuzu_emu.HomeNavigationDirections
33import org.yuzu.yuzu_emu.NativeLibrary
33import org.yuzu.yuzu_emu.R 34import org.yuzu.yuzu_emu.R
34import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter 35import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
35import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding 36import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
@@ -133,6 +134,20 @@ class HomeSettingsFragment : Fragment() {
133 ) 134 )
134 add( 135 add(
135 HomeSetting( 136 HomeSetting(
137 R.string.applets,
138 R.string.applets_description,
139 R.drawable.ic_applet,
140 {
141 binding.root.findNavController()
142 .navigate(R.id.action_homeSettingsFragment_to_appletLauncherFragment)
143 },
144 { NativeLibrary.isFirmwareAvailable() },
145 R.string.applets_error_firmware,
146 R.string.applets_error_description
147 )
148 )
149 add(
150 HomeSetting(
136 R.string.select_games_folder, 151 R.string.select_games_folder,
137 R.string.select_games_folder_description, 152 R.string.select_games_folder_description,
138 R.drawable.ic_add, 153 R.drawable.ic_add,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
index 541b22f47..a6183d19e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -8,6 +8,7 @@ import android.content.DialogInterface
8import android.content.Intent 8import android.content.Intent
9import android.net.Uri 9import android.net.Uri
10import android.os.Bundle 10import android.os.Bundle
11import android.text.Html
11import androidx.fragment.app.DialogFragment 12import androidx.fragment.app.DialogFragment
12import androidx.fragment.app.FragmentActivity 13import androidx.fragment.app.FragmentActivity
13import androidx.fragment.app.activityViewModels 14import androidx.fragment.app.activityViewModels
@@ -32,7 +33,9 @@ class MessageDialogFragment : DialogFragment() {
32 if (titleId != 0) dialog.setTitle(titleId) 33 if (titleId != 0) dialog.setTitle(titleId)
33 if (titleString.isNotEmpty()) dialog.setTitle(titleString) 34 if (titleString.isNotEmpty()) dialog.setTitle(titleString)
34 35
35 if (descriptionId != 0) dialog.setMessage(descriptionId) 36 if (descriptionId != 0) {
37 dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
38 }
36 if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString) 39 if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
37 40
38 if (helpLinkId != 0) { 41 if (helpLinkId != 0) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Applet.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Applet.kt
new file mode 100644
index 000000000..8677674a3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Applet.kt
@@ -0,0 +1,55 @@
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 androidx.annotation.DrawableRes
7import androidx.annotation.StringRes
8import org.yuzu.yuzu_emu.R
9
10data class Applet(
11 @StringRes val titleId: Int,
12 @StringRes val descriptionId: Int,
13 @DrawableRes val iconId: Int,
14 val appletInfo: AppletInfo,
15 val cabinetMode: CabinetMode = CabinetMode.None
16)
17
18// Combination of Common::AM::Applets::AppletId enum and the entry id
19enum class AppletInfo(val appletId: Int, val entryId: Long = 0) {
20 None(0x00),
21 Application(0x01),
22 OverlayDisplay(0x02),
23 QLaunch(0x03),
24 Starter(0x04),
25 Auth(0x0A),
26 Cabinet(0x0B, 0x0100000000001002),
27 Controller(0x0C),
28 DataErase(0x0D),
29 Error(0x0E),
30 NetConnect(0x0F),
31 ProfileSelect(0x10),
32 SoftwareKeyboard(0x11),
33 MiiEdit(0x12, 0x0100000000001009),
34 Web(0x13),
35 Shop(0x14),
36 PhotoViewer(0x015, 0x010000000000100D),
37 Settings(0x16),
38 OfflineWeb(0x17),
39 LoginShare(0x18),
40 WebAuth(0x19),
41 MyPage(0x1A)
42}
43
44// Matches enum in Service::NFP::CabinetMode with extra metadata
45enum class CabinetMode(
46 val id: Int,
47 @StringRes val titleId: Int = 0,
48 @DrawableRes val iconId: Int = 0
49) {
50 None(-1),
51 StartNicknameAndOwnerSettings(0, R.string.cabinet_nickname_and_owner, R.drawable.ic_edit),
52 StartGameDataEraser(1, R.string.cabinet_game_data_eraser, R.drawable.ic_refresh),
53 StartRestorer(2, R.string.cabinet_restorer, R.drawable.ic_restore),
54 StartFormatter(3, R.string.cabinet_formatter, R.drawable.ic_clear)
55}
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 b43978fce..de84b2adb 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
@@ -11,12 +11,12 @@ import kotlinx.serialization.Serializable
11@Parcelize 11@Parcelize
12@Serializable 12@Serializable
13class Game( 13class Game(
14 val title: String, 14 val title: String = "",
15 val path: String, 15 val path: String,
16 val programId: String, 16 val programId: String = "",
17 val developer: String, 17 val developer: String = "",
18 val version: String, 18 val version: String = "",
19 val isHomebrew: Boolean 19 val isHomebrew: Boolean = false
20) : Parcelable { 20) : Parcelable {
21 val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime" 21 val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime"
22 val keyLastPlayedTime get() = "${programId}_LastPlayed" 22 val keyLastPlayedTime get() = "${programId}_LastPlayed"
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 233aa4101..ba1177426 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
@@ -403,6 +403,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
403 } else { 403 } else {
404 firmwarePath.deleteRecursively() 404 firmwarePath.deleteRecursively()
405 cacheFirmwareDir.copyRecursively(firmwarePath, true) 405 cacheFirmwareDir.copyRecursively(firmwarePath, true)
406 NativeLibrary.initializeSystem()
406 getString(R.string.save_file_imported_success) 407 getString(R.string.save_file_imported_success)
407 } 408 }
408 } catch (e: Exception) { 409 } catch (e: Exception) {
@@ -648,7 +649,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
648 } 649 }
649 650
650 // Reinitialize relevant data 651 // Reinitialize relevant data
651 NativeLibrary.initializeEmulation() 652 NativeLibrary.initializeSystem()
652 gamesViewModel.reloadGames(false) 653 gamesViewModel.reloadGames(false)
653 654
654 return@newInstance getString(R.string.user_data_import_success) 655 return@newInstance getString(R.string.user_data_import_success)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
index 3c9f6bad0..79a07f7ef 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -15,7 +15,7 @@ object DirectoryInitialization {
15 fun start() { 15 fun start() {
16 if (!areDirectoriesReady) { 16 if (!areDirectoriesReady) {
17 initializeInternalStorage() 17 initializeInternalStorage()
18 NativeLibrary.initializeEmulation() 18 NativeLibrary.initializeSystem()
19 areDirectoriesReady = true 19 areDirectoriesReady = true
20 } 20 }
21 } 21 }
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 686b73588..f7931a89d 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -755,4 +755,49 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv*
755 } 755 }
756} 756}
757 757
758jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz,
759 jlong jid) {
760 auto bis_system =
761 EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
762 if (!bis_system) {
763 return ToJString(env, "");
764 }
765
766 auto applet_nca =
767 bis_system->GetEntry(static_cast<u64>(jid), FileSys::ContentRecordType::Program);
768 if (!applet_nca) {
769 return ToJString(env, "");
770 }
771
772 return ToJString(env, applet_nca->GetFullPath());
773}
774
775void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentAppletId(JNIEnv* env, jclass clazz,
776 jint jappletId) {
777 EmulationSession::GetInstance().System().GetAppletManager().SetCurrentAppletId(
778 static_cast<Service::AM::Applets::AppletId>(jappletId));
779}
780
781void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass clazz,
782 jint jcabinetMode) {
783 EmulationSession::GetInstance().System().GetAppletManager().SetCabinetMode(
784 static_cast<Service::NFP::CabinetMode>(jcabinetMode));
785}
786
787jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, jclass clazz) {
788 auto bis_system =
789 EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
790 if (!bis_system) {
791 return false;
792 }
793
794 // Query an applet to see if it's available
795 auto applet_nca =
796 bis_system->GetEntry(0x010000000000100Dull, FileSys::ContentRecordType::Program);
797 if (!applet_nca) {
798 return false;
799 }
800 return true;
801}
802
758} // extern "C" 803} // extern "C"
diff --git a/src/android/app/src/main/res/drawable/ic_album.xml b/src/android/app/src/main/res/drawable/ic_album.xml
new file mode 100644
index 000000000..f2b63813f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_album.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_applet.xml b/src/android/app/src/main/res/drawable/ic_applet.xml
new file mode 100644
index 000000000..b154e6f56
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_applet.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M17,16l-4,-4V8.82C14.16,8.4 15,7.3 15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6c0,1.3 0.84,2.4 2,2.82V12l-4,4H3v5h5v-3.05l4,-4.2 4,4.2V21h5v-5h-4z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_edit.xml b/src/android/app/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 000000000..ac22ce8a5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_mii.xml b/src/android/app/src/main/res/drawable/ic_mii.xml
new file mode 100644
index 000000000..1271ec401
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_mii.xml
@@ -0,0 +1,18 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M9,13m-1.25,0a1.25,1.25 0,1 1,2.5 0a1.25,1.25 0,1 1,-2.5 0" />
9 <path
10 android:fillColor="?attr/colorControlNormal"
11 android:pathData="M20.77,8.58l-0.92,2.01c0.09,0.46 0.15,0.93 0.15,1.41 0,4.41 -3.59,8 -8,8s-8,-3.59 -8,-8c0,-0.05 0.01,-0.1 0,-0.14 2.6,-0.98 4.69,-2.99 5.74,-5.55C11.58,8.56 14.37,10 17.5,10c0.45,0 0.89,-0.04 1.33,-0.1l-0.6,-1.32 -0.88,-1.93 -1.93,-0.88 -2.79,-1.27 2.79,-1.27 0.71,-0.32C14.87,2.33 13.47,2 12,2 6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10c0,-1.47 -0.33,-2.87 -0.9,-4.13l-0.33,0.71z" />
12 <path
13 android:fillColor="?attr/colorControlNormal"
14 android:pathData="M15,13m-1.25,0a1.25,1.25 0,1 1,2.5 0a1.25,1.25 0,1 1,-2.5 0" />
15 <path
16 android:fillColor="?attr/colorControlNormal"
17 android:pathData="M20.6,5.6L19.5,8l-1.1,-2.4L16,4.5l2.4,-1.1L19.5,1l1.1,2.4L23,4.5z" />
18</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_refresh.xml b/src/android/app/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 000000000..d0d87ecc2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_refresh.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_restore.xml b/src/android/app/src/main/res/drawable/ic_restore.xml
new file mode 100644
index 000000000..d6d9d4017
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_restore.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
9</vector>
diff --git a/src/android/app/src/main/res/layout/card_applet_option.xml b/src/android/app/src/main/res/layout/card_applet_option.xml
new file mode 100644
index 000000000..19fbec9f1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_applet_option.xml
@@ -0,0 +1,57 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 style="?attr/materialCardViewOutlinedStyle"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:layout_marginHorizontal="16dp"
9 android:layout_marginVertical="12dp"
10 android:background="?attr/selectableItemBackground"
11 android:clickable="true"
12 android:focusable="true">
13
14 <LinearLayout
15 android:layout_width="match_parent"
16 android:layout_height="wrap_content"
17 android:orientation="horizontal"
18 android:layout_gravity="center"
19 android:padding="24dp">
20
21 <ImageView
22 android:id="@+id/icon"
23 android:layout_width="24dp"
24 android:layout_height="24dp"
25 android:layout_marginEnd="20dp"
26 android:layout_gravity="center_vertical"
27 app:tint="?attr/colorOnSurface" />
28
29 <LinearLayout
30 android:layout_width="0dp"
31 android:layout_height="wrap_content"
32 android:layout_weight="1"
33 android:orientation="vertical"
34 android:layout_gravity="center_vertical">
35
36 <com.google.android.material.textview.MaterialTextView
37 android:id="@+id/title"
38 style="@style/TextAppearance.Material3.TitleMedium"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:textAlignment="viewStart"
42 tools:text="@string/applets" />
43
44 <com.google.android.material.textview.MaterialTextView
45 android:id="@+id/description"
46 style="@style/TextAppearance.Material3.BodyMedium"
47 android:layout_width="match_parent"
48 android:layout_height="wrap_content"
49 android:layout_marginTop="6dp"
50 android:textAlignment="viewStart"
51 tools:text="@string/applets_description" />
52
53 </LinearLayout>
54
55 </LinearLayout>
56
57</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/dialog_list.xml b/src/android/app/src/main/res/layout/dialog_list.xml
new file mode 100644
index 000000000..7de2b2c3a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_list.xml
@@ -0,0 +1,15 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="wrap_content">
5
6 <androidx.recyclerview.widget.RecyclerView
7 android:id="@+id/dialog_list"
8 android:layout_width="match_parent"
9 android:layout_height="wrap_content"
10 android:clipToPadding="false"
11 android:fadeScrollbars="false"
12 android:paddingVertical="12dp"
13 android:scrollbars="vertical" />
14
15</androidx.appcompat.widget.LinearLayoutCompat>
diff --git a/src/android/app/src/main/res/layout/dialog_list_item.xml b/src/android/app/src/main/res/layout/dialog_list_item.xml
new file mode 100644
index 000000000..39f3558ff
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_list_item.xml
@@ -0,0 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:background="?attr/selectableItemBackground"
7 android:clickable="true"
8 android:focusable="true"
9 android:orientation="horizontal"
10 android:paddingHorizontal="24dp"
11 android:paddingVertical="16dp">
12
13 <ImageView
14 android:id="@+id/icon"
15 android:layout_width="20dp"
16 android:layout_height="20dp"
17 android:layout_gravity="center"
18 tools:src="@drawable/ic_nfc" />
19
20 <com.google.android.material.textview.MaterialTextView
21 android:id="@+id/title"
22 style="@style/TextAppearance.Material3.BodyMedium"
23 android:layout_width="match_parent"
24 android:layout_height="wrap_content"
25 android:layout_marginStart="16dp"
26 android:layout_gravity="center_vertical|start"
27 android:textAlignment="viewStart"
28 tools:text="List option" />
29
30</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_applet_launcher.xml b/src/android/app/src/main/res/layout/fragment_applet_launcher.xml
new file mode 100644
index 000000000..fe8fae40f
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_applet_launcher.xml
@@ -0,0 +1,31 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/coordinator_applets"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface">
8
9 <com.google.android.material.appbar.AppBarLayout
10 android:id="@+id/appbar_applets"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:fitsSystemWindows="true">
14
15 <com.google.android.material.appbar.MaterialToolbar
16 android:id="@+id/toolbar_applets"
17 android:layout_width="match_parent"
18 android:layout_height="?attr/actionBarSize"
19 app:navigationIcon="@drawable/ic_back"
20 app:title="@string/applets" />
21
22 </com.google.android.material.appbar.AppBarLayout>
23
24 <androidx.recyclerview.widget.RecyclerView
25 android:id="@+id/list_applets"
26 android:layout_width="match_parent"
27 android:layout_height="match_parent"
28 android:clipToPadding="false"
29 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
30
31</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 82749359d..6d4c1f86d 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -25,6 +25,9 @@
25 <action 25 <action
26 android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment" 26 android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"
27 app:destination="@id/driverManagerFragment" /> 27 app:destination="@id/driverManagerFragment" />
28 <action
29 android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
30 app:destination="@id/appletLauncherFragment" />
28 </fragment> 31 </fragment>
29 32
30 <fragment 33 <fragment
@@ -102,5 +105,17 @@
102 android:id="@+id/driverManagerFragment" 105 android:id="@+id/driverManagerFragment"
103 android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" 106 android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
104 android:label="DriverManagerFragment" /> 107 android:label="DriverManagerFragment" />
108 <fragment
109 android:id="@+id/appletLauncherFragment"
110 android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
111 android:label="AppletLauncherFragment" >
112 <action
113 android:id="@+id/action_appletLauncherFragment_to_cabinetLauncherDialogFragment"
114 app:destination="@id/cabinetLauncherDialogFragment" />
115 </fragment>
116 <dialog
117 android:id="@+id/cabinetLauncherDialogFragment"
118 android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
119 android:label="CabinetLauncherDialogFragment" />
105 120
106</navigation> 121</navigation>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 9e4854221..b92978140 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -124,6 +124,24 @@
124 <string name="share_save_file">Share save file</string> 124 <string name="share_save_file">Share save file</string>
125 <string name="export_save_failed">Failed to export save</string> 125 <string name="export_save_failed">Failed to export save</string>
126 126
127 <!-- Applet launcher strings -->
128 <string name="applets">Applet launcher</string>
129 <string name="applets_description">Launch system applets using installed firmware</string>
130 <string name="applets_error_firmware">Firmware not installed</string>
131 <string name="applets_error_applet">Applet not available</string>
132 <string name="applets_error_description"><![CDATA[Please ensure your <a href="https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys">prod.keys</a> file and <a href="https://yuzu-emu.org/help/quickstart/#dumping-system-firmware">firmware</a> are installed and try again.]]></string>
133 <string name="album_applet">Album</string>
134 <string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string>
135 <string name="mii_edit_applet">Mii edit</string>
136 <string name="mii_edit_applet_description">View and edit Miis with the system editor</string>
137 <string name="cabinet_applet">Cabinet</string>
138 <string name="cabinet_applet_description">Edit and delete data stored on amiibo</string>
139 <string name="cabinet_launcher">Cabinet launcher</string>
140 <string name="cabinet_nickname_and_owner">Nickname and owner settings</string>
141 <string name="cabinet_game_data_eraser">Game data eraser</string>
142 <string name="cabinet_restorer">Restorer</string>
143 <string name="cabinet_formatter">Formatter</string>
144
127 <!-- About screen strings --> 145 <!-- About screen strings -->
128 <string name="gaia_is_not_real">Gaia isn\'t real</string> 146 <string name="gaia_is_not_real">Gaia isn\'t real</string>
129 <string name="copied_to_clipboard">Copied to clipboard</string> 147 <string name="copied_to_clipboard">Copied to clipboard</string>