diff options
Diffstat (limited to 'src/android/app')
11 files changed, 155 insertions, 15 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 9a47e2bd8..fe79a701c 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -95,6 +95,7 @@ android { // builds a release build that doesn't need signing // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. register("relWithDebInfo") { + isDefault = true resValue("string", "app_name_suffixed", "yuzu Debug Release") signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = true @@ -122,6 +123,7 @@ android { flavorDimensions.add("version") productFlavors { create("mainline") { + isDefault = true dimension = "version" buildConfigField("Boolean", "PREMIUM", "false") } @@ -160,6 +162,11 @@ android { } } +tasks.create<Delete>("ktlintReset") { + delete(File(buildDir.path + File.separator + "intermediates/ktLint")) +} + +tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset") tasks.getByPath("preBuild").dependsOn("ktlintCheck") ktlint { diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 6184f3eb6..36e2dac98 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-3.0-or-later android:hasFragileUserData="false" android:supportsRtl="true" android:isGame="true" + android:appCategory="game" android:localeConfig="@xml/locales_config" android:banner="@drawable/tv_banner" android:extractNativeLibs="true" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index aadc445f9..9f859b442 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -3,19 +3,25 @@ package org.yuzu.yuzu_emu.adapters +import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.HomeSetting -class HomeSettingAdapter(private val activity: AppCompatActivity, var options: List<HomeSetting>) : +class HomeSettingAdapter( + private val activity: AppCompatActivity, + private val viewLifecycle: LifecycleOwner, + var options: List<HomeSetting> +) : RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), View.OnClickListener { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { @@ -79,6 +85,22 @@ class HomeSettingAdapter(private val activity: AppCompatActivity, var options: L binding.optionDescription.alpha = 0.5f binding.optionIcon.alpha = 0.5f } + + option.details.observe(viewLifecycle) { updateOptionDetails(it) } + binding.optionDetail.postDelayed( + { + binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE + binding.optionDetail.isSelected = true + }, + 3000 + ) + } + + private fun updateOptionDetails(detailString: String) { + if (detailString.isNotEmpty()) { + binding.optionDetail.text = detailString + binding.optionDetail.visibility = View.VISIBLE + } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 0e7c1ba88..25b9d4018 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -297,11 +297,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { emulationActivity?.let { it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { Settings.LayoutOption_MobileLandscape -> - ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } } } 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 c001af892..d5e793491 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 @@ -129,7 +129,11 @@ class HomeSettingsFragment : Fragment() { mainActivity.getGamesDirectory.launch( Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data ) - } + }, + { true }, + 0, + 0, + homeViewModel.gamesDir ) ) add( @@ -201,7 +205,11 @@ class HomeSettingsFragment : Fragment() { binding.homeSettingsList.apply { layoutManager = LinearLayoutManager(requireContext()) - adapter = HomeSettingAdapter(requireActivity() as AppCompatActivity, optionsList) + adapter = HomeSettingAdapter( + requireActivity() as AppCompatActivity, + viewLifecycleOwner, + optionsList + ) } setInsets() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt index 522d07c37..498c222fa 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt @@ -3,6 +3,9 @@ package org.yuzu.yuzu_emu.model +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + data class HomeSetting( val titleId: Int, val descriptionId: Int, @@ -10,5 +13,6 @@ data class HomeSetting( val onClick: () -> Unit, val isEnabled: () -> Boolean = { true }, val disabledTitleId: Int = 0, - val disabledMessageId: Int = 0 + val disabledMessageId: Int = 0, + val details: LiveData<String> = MutableLiveData("") ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index e13d84c9c..a48ef7a88 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -3,9 +3,15 @@ package org.yuzu.yuzu_emu.model +import android.net.Uri +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.utils.GameHelper class HomeViewModel : ViewModel() { private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>() @@ -17,6 +23,14 @@ class HomeViewModel : ViewModel() { private val _shouldPageForward = MutableLiveData(false) val shouldPageForward: LiveData<Boolean> get() = _shouldPageForward + private val _gamesDir = MutableLiveData( + Uri.parse( + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + .getString(GameHelper.KEY_GAME_PATH, "") + ).path ?: "" + ) + val gamesDir: LiveData<String> get() = _gamesDir + var navigatedToSetup = false init { @@ -40,4 +54,9 @@ class HomeViewModel : ViewModel() { fun setShouldPageForward(pageForward: Boolean) { _shouldPageForward.value = pageForward } + + fun setGamesDir(activity: FragmentActivity, dir: String) { + ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) + _gamesDir.value = dir + } } 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 f77d06262..aaf3a0ec1 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 @@ -290,6 +290,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ).show() gamesViewModel.reloadGames(true) + homeViewModel.setGamesDir(this, result.path!!) } val getProdKey = diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index f8e7eeca7..f71d0a098 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.model.MinimalDocumentFile object GameHelper { const val KEY_GAME_PATH = "game_path" @@ -29,15 +30,7 @@ object GameHelper { // Ensure keys are loaded so that ROM metadata can be decrypted. NativeLibrary.reloadKeys() - val children = FileUtil.listFiles(context, gamesUri) - for (file in children) { - if (!file.isDirectory) { - // Check that the file has an extension we care about before trying to read out of it. - if (Game.extensions.contains(FileUtil.getExtension(file.uri))) { - games.add(getGame(file.uri)) - } - } - } + addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3) // Cache list of games found on disk val serializedGames = mutableSetOf<String>() @@ -52,6 +45,30 @@ object GameHelper { return games.toList() } + private fun addGamesRecursive( + games: MutableList<Game>, + files: Array<MinimalDocumentFile>, + depth: Int + ) { + if (depth <= 0) { + return + } + + files.forEach { + if (it.isDirectory) { + addGamesRecursive( + games, + FileUtil.listFiles(YuzuApplication.appContext, it.uri), + depth - 1 + ) + } else { + if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { + games.add(getGame(it.uri)) + } + } + } + } + private fun getGame(uri: Uri): Game { val filePath = uri.toString() var name = NativeLibrary.getTitle(filePath) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index c23b2f19e..8b99d1d6e 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -30,6 +30,7 @@ #include "core/cpu_manager.h" #include "core/crypto/key_manager.h" #include "core/file_sys/card_image.h" +#include "core/file_sys/content_archive.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/submission_package.h" #include "core/file_sys/vfs.h" @@ -224,6 +225,42 @@ public: m_system.Renderer().NotifySurfaceChanged(); } + void ConfigureFilesystemProvider(const std::string& filepath) { + const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read); + if (!file) { + return; + } + + auto loader = Loader::GetLoader(m_system, file); + if (!loader) { + return; + } + + const auto file_type = loader->GetFileType(); + if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { + return; + } + + u64 program_id = 0; + const auto res2 = loader->ReadProgramId(program_id); + if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { + m_manual_provider->AddEntry(FileSys::TitleType::Application, + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), + program_id, file); + } else if (res2 == Loader::ResultStatus::Success && + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { + const auto nsp = file_type == Loader::FileType::NSP + ? std::make_shared<FileSys::NSP>(file) + : FileSys::XCI{file}.GetSecurePartitionNSP(); + for (const auto& title : nsp->GetNCAs()) { + for (const auto& entry : title.second) { + m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first, + entry.second->GetBaseFile()); + } + } + } + } + Core::SystemResultStatus InitializeEmulation(const std::string& filepath) { std::scoped_lock lock(m_mutex); @@ -254,8 +291,14 @@ public: std::move(android_keyboard), // Software Keyboard nullptr, // Web Browser }); + + // 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 m_profile_manager = std::make_unique<Service::Account::ProfileManager>(); @@ -489,6 +532,7 @@ private: bool m_is_paused{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; + std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider; // GPU driver parameters std::shared_ptr<Common::DynamicLibrary> m_vulkan_library; diff --git a/src/android/app/src/main/res/layout/card_home_option.xml b/src/android/app/src/main/res/layout/card_home_option.xml index dc289db17..f9f1d89fb 100644 --- a/src/android/app/src/main/res/layout/card_home_option.xml +++ b/src/android/app/src/main/res/layout/card_home_option.xml @@ -53,6 +53,23 @@ android:layout_marginTop="5dp" tools:text="@string/install_prod_keys_description" /> + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.Material3.LabelMedium" + android:id="@+id/option_detail" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="14sp" + android:textStyle="bold" + android:singleLine="true" + android:marqueeRepeatLimit="marquee_forever" + android:ellipsize="none" + android:requiresFadingEdge="horizontal" + android:layout_marginTop="5dp" + android:visibility="gone" + tools:visibility="visible" + tools:text="/tree/primary:Games" /> + </LinearLayout> </LinearLayout> |