diff options
| author | liamwhite <liamwhite@users.noreply.github.com> | 2023-10-31 16:55:57 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-31 16:55:57 -0400 | 
| commit | 7e284809dea3134c7d57db602deb9893170ec2ca (patch) | |
| tree | c5d6d6f37bffaf610266516cd3c276f2b3f842bc | |
| parent | 324c93e4aa63da9f4dcca7cc6c283185db86ff3a (diff) | |
| parent | 133788d0d4c12df7d7e39c4962cadadc781c596c (diff) | |
Merge pull request #11931 from t895/applet-launcher
android: Applet launcher UI
25 files changed, 717 insertions, 18 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 {      external fun reloadKeys(): Boolean -    external fun initializeEmulation() +    external fun initializeSystem()      external fun defaultCPUCore(): Int @@ -506,6 +506,36 @@ object NativeLibrary {      external fun initializeEmptyUserDirectory()      /** +     * Gets the launch path for a given applet. It is the caller's responsibility to also +     * set the system's current applet ID before trying to launch the nca given by this function. +     * +     * @param id The applet entry ID +     * @return The applet's launch path +     */ +    external fun getAppletLaunchPath(id: Long): String + +    /** +     * Sets the system's current applet ID before launching. +     * +     * @param appletId One of the ids in the Service::AM::Applets::AppletId enum +     */ +    external fun setCurrentAppletId(appletId: Int) + +    /** +     * Sets the cabinet mode for launching the cabinet applet. +     * +     * @param cabinetMode One of the modes that corresponds to the enum in Service::NFP::CabinetMode +     */ +    external fun setCabinetMode(cabinetMode: Int) + +    /** +     * Checks whether NAND contents are available and valid. +     * +     * @return 'true' if firmware is available +     */ +    external fun isFirmwareAvailable(): Boolean + +    /**       * Button type for use in onTouchEvent       */      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 @@ +// 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 android.widget.Toast +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.FragmentActivity +import androidx.navigation.findNavController +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding +import org.yuzu.yuzu_emu.model.Applet +import org.yuzu.yuzu_emu.model.AppletInfo +import org.yuzu.yuzu_emu.model.Game + +class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : +    RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(), +    View.OnClickListener { + +    override fun onCreateViewHolder( +        parent: ViewGroup, +        viewType: Int +    ): AppletAdapter.AppletViewHolder { +        CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .apply { root.setOnClickListener(this@AppletAdapter) } +            .also { return AppletViewHolder(it) } +    } + +    override fun onBindViewHolder(holder: AppletViewHolder, position: Int) = +        holder.bind(applets[position]) + +    override fun getItemCount(): Int = applets.size + +    override fun onClick(view: View) { +        val applet = (view.tag as AppletViewHolder).applet +        val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId) +        if (appletPath.isEmpty()) { +            Toast.makeText( +                YuzuApplication.appContext, +                R.string.applets_error_applet, +                Toast.LENGTH_SHORT +            ).show() +            return +        } + +        if (applet.appletInfo == AppletInfo.Cabinet) { +            view.findNavController() +                .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment) +            return +        } + +        NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId) +        val appletGame = Game( +            title = YuzuApplication.appContext.getString(applet.titleId), +            path = appletPath +        ) +        val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) +        view.findNavController().navigate(action) +    } + +    inner class AppletViewHolder(val binding: CardAppletOptionBinding) : +        RecyclerView.ViewHolder(binding.root) { +        lateinit var applet: Applet + +        init { +            itemView.tag = this +        } + +        fun bind(applet: Applet) { +            this.applet = applet + +            binding.title.setText(applet.titleId) +            binding.description.setText(applet.descriptionId) +            binding.icon.setImageDrawable( +                ResourcesCompat.getDrawable( +                    binding.icon.context.resources, +                    applet.iconId, +                    binding.icon.context.theme +                ) +            ) +        } +    } +} 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 @@ +// 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.core.content.res.ResourcesCompat +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.databinding.DialogListItemBinding +import org.yuzu.yuzu_emu.model.CabinetMode +import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder +import org.yuzu.yuzu_emu.model.AppletInfo +import org.yuzu.yuzu_emu.model.Game + +class CabinetLauncherDialogAdapter(val fragment: Fragment) : +    RecyclerView.Adapter<CabinetModeViewHolder>(), +    View.OnClickListener { +    private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size) + +    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder { +        DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) } +            .also { return CabinetModeViewHolder(it) } +    } + +    override fun getItemCount(): Int = cabinetModes.size + +    override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) = +        holder.bind(cabinetModes[position]) + +    override fun onClick(view: View) { +        val mode = (view.tag as CabinetModeViewHolder).cabinetMode +        val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId) +        NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId) +        NativeLibrary.setCabinetMode(mode.id) +        val appletGame = Game( +            title = YuzuApplication.appContext.getString(R.string.cabinet_applet), +            path = appletPath +        ) +        val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) +        fragment.findNavController().navigate(action) +    } + +    inner class CabinetModeViewHolder(val binding: DialogListItemBinding) : +        RecyclerView.ViewHolder(binding.root) { +        lateinit var cabinetMode: CabinetMode + +        init { +            itemView.tag = this +        } + +        fun bind(cabinetMode: CabinetMode) { +            this.cabinetMode = cabinetMode +            binding.icon.setImageDrawable( +                ResourcesCompat.getDrawable( +                    binding.icon.context.resources, +                    cabinetMode.iconId, +                    binding.icon.context.theme +                ) +            ) +            binding.title.setText(cabinetMode.titleId) +        } +    } +} 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 @@ +// 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.AppletAdapter +import org.yuzu.yuzu_emu.databinding.FragmentAppletLauncherBinding +import org.yuzu.yuzu_emu.model.Applet +import org.yuzu.yuzu_emu.model.AppletInfo +import org.yuzu.yuzu_emu.model.HomeViewModel + +class AppletLauncherFragment : Fragment() { +    private var _binding: FragmentAppletLauncherBinding? = 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 = FragmentAppletLauncherBinding.inflate(inflater) +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState) +        homeViewModel.setNavigationVisibility(visible = false, animated = true) +        homeViewModel.setStatusBarShadeVisibility(visible = false) + +        binding.toolbarApplets.setNavigationOnClickListener { +            binding.root.findNavController().popBackStack() +        } + +        val applets = listOf( +            Applet( +                R.string.album_applet, +                R.string.album_applet_description, +                R.drawable.ic_album, +                AppletInfo.PhotoViewer +            ), +            Applet( +                R.string.cabinet_applet, +                R.string.cabinet_applet_description, +                R.drawable.ic_nfc, +                AppletInfo.Cabinet +            ), +            Applet( +                R.string.mii_edit_applet, +                R.string.mii_edit_applet_description, +                R.drawable.ic_mii, +                AppletInfo.MiiEdit +            ) +        ) + +        binding.listApplets.apply { +            layoutManager = GridLayoutManager( +                requireContext(), +                resources.getInteger(R.integer.grid_columns) +            ) +            adapter = AppletAdapter(requireActivity(), applets) +        } + +        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.toolbarApplets.layoutParams as ViewGroup.MarginLayoutParams +            mlpAppBar.leftMargin = leftInsets +            mlpAppBar.rightMargin = rightInsets +            binding.toolbarApplets.layoutParams = mlpAppBar + +            val mlpListApplets = +                binding.listApplets.layoutParams as ViewGroup.MarginLayoutParams +            mlpListApplets.leftMargin = leftInsets +            mlpListApplets.rightMargin = rightInsets +            binding.listApplets.layoutParams = mlpListApplets + +            binding.listApplets.updatePadding(bottom = barInsets.bottom) + +            windowInsets +        } +} 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 @@ +// 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.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter +import org.yuzu.yuzu_emu.databinding.DialogListBinding + +class CabinetLauncherDialogFragment : DialogFragment() { +    private lateinit var binding: DialogListBinding + +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { +        binding = DialogListBinding.inflate(layoutInflater) +        binding.dialogList.apply { +            layoutManager = LinearLayoutManager(requireContext()) +            adapter = CabinetLauncherDialogAdapter(this@CabinetLauncherDialogFragment) +        } + +        return MaterialAlertDialogBuilder(requireContext()) +            .setTitle(R.string.cabinet_launcher) +            .setView(binding.root) +            .create() +    } + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        return binding.root +    } +} 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  import com.google.android.material.transition.MaterialSharedAxis  import org.yuzu.yuzu_emu.BuildConfig  import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter  import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding @@ -133,6 +134,20 @@ class HomeSettingsFragment : Fragment() {              )              add(                  HomeSetting( +                    R.string.applets, +                    R.string.applets_description, +                    R.drawable.ic_applet, +                    { +                        binding.root.findNavController() +                            .navigate(R.id.action_homeSettingsFragment_to_appletLauncherFragment) +                    }, +                    { NativeLibrary.isFirmwareAvailable() }, +                    R.string.applets_error_firmware, +                    R.string.applets_error_description +                ) +            ) +            add( +                HomeSetting(                      R.string.select_games_folder,                      R.string.select_games_folder_description,                      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  import android.content.Intent  import android.net.Uri  import android.os.Bundle +import android.text.Html  import androidx.fragment.app.DialogFragment  import androidx.fragment.app.FragmentActivity  import androidx.fragment.app.activityViewModels @@ -32,7 +33,9 @@ class MessageDialogFragment : DialogFragment() {          if (titleId != 0) dialog.setTitle(titleId)          if (titleString.isNotEmpty()) dialog.setTitle(titleString) -        if (descriptionId != 0) dialog.setMessage(descriptionId) +        if (descriptionId != 0) { +            dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) +        }          if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)          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 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.R + +data class Applet( +    @StringRes val titleId: Int, +    @StringRes val descriptionId: Int, +    @DrawableRes val iconId: Int, +    val appletInfo: AppletInfo, +    val cabinetMode: CabinetMode = CabinetMode.None +) + +// Combination of Common::AM::Applets::AppletId enum and the entry id +enum class AppletInfo(val appletId: Int, val entryId: Long = 0) { +    None(0x00), +    Application(0x01), +    OverlayDisplay(0x02), +    QLaunch(0x03), +    Starter(0x04), +    Auth(0x0A), +    Cabinet(0x0B, 0x0100000000001002), +    Controller(0x0C), +    DataErase(0x0D), +    Error(0x0E), +    NetConnect(0x0F), +    ProfileSelect(0x10), +    SoftwareKeyboard(0x11), +    MiiEdit(0x12, 0x0100000000001009), +    Web(0x13), +    Shop(0x14), +    PhotoViewer(0x015, 0x010000000000100D), +    Settings(0x16), +    OfflineWeb(0x17), +    LoginShare(0x18), +    WebAuth(0x19), +    MyPage(0x1A) +} + +// Matches enum in Service::NFP::CabinetMode with extra metadata +enum class CabinetMode( +    val id: Int, +    @StringRes val titleId: Int = 0, +    @DrawableRes val iconId: Int = 0 +) { +    None(-1), +    StartNicknameAndOwnerSettings(0, R.string.cabinet_nickname_and_owner, R.drawable.ic_edit), +    StartGameDataEraser(1, R.string.cabinet_game_data_eraser, R.drawable.ic_refresh), +    StartRestorer(2, R.string.cabinet_restorer, R.drawable.ic_restore), +    StartFormatter(3, R.string.cabinet_formatter, R.drawable.ic_clear) +} 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  @Parcelize  @Serializable  class Game( -    val title: String, +    val title: String = "",      val path: String, -    val programId: String, -    val developer: String, -    val version: String, -    val isHomebrew: Boolean +    val programId: String = "", +    val developer: String = "", +    val version: String = "", +    val isHomebrew: Boolean = false  ) : Parcelable {      val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime"      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 {                      } else {                          firmwarePath.deleteRecursively()                          cacheFirmwareDir.copyRecursively(firmwarePath, true) +                        NativeLibrary.initializeSystem()                          getString(R.string.save_file_imported_success)                      }                  } catch (e: Exception) { @@ -648,7 +649,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {                  }                  // Reinitialize relevant data -                NativeLibrary.initializeEmulation() +                NativeLibrary.initializeSystem()                  gamesViewModel.reloadGames(false)                  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 {      fun start() {          if (!areDirectoriesReady) {              initializeInternalStorage() -            NativeLibrary.initializeEmulation() +            NativeLibrary.initializeSystem()              areDirectoriesReady = true          }      } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 686b73588..0e458df38 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -247,6 +247,17 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)      }  } +void EmulationSession::InitializeSystem() { +    // Initialize filesystem. +    m_system.SetFilesystem(m_vfs); +    m_system.GetUserChannel().clear(); +    m_manual_provider = std::make_unique<FileSys::ManualContentProvider>(); +    m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); +    m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, +                                     m_manual_provider.get()); +    m_system.GetFileSystemController().CreateFactories(*m_vfs); +} +  Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath) {      std::scoped_lock lock(m_mutex); @@ -254,9 +265,6 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string      m_window =          std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library); -    m_system.SetFilesystem(m_vfs); -    m_system.GetUserChannel().clear(); -      // Initialize system.      jauto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();      m_software_keyboard = android_keyboard.get(); @@ -277,11 +285,6 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string      });      // Initialize filesystem. -    m_manual_provider = std::make_unique<FileSys::ManualContentProvider>(); -    m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); -    m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, -                                     m_manual_provider.get()); -    m_system.GetFileSystemController().CreateFactories(*m_vfs);      ConfigureFilesystemProvider(filepath);      // Initialize account manager @@ -663,11 +666,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass c      }  } -void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation(JNIEnv* env, jclass clazz) { +void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz) {      // Create the default config.ini.      Config{};      // Initialize the emulated system.      EmulationSession::GetInstance().System().Initialize(); +    EmulationSession::GetInstance().InitializeSystem();  }  jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) { @@ -755,4 +759,49 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv*      }  } +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz, +                                                                  jlong jid) { +    auto bis_system = +        EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents(); +    if (!bis_system) { +        return ToJString(env, ""); +    } + +    auto applet_nca = +        bis_system->GetEntry(static_cast<u64>(jid), FileSys::ContentRecordType::Program); +    if (!applet_nca) { +        return ToJString(env, ""); +    } + +    return ToJString(env, applet_nca->GetFullPath()); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentAppletId(JNIEnv* env, jclass clazz, +                                                              jint jappletId) { +    EmulationSession::GetInstance().System().GetAppletManager().SetCurrentAppletId( +        static_cast<Service::AM::Applets::AppletId>(jappletId)); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass clazz, +                                                          jint jcabinetMode) { +    EmulationSession::GetInstance().System().GetAppletManager().SetCabinetMode( +        static_cast<Service::NFP::CabinetMode>(jcabinetMode)); +} + +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, jclass clazz) { +    auto bis_system = +        EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents(); +    if (!bis_system) { +        return false; +    } + +    // Query an applet to see if it's available +    auto applet_nca = +        bis_system->GetEntry(0x010000000000100Dull, FileSys::ContentRecordType::Program); +    if (!applet_nca) { +        return false; +    } +    return true; +} +  } // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index b1db87e41..0aa2b085b 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -43,6 +43,7 @@ public:      const Core::PerfStatsResults& PerfStats() const;      void ConfigureFilesystemProvider(const std::string& filepath); +    void InitializeSystem();      Core::SystemResultStatus InitializeEmulation(const std::string& filepath);      bool IsHandheldOnly(); 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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M20.6,5.6L19.5,8l-1.1,-2.4L16,4.5l2.4,-1.1L19.5,1l1.1,2.4L23,4.5z" /> +</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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</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 @@ +<?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" +    android:background="?attr/selectableItemBackground" +    android:clickable="true" +    android:focusable="true"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:orientation="horizontal" +        android:layout_gravity="center" +        android:padding="24dp"> + +        <ImageView +            android:id="@+id/icon" +            android:layout_width="24dp" +            android:layout_height="24dp" +            android:layout_marginEnd="20dp" +            android:layout_gravity="center_vertical" +            app:tint="?attr/colorOnSurface" /> + +        <LinearLayout +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:layout_weight="1" +            android:orientation="vertical" +            android:layout_gravity="center_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:textAlignment="viewStart" +                tools:text="@string/applets" /> + +            <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:textAlignment="viewStart" +                tools:text="@string/applets_description" /> + +        </LinearLayout> + +    </LinearLayout> + +</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 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="wrap_content"> + +    <androidx.recyclerview.widget.RecyclerView +        android:id="@+id/dialog_list" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:clipToPadding="false" +        android:fadeScrollbars="false" +        android:paddingVertical="12dp" +        android:scrollbars="vertical" /> + +</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 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:tools="http://schemas.android.com/tools" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:background="?attr/selectableItemBackground" +    android:clickable="true" +    android:focusable="true" +    android:orientation="horizontal" +    android:paddingHorizontal="24dp" +    android:paddingVertical="16dp"> + +    <ImageView +        android:id="@+id/icon" +        android:layout_width="20dp" +        android:layout_height="20dp" +        android:layout_gravity="center" +        tools:src="@drawable/ic_nfc" /> + +    <com.google.android.material.textview.MaterialTextView +        android:id="@+id/title" +        style="@style/TextAppearance.Material3.BodyMedium" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_marginStart="16dp" +        android:layout_gravity="center_vertical|start" +        android:textAlignment="viewStart" +        tools:text="List option" /> + +</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 @@ +<?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_applets" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:background="?attr/colorSurface"> + +    <com.google.android.material.appbar.AppBarLayout +        android:id="@+id/appbar_applets" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:fitsSystemWindows="true"> + +        <com.google.android.material.appbar.MaterialToolbar +            android:id="@+id/toolbar_applets" +            android:layout_width="match_parent" +            android:layout_height="?attr/actionBarSize" +            app:navigationIcon="@drawable/ic_back" +            app:title="@string/applets" /> + +    </com.google.android.material.appbar.AppBarLayout> + +    <androidx.recyclerview.widget.RecyclerView +        android:id="@+id/list_applets" +        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> 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 @@          <action              android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"              app:destination="@id/driverManagerFragment" /> +        <action +            android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment" +            app:destination="@id/appletLauncherFragment" />      </fragment>      <fragment @@ -102,5 +105,17 @@          android:id="@+id/driverManagerFragment"          android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"          android:label="DriverManagerFragment" /> +    <fragment +        android:id="@+id/appletLauncherFragment" +        android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment" +        android:label="AppletLauncherFragment" > +        <action +            android:id="@+id/action_appletLauncherFragment_to_cabinetLauncherDialogFragment" +            app:destination="@id/cabinetLauncherDialogFragment" /> +    </fragment> +    <dialog +        android:id="@+id/cabinetLauncherDialogFragment" +        android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment" +        android:label="CabinetLauncherDialogFragment" />  </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 @@      <string name="share_save_file">Share save file</string>      <string name="export_save_failed">Failed to export save</string> +    <!-- Applet launcher strings --> +    <string name="applets">Applet launcher</string> +    <string name="applets_description">Launch system applets using installed firmware</string> +    <string name="applets_error_firmware">Firmware not installed</string> +    <string name="applets_error_applet">Applet not available</string> +    <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> +    <string name="album_applet">Album</string> +    <string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string> +    <string name="mii_edit_applet">Mii edit</string> +    <string name="mii_edit_applet_description">View and edit Miis with the system editor</string> +    <string name="cabinet_applet">Cabinet</string> +    <string name="cabinet_applet_description">Edit and delete data stored on amiibo</string> +    <string name="cabinet_launcher">Cabinet launcher</string> +    <string name="cabinet_nickname_and_owner">Nickname and owner settings</string> +    <string name="cabinet_game_data_eraser">Game data eraser</string> +    <string name="cabinet_restorer">Restorer</string> +    <string name="cabinet_formatter">Formatter</string> +      <!-- About screen strings -->      <string name="gaia_is_not_real">Gaia isn\'t real</string>      <string name="copied_to_clipboard">Copied to clipboard</string> | 
